wip
wip wip - everything but publish, autoplay, and styling collection publishing add channel to collection publish cleanup wip bump clear mass add after success move collection item management controls redirect replace to published collection id bump playlist selector on create bump use new collection add ui element bump wip gitignore add content json wip bump context add to playlist basic collections page style pass wip wip: edits, buttons, styles... change fileAuthor to claimAuthor update, pending bugfixes, delete modal progress, collection header, other bugfixes bump cleaning show page bugfix builtin collection headers no playlists, no grid title wip style tweaks use normal looking claim previews for collection tiles add collection changes style library previews collection menulist for delete/view on library delete modal works for unpublished rearrange collection publish tabs clean up collection publishing and items show on odysee begin collectoin edit header and css renaming better thumbnails bump fix collection publish redirect view collection in menu does something copy and thumbs list previews, pending, context menus, list page enter to add collection, lists page empty state playable lists only, delete feature, bump put fileListDownloaded back better collection titles improve collection claim details fix horiz more icon fix up channel page style, copy, bump refactor preview overlay properties, fix reposts showing as floppydisk add watch later toast, small overlay properties on wunderbar results, fix collection actions buttons bump cleanup cleaning, refactoring bump preview thumb styling, cleanup support discover page lists search sync, bump bump, fix sync more enforce builtin order for now new lists page empty state try to indicate unpublished edits in lists bump fix autoplay and linting consts, fix autoplay bugs fixes cleanup fix, bump lists experimental ui, fixes refactor listIndex out hack in collection fallback thumb bump
This commit is contained in:
parent
46d258c439
commit
ca116ba010
111 changed files with 3504 additions and 336 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -23,8 +23,12 @@ package-lock.json
|
|||
/web/.env.defaults
|
||||
/custom/*
|
||||
/custom/homepages/*
|
||||
/custom/content/*
|
||||
!/custom/content/default.json
|
||||
!/custom/content/test.json
|
||||
!/custom/homepages/.gitkeep
|
||||
!/custom/homepages
|
||||
!/custom/content
|
||||
!/custom/homepage.example.js
|
||||
!/custom/robots.disallowall
|
||||
!/custom/robots.allowall
|
||||
|
|
|
@ -143,7 +143,7 @@
|
|||
"imagesloaded": "^4.1.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#ecfcc95bebbdbe303b3ea065134457a5e168fb89",
|
||||
"lbry-redux": "lbryio/lbry-redux#5cd9e7601cdc1c81b8d0e2d2f02e9b9d2fb86575",
|
||||
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -1917,5 +1917,35 @@
|
|||
"Set to current date and time": "Set to current date and time",
|
||||
"Remove custom release date": "Remove custom release date",
|
||||
"%SITE_NAME% login": "%SITE_NAME% login",
|
||||
"Lists": "Lists",
|
||||
"Watch Later": "Watch Later",
|
||||
"Add to Lists": "Add to Lists",
|
||||
"Nothing in %collection_name%": "Nothing in %collection_name%",
|
||||
"Playlists": "Playlists",
|
||||
"Edit List": "Edit List",
|
||||
"Delete List": "Delete List",
|
||||
"Private": "Private",
|
||||
"View List": "View List",
|
||||
"Delete Collection": "Delete Collection",
|
||||
"Info": "Info",
|
||||
"Publishes": "Publishes",
|
||||
"Add To...": "Add To...",
|
||||
"Unpublished Edits": "Unpublished Edits",
|
||||
"Report channel": "Report channel",
|
||||
"List": "List",
|
||||
"Items": "Items",
|
||||
"Credits": "Credits",
|
||||
"MyAwesomeCollection": "MyAwesomeCollection",
|
||||
"My Awesome Collection": "My Awesome Collection",
|
||||
"This collection has no items.": "This collection has no items.",
|
||||
"Select File": "Select File",
|
||||
"File Selected": "File Selected",
|
||||
"Url": "Url",
|
||||
"URL Selected": "URL Selected",
|
||||
"Keep": "Keep",
|
||||
"Add this claim to a list": "Add this claim to a list",
|
||||
"List is Empty": "List is Empty",
|
||||
"Confirm Collection Unpublish": "Confirm Collection Unpublish",
|
||||
"This will permanently delete the list.": "This will permanently delete the list.",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { selectGetSyncErrorMessage, selectSyncFatalError } from 'redux/selectors
|
|||
import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user';
|
||||
import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectUnclaimedRewards } from 'redux/selectors/rewards';
|
||||
import { doFetchChannelListMine, selectMyChannelUrls, SETTINGS } from 'lbry-redux';
|
||||
import { doFetchChannelListMine, doFetchCollectionListMine, SETTINGS, selectMyChannelUrls } from 'lbry-redux';
|
||||
import {
|
||||
makeSelectClientSetting,
|
||||
selectLanguage,
|
||||
|
@ -52,6 +52,7 @@ const select = (state) => ({
|
|||
const perform = (dispatch) => ({
|
||||
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||
fetchCollectionListMine: () => dispatch(doFetchCollectionListMine()),
|
||||
setLanguage: (language) => dispatch(doSetLanguage(language)),
|
||||
signIn: () => dispatch(doSignIn()),
|
||||
requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()),
|
||||
|
|
|
@ -61,6 +61,7 @@ type Props = {
|
|||
},
|
||||
fetchAccessToken: () => void,
|
||||
fetchChannelListMine: () => void,
|
||||
fetchCollectionListMine: () => void,
|
||||
signIn: () => void,
|
||||
requestDownloadUpgrade: () => void,
|
||||
onSignedIn: () => void,
|
||||
|
@ -94,6 +95,7 @@ function App(props: Props) {
|
|||
user,
|
||||
fetchAccessToken,
|
||||
fetchChannelListMine,
|
||||
fetchCollectionListMine,
|
||||
signIn,
|
||||
autoUpdateDownloaded,
|
||||
isUpgradeAvailable,
|
||||
|
@ -233,8 +235,9 @@ function App(props: Props) {
|
|||
|
||||
// @if TARGET='app'
|
||||
fetchChannelListMine(); // This is fetched after a user is signed in on web
|
||||
fetchCollectionListMine();
|
||||
// @endif
|
||||
}, [appRef, fetchAccessToken, fetchChannelListMine]);
|
||||
}, [appRef, fetchAccessToken, fetchChannelListMine, fetchCollectionListMine]);
|
||||
|
||||
useEffect(() => {
|
||||
// $FlowFixMe
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, SETTINGS } from 'lbry-redux';
|
||||
import { makeSelectClaimForUri, SETTINGS, COLLECTIONS_CONSTS, makeSelectNextUrlForCollectionAndUrl } from 'lbry-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { makeSelectIsPlayerFloating, makeSelectNextUnplayedRecommended } from 'redux/selectors/content';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
|
@ -8,11 +8,24 @@ import AutoplayCountdown from './view';
|
|||
import { selectModal } from 'redux/selectors/app';
|
||||
|
||||
/*
|
||||
AutoplayCountdown does not fetch it's own next content to play, it relies on <RecommendedContent> being rendered. This is dumb but I'm just the guy who noticed
|
||||
AutoplayCountdown does not fetch it's own next content to play, it relies on <RecommendedContent> being rendered.
|
||||
This is dumb but I'm just the guy who noticed -kj
|
||||
*/
|
||||
const select = (state, props) => {
|
||||
const nextRecommendedUri = makeSelectNextUnplayedRecommended(props.uri)(state);
|
||||
const { location } = props;
|
||||
const { search } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
||||
|
||||
let nextRecommendedUri;
|
||||
if (collectionId) {
|
||||
nextRecommendedUri = makeSelectNextUrlForCollectionAndUrl(collectionId, props.uri)(state);
|
||||
} else {
|
||||
nextRecommendedUri = makeSelectNextUnplayedRecommended(props.uri)(state);
|
||||
}
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
nextRecommendedUri,
|
||||
nextRecommendedClaim: makeSelectClaimForUri(nextRecommendedUri)(state),
|
||||
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||
|
|
|
@ -6,18 +6,19 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { withRouter } from 'react-router';
|
||||
import debounce from 'util/debounce';
|
||||
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 150;
|
||||
const CLASSNAME_AUTOPLAY_COUNTDOWN = 'autoplay-countdown';
|
||||
|
||||
type Props = {
|
||||
history: { push: string => void },
|
||||
history: { push: (string) => void },
|
||||
nextRecommendedClaim: ?StreamClaim,
|
||||
nextRecommendedUri: string,
|
||||
isFloating: boolean,
|
||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
||||
doPlayUri: string => void,
|
||||
doPlayUri: (string) => void,
|
||||
modal: { id: string, modalProps: {} },
|
||||
collectionId?: string,
|
||||
};
|
||||
|
||||
function AutoplayCountdown(props: Props) {
|
||||
|
@ -29,6 +30,7 @@ function AutoplayCountdown(props: Props) {
|
|||
isFloating,
|
||||
history: { push },
|
||||
modal,
|
||||
collectionId,
|
||||
} = props;
|
||||
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
|
||||
|
||||
|
@ -44,6 +46,11 @@ function AutoplayCountdown(props: Props) {
|
|||
let navigateUrl;
|
||||
if (nextTitle) {
|
||||
navigateUrl = formatLbryUrlForWeb(nextRecommendedUri);
|
||||
if (collectionId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
navigateUrl = navigateUrl + `?` + collectionParams.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const doNavigate = useCallback(() => {
|
||||
|
@ -71,7 +78,7 @@ function AutoplayCountdown(props: Props) {
|
|||
// Ensure correct 'setTimerPaused' on initial render.
|
||||
setTimerPaused(shouldPauseAutoplay());
|
||||
|
||||
const handleScroll = debounce(e => {
|
||||
const handleScroll = debounce((e) => {
|
||||
setTimerPaused(shouldPauseAutoplay());
|
||||
}, DEBOUNCE_SCROLL_HANDLER_MS);
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ type Props = {
|
|||
tileLayout: boolean,
|
||||
viewHiddenChannels: boolean,
|
||||
doResolveUris: (Array<string>, boolean) => void,
|
||||
claimType: string,
|
||||
};
|
||||
|
||||
function ChannelContent(props: Props) {
|
||||
|
@ -49,6 +50,7 @@ function ChannelContent(props: Props) {
|
|||
tileLayout,
|
||||
viewHiddenChannels,
|
||||
doResolveUris,
|
||||
claimType,
|
||||
} = props;
|
||||
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
|
@ -58,6 +60,7 @@ function ChannelContent(props: Props) {
|
|||
} = useHistory();
|
||||
const url = `${pathname}${search}`;
|
||||
const claimId = claim && claim.claim_id;
|
||||
const showFilters = !claimType || claimType === 'stream';
|
||||
|
||||
function handleInputChange(e) {
|
||||
const { value } = e.target;
|
||||
|
@ -134,25 +137,30 @@ function ChannelContent(props: Props) {
|
|||
<ClaimListDiscover
|
||||
showHiddenByUser={viewHiddenChannels}
|
||||
forceShowReposts
|
||||
hideFilters={!showFilters}
|
||||
hideAdvancedFilter={!showFilters}
|
||||
tileLayout={tileLayout}
|
||||
uris={searchResults}
|
||||
channelIds={[claim.claim_id]}
|
||||
claimType={claimType}
|
||||
feeAmount={CS.FEE_AMOUNT_ANY}
|
||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||
pageSize={defaultPageSize}
|
||||
infiniteScroll={defaultInfiniteScroll}
|
||||
injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && <Ads type="video" />}
|
||||
meta={
|
||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||
<Icon icon={ICONS.SEARCH} />
|
||||
<FormField
|
||||
className="wunderbar__input--inline"
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
type="text"
|
||||
placeholder={__('Search')}
|
||||
/>
|
||||
</Form>
|
||||
showFilters && (
|
||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||
<Icon icon={ICONS.SEARCH} />
|
||||
<FormField
|
||||
className="wunderbar__input--inline"
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
type="text"
|
||||
placeholder={__('Search')}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
isChannel
|
||||
channelIsMine={channelIsMine}
|
||||
|
|
|
@ -6,13 +6,22 @@ import Button from 'component/button';
|
|||
|
||||
type Props = {
|
||||
doOpenModal: (string, {}) => void,
|
||||
claim: StreamClaim,
|
||||
abandonActionCallback: any => void,
|
||||
claim: Claim,
|
||||
abandonActionCallback: (any) => void,
|
||||
iconSize: number,
|
||||
};
|
||||
|
||||
export default function ClaimAbandonButton(props: Props) {
|
||||
const { doOpenModal, claim, abandonActionCallback } = props;
|
||||
const { value_type } = claim || {};
|
||||
let buttonLabel;
|
||||
if (value_type === 'channel') {
|
||||
buttonLabel = __('Delete Channel');
|
||||
} else if (value_type === 'collection') {
|
||||
buttonLabel = __('Delete List');
|
||||
} else if (value_type === 'stream') {
|
||||
buttonLabel = __('Delete Publish');
|
||||
}
|
||||
|
||||
function abandonClaim() {
|
||||
doOpenModal(MODALS.CONFIRM_CLAIM_REVOKE, { claim: claim, cb: abandonActionCallback });
|
||||
|
@ -21,7 +30,7 @@ export default function ClaimAbandonButton(props: Props) {
|
|||
return (
|
||||
<Button
|
||||
disabled={!claim}
|
||||
label={__('Delete Channel')}
|
||||
label={buttonLabel}
|
||||
button="alt"
|
||||
iconColor="red"
|
||||
icon={ICONS.DELETE}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectChannelForClaimUri } from 'lbry-redux';
|
||||
import FileAuthor from './view';
|
||||
import ClaimAuthor from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
channelUri: makeSelectChannelForClaimUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select)(FileAuthor);
|
||||
export default connect(select)(ClaimAuthor);
|
|
@ -7,7 +7,7 @@ type Props = {
|
|||
hideActions?: boolean,
|
||||
};
|
||||
|
||||
function FileAuthor(props: Props) {
|
||||
function ClaimAuthor(props: Props) {
|
||||
const { channelUri, hideActions } = props;
|
||||
|
||||
return channelUri ? (
|
||||
|
@ -17,4 +17,4 @@ function FileAuthor(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default FileAuthor;
|
||||
export default ClaimAuthor;
|
23
ui/component/claimCollectionAdd/index.js
Normal file
23
ui/component/claimCollectionAdd/index.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ClaimCollectionAdd from './view';
|
||||
import { withRouter } from 'react-router';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
doLocalCollectionCreate,
|
||||
selectBuiltinCollections,
|
||||
selectMyPublishedCollections,
|
||||
selectMyUnpublishedCollections,
|
||||
} from 'lbry-redux';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
builtin: selectBuiltinCollections(state),
|
||||
published: selectMyPublishedCollections(state),
|
||||
unpublished: selectMyUnpublishedCollections(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
addCollection: (name, items, type) => dispatch(doLocalCollectionCreate(name, items, type)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(ClaimCollectionAdd));
|
133
ui/component/claimCollectionAdd/view.jsx
Normal file
133
ui/component/claimCollectionAdd/view.jsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import type { ElementRef } from 'react';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import { FormField } from 'component/common/form';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import CollectionSelectItem from 'component/collectionSelectItem';
|
||||
|
||||
type Props = {
|
||||
claim: Claim,
|
||||
builtin: any,
|
||||
published: any,
|
||||
unpublished: any,
|
||||
addCollection: (string, Array<string>, string) => void,
|
||||
closeModal: () => void,
|
||||
uri: string,
|
||||
};
|
||||
|
||||
const ClaimCollectionAdd = (props: Props) => {
|
||||
const { builtin, published, unpublished, addCollection, claim, closeModal, uri } = props;
|
||||
const buttonref: ElementRef<any> = React.useRef();
|
||||
const permanentUrl = claim && claim.permanent_url;
|
||||
const isChannel = claim && claim.value_type === 'channel';
|
||||
|
||||
const [addNewCollection, setAddNewCollection] = React.useState(false);
|
||||
const [newCollectionName, setNewCollectionName] = React.useState('');
|
||||
|
||||
// TODO: when other collection types added, filter list in context
|
||||
// const isPlayable =
|
||||
// claim &&
|
||||
// claim.value &&
|
||||
// // $FlowFixMe
|
||||
// claim.value.stream_type &&
|
||||
// (claim.value.stream_type === 'audio' || claim.value.stream_type === 'video');
|
||||
|
||||
function handleNameInput(e) {
|
||||
const { value } = e.target;
|
||||
setNewCollectionName(value);
|
||||
}
|
||||
|
||||
function handleAddCollection() {
|
||||
addCollection(newCollectionName, [permanentUrl], isChannel ? 'collection' : 'playlist');
|
||||
setNewCollectionName('');
|
||||
}
|
||||
|
||||
function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||
const KEYCODE_ENTER = 13;
|
||||
if (e.keyCode === KEYCODE_ENTER) {
|
||||
e.preventDefault();
|
||||
buttonref.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
function onTextareaFocus() {
|
||||
window.addEventListener('keydown', altEnterListener);
|
||||
}
|
||||
|
||||
function onTextareaBlur() {
|
||||
window.removeEventListener('keydown', altEnterListener);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={__('Add To...')}
|
||||
actions={
|
||||
<div className="card__body">
|
||||
{uri && (
|
||||
<fieldset-section>
|
||||
<div className={'card__body-scrollable'}>
|
||||
{(Object.values(builtin): any)
|
||||
// $FlowFixMe
|
||||
.filter((list) => (isChannel ? list.type === 'collection' : list.type === 'playlist'))
|
||||
.map((l) => {
|
||||
const { id } = l;
|
||||
return <CollectionSelectItem collectionId={id} uri={permanentUrl} key={id} category={'builtin'} />;
|
||||
})}
|
||||
{unpublished &&
|
||||
(Object.values(unpublished): any)
|
||||
// $FlowFixMe
|
||||
.filter((list) => (isChannel ? list.type === 'collection' : list.type === 'playlist'))
|
||||
.map((l) => {
|
||||
const { id } = l;
|
||||
return (
|
||||
<CollectionSelectItem collectionId={id} uri={permanentUrl} key={id} category={'unpublished'} />
|
||||
);
|
||||
})}
|
||||
{published &&
|
||||
(Object.values(published): any).map((l) => {
|
||||
// $FlowFixMe
|
||||
const { id } = l;
|
||||
return (
|
||||
<CollectionSelectItem collectionId={id} uri={permanentUrl} key={id} category={'published'} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</fieldset-section>
|
||||
)}
|
||||
<fieldset-section>
|
||||
{addNewCollection && (
|
||||
<FormField
|
||||
autoFocus
|
||||
type="text"
|
||||
name="new_collection"
|
||||
value={newCollectionName}
|
||||
label={'New List Title'}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
inputButton={
|
||||
<Button
|
||||
button={'secondary'}
|
||||
icon={ICONS.ADD}
|
||||
disabled={!newCollectionName.length}
|
||||
onClick={() => handleAddCollection()}
|
||||
ref={buttonref}
|
||||
/>
|
||||
}
|
||||
onChange={handleNameInput}
|
||||
/>
|
||||
)}
|
||||
{!addNewCollection && (
|
||||
<Button button={'link'} label={'New List'} onClick={() => setAddNewCollection(true)} />
|
||||
)}
|
||||
</fieldset-section>
|
||||
<div className="card__actions">
|
||||
<Button button="secondary" label={__('Done')} onClick={closeModal} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ClaimCollectionAdd;
|
12
ui/component/claimCollectionAddButton/index.js
Normal file
12
ui/component/claimCollectionAddButton/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import PlaylistAddButton from './view';
|
||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doOpenModal,
|
||||
})(PlaylistAddButton);
|
40
ui/component/claimCollectionAddButton/view.jsx
Normal file
40
ui/component/claimCollectionAddButton/view.jsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
// @flow
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
doOpenModal: (string, {}) => void,
|
||||
fileAction?: boolean,
|
||||
type?: boolean,
|
||||
claim: Claim,
|
||||
};
|
||||
|
||||
export default function CollectionAddButton(props: Props) {
|
||||
const { doOpenModal, uri, fileAction, type = 'playlist', claim } = props;
|
||||
|
||||
// $FlowFixMe
|
||||
const streamType = (claim && claim.value && claim.value.stream_type) || '';
|
||||
const isPlayable = streamType === 'video' || streamType === 'audio';
|
||||
|
||||
if (!isPlayable) return null;
|
||||
return (
|
||||
<Button
|
||||
button={fileAction ? undefined : 'alt'}
|
||||
className={classnames({ 'button--file-action': fileAction })}
|
||||
icon={fileAction ? ICONS.ADD : ICONS.LIBRARY}
|
||||
iconSize={fileAction ? 22 : undefined}
|
||||
label={uri ? __('Save') : 'New List'}
|
||||
requiresAuth={IS_WEB}
|
||||
title={__('Add this claim to a list')}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
doOpenModal(MODALS.COLLECTION_ADD, { uri, type });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -9,6 +9,4 @@ const select = (state) => ({
|
|||
claimsByUri: selectClaimsByUri(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({});
|
||||
|
||||
export default connect(select, perform)(ClaimList);
|
||||
export default connect(select)(ClaimList);
|
||||
|
|
|
@ -45,6 +45,7 @@ type Props = {
|
|||
livestreamMap?: { [string]: any },
|
||||
searchOptions?: any,
|
||||
channelIsMine: boolean,
|
||||
collectionId?: string,
|
||||
};
|
||||
|
||||
export default function ClaimList(props: Props) {
|
||||
|
@ -76,6 +77,7 @@ export default function ClaimList(props: Props) {
|
|||
livestreamMap,
|
||||
searchOptions,
|
||||
channelIsMine,
|
||||
collectionId,
|
||||
} = props;
|
||||
|
||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||
|
@ -135,6 +137,7 @@ export default function ClaimList(props: Props) {
|
|||
properties={renderProperties}
|
||||
live={resolveLive(index)}
|
||||
channelIsMine={channelIsMine}
|
||||
collectionId={collectionId}
|
||||
/>
|
||||
))}
|
||||
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
||||
|
@ -194,6 +197,7 @@ export default function ClaimList(props: Props) {
|
|||
renderActions={renderActions}
|
||||
showUserBlocked={showHiddenByUser}
|
||||
showHiddenByUser={showHiddenByUser}
|
||||
collectionId={collectionId}
|
||||
customShouldHide={(claim: StreamClaim) => {
|
||||
// Hack to hide spee.ch thumbnail publishes
|
||||
// If it meets these requirements, it was probably uploaded here:
|
||||
|
|
|
@ -29,7 +29,7 @@ type Props = {
|
|||
channelIds?: Array<string>,
|
||||
tileLayout: boolean,
|
||||
doSetClientSetting: (string, boolean, ?boolean) => void,
|
||||
setPage: number => void,
|
||||
setPage: (number) => void,
|
||||
hideFilters: boolean,
|
||||
searchInLanguage: boolean,
|
||||
languageSetting: string,
|
||||
|
@ -74,7 +74,7 @@ function ClaimListHeader(props: Props) {
|
|||
const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY);
|
||||
const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds;
|
||||
const feeAmountParam = urlParams.get('fee_amount') || feeAmount || CS.FEE_AMOUNT_ANY;
|
||||
const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL);
|
||||
const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL && claimType === CS.CLAIM_COLLECTION);
|
||||
const isFiltered = () =>
|
||||
Boolean(
|
||||
urlParams.get(CS.FRESH_KEY) ||
|
||||
|
@ -144,7 +144,7 @@ function ClaimListHeader(props: Props) {
|
|||
|
||||
function buildUrl(delta) {
|
||||
const newUrlParams = new URLSearchParams(location.search);
|
||||
CS.KEYS.forEach(k => {
|
||||
CS.KEYS.forEach((k) => {
|
||||
// $FlowFixMe get() can return null
|
||||
if (urlParams.get(k) !== null) newUrlParams.set(k, urlParams.get(k));
|
||||
});
|
||||
|
@ -161,7 +161,11 @@ function ClaimListHeader(props: Props) {
|
|||
}
|
||||
break;
|
||||
case CS.CONTENT_KEY:
|
||||
if (delta.value === CS.CLAIM_CHANNEL || delta.value === CS.CLAIM_REPOST) {
|
||||
if (
|
||||
delta.value === CS.CLAIM_CHANNEL ||
|
||||
delta.value === CS.CLAIM_REPOST ||
|
||||
delta.value === CS.CLAIM_COLLECTION
|
||||
) {
|
||||
newUrlParams.delete(CS.DURATION_KEY);
|
||||
newUrlParams.set(CS.CONTENT_KEY, delta.value);
|
||||
} else if (delta.value === CS.CONTENT_ALL) {
|
||||
|
@ -212,13 +216,13 @@ function ClaimListHeader(props: Props) {
|
|||
<>
|
||||
<div className="claim-search__wrapper">
|
||||
<div className="claim-search__top">
|
||||
<div className="claim-search__menu-group">
|
||||
{!hideFilters &&
|
||||
CS.ORDER_BY_TYPES.map(type => (
|
||||
{!hideFilters && (
|
||||
<div className="claim-search__menu-group">
|
||||
{CS.ORDER_BY_TYPES.map((type) => (
|
||||
<Button
|
||||
key={type}
|
||||
button="alt"
|
||||
onClick={e =>
|
||||
onClick={(e) =>
|
||||
handleChange({
|
||||
key: CS.ORDER_BY_KEY,
|
||||
value: type,
|
||||
|
@ -233,8 +237,8 @@ function ClaimListHeader(props: Props) {
|
|||
label={__(toCapitalCase(type))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
<div className="claim-search__menu-group">
|
||||
{!hideAdvancedFilter && !SIMPLE_SITE && (
|
||||
<Button
|
||||
|
@ -276,21 +280,23 @@ function ClaimListHeader(props: Props) {
|
|||
name="trending_time"
|
||||
label={__('How Fresh')}
|
||||
value={freshnessParam}
|
||||
onChange={e =>
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
key: CS.FRESH_KEY,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{CS.FRESH_TYPES.map(time => (
|
||||
{CS.FRESH_TYPES.map((time) => (
|
||||
<option key={time} value={time}>
|
||||
{/* i18fixme */}
|
||||
{time === CS.FRESH_DAY && __('Today')}
|
||||
{time !== CS.FRESH_ALL &&
|
||||
time !== CS.FRESH_DEFAULT &&
|
||||
time !== CS.FRESH_DAY &&
|
||||
__('This ' + toCapitalCase(time)) /* yes, concat before i18n, since it is read from const */}
|
||||
{
|
||||
time !== CS.FRESH_ALL &&
|
||||
time !== CS.FRESH_DEFAULT &&
|
||||
time !== CS.FRESH_DAY &&
|
||||
__('This ' + toCapitalCase(time)) /* yes, concat before i18n, since it is read from const */
|
||||
}
|
||||
{time === CS.FRESH_ALL && __('All time')}
|
||||
{time === CS.FRESH_DEFAULT && __('Default')}
|
||||
</option>
|
||||
|
@ -314,18 +320,19 @@ function ClaimListHeader(props: Props) {
|
|||
name="claimType"
|
||||
label={__('Content Type')}
|
||||
value={contentTypeParam || CS.CONTENT_ALL}
|
||||
onChange={e =>
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
key: CS.CONTENT_KEY,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{CS.CONTENT_TYPES.map(type => {
|
||||
{CS.CONTENT_TYPES.map((type) => {
|
||||
if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIdsParam)) {
|
||||
return (
|
||||
<option key={type} value={type}>
|
||||
{/* i18fixme */}
|
||||
{type === CS.CLAIM_COLLECTION && __('List')}
|
||||
{type === CS.CLAIM_CHANNEL && __('Channel')}
|
||||
{type === CS.CLAIM_REPOST && __('Repost')}
|
||||
{type === CS.FILE_VIDEO && __('Video')}
|
||||
|
@ -358,7 +365,7 @@ function ClaimListHeader(props: Props) {
|
|||
name="claimType"
|
||||
label={__('Language')}
|
||||
value={languageValue || CS.LANGUAGES_ALL}
|
||||
onChange={e =>
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
key: CS.LANGUAGE_KEY,
|
||||
value: e.target.value,
|
||||
|
@ -398,14 +405,14 @@ function ClaimListHeader(props: Props) {
|
|||
)
|
||||
}
|
||||
value={durationParam || CS.DURATION_ALL}
|
||||
onChange={e =>
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
key: CS.DURATION_KEY,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{CS.DURATION_TYPES.map(dur => (
|
||||
{CS.DURATION_TYPES.map((dur) => (
|
||||
<option key={dur} value={dur}>
|
||||
{/* i18fixme */}
|
||||
{dur === CS.DURATION_SHORT && __('Short (< 4 minutes)')}
|
||||
|
@ -428,7 +435,7 @@ function ClaimListHeader(props: Props) {
|
|||
type="select"
|
||||
name="paidcontent"
|
||||
value={feeAmountParam}
|
||||
onChange={e =>
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
key: CS.FEE_AMOUNT_KEY,
|
||||
value: e.target.value,
|
||||
|
|
|
@ -1,20 +1,48 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, makeSelectClaimIsMine } from 'lbry-redux';
|
||||
import {
|
||||
doCollectionEdit,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectCollectionForIdHasClaimUrl,
|
||||
makeSelectNameForCollectionId,
|
||||
makeSelectCollectionIsMine,
|
||||
COLLECTIONS_CONSTS,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { doToggleMuteChannel } from 'redux/actions/blocked';
|
||||
import { doCommentModBlock, doCommentModUnBlock } from 'redux/actions/comments';
|
||||
import { makeSelectChannelIsBlocked } from 'redux/selectors/comments';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { makeSelectUserPropForProp } from 'redux/selectors/user';
|
||||
import ClaimPreview from './view';
|
||||
import * as USER from 'constants/user';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: props.channelIsMine ? (props.isRepost ? makeSelectClaimIsMine(props.uri)(state) : true) : makeSelectClaimIsMine(props.uri)(state),
|
||||
channelIsMuted: makeSelectChannelIsMuted(props.uri)(state),
|
||||
channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||
const permanentUri = claim && claim.permanent_url;
|
||||
return {
|
||||
claim,
|
||||
claimIsMine: props.channelIsMine
|
||||
? props.isRepost
|
||||
? makeSelectClaimIsMine(props.uri)(state)
|
||||
: true
|
||||
: makeSelectClaimIsMine(props.uri)(state),
|
||||
hasClaimInWatchLater: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.WATCH_LATER_ID, permanentUri)(state),
|
||||
channelIsMuted: makeSelectChannelIsMuted(props.uri)(state),
|
||||
channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state),
|
||||
claimInCollection: makeSelectCollectionForIdHasClaimUrl(props.collectionId, permanentUri)(state),
|
||||
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||
hasExperimentalUi: makeSelectUserPropForProp(USER.EXPERIMENTAL_UI)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, {
|
||||
doToggleMuteChannel,
|
||||
doCommentModBlock,
|
||||
doCommentModUnBlock,
|
||||
doCollectionEdit,
|
||||
doOpenModal,
|
||||
doToast,
|
||||
})(ClaimPreview);
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
import { URL, SHARE_DOMAIN_URL } from 'config';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import { generateShareUrl } from 'util/url';
|
||||
import { useHistory } from 'react-router';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
const SHARE_DOMAIN = SHARE_DOMAIN_URL || URL;
|
||||
|
||||
|
@ -23,6 +25,15 @@ type Props = {
|
|||
doCommentModUnBlock: (string) => void,
|
||||
channelIsMine: boolean,
|
||||
isRepost: boolean,
|
||||
doCollectionEdit: (string, any) => void,
|
||||
hasClaimInWatchLater: boolean,
|
||||
doOpenModal: (string, {}) => void,
|
||||
claimInCollection: boolean,
|
||||
collectionName?: string,
|
||||
collectionId: string,
|
||||
isMyCollection: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
hasExperimentalUi: boolean,
|
||||
};
|
||||
|
||||
function ClaimMenuList(props: Props) {
|
||||
|
@ -36,21 +47,36 @@ function ClaimMenuList(props: Props) {
|
|||
channelIsBlocked,
|
||||
doCommentModBlock,
|
||||
doCommentModUnBlock,
|
||||
doCollectionEdit,
|
||||
hasClaimInWatchLater,
|
||||
doOpenModal,
|
||||
collectionId,
|
||||
collectionName,
|
||||
isMyCollection,
|
||||
doToast,
|
||||
hasExperimentalUi,
|
||||
} = props;
|
||||
|
||||
const { push } = useHistory();
|
||||
|
||||
const channelUri =
|
||||
claim &&
|
||||
(claim.value_type === 'channel'
|
||||
? claim.permanent_url
|
||||
: claim.signing_channel && claim.signing_channel.permanent_url);
|
||||
|
||||
const shareUrl: string = generateShareUrl(SHARE_DOMAIN, uri);
|
||||
|
||||
if (!channelUri || !claim) {
|
||||
if (!claim) {
|
||||
return null;
|
||||
}
|
||||
const channelUri = claim
|
||||
? claim.value_type === 'channel'
|
||||
? claim.permanent_url
|
||||
: (claim.signing_channel && claim.signing_channel.permanent_url) || ''
|
||||
: '';
|
||||
|
||||
const shareUrl: string = generateShareUrl(SHARE_DOMAIN, uri);
|
||||
const isCollectionClaim = claim && claim.value_type === 'collection';
|
||||
// $FlowFixMe
|
||||
const isPlayable =
|
||||
claim &&
|
||||
!claim.repost_url &&
|
||||
// $FlowFixMe
|
||||
claim.value.stream_type &&
|
||||
// $FlowFixMe
|
||||
(claim.value.stream_type === 'audio' || claim.value.stream_type === 'video');
|
||||
|
||||
function handleToggleMute() {
|
||||
doToggleMuteChannel(channelUri);
|
||||
|
@ -84,7 +110,69 @@ function ClaimMenuList(props: Props) {
|
|||
<Icon size={20} icon={ICONS.MORE_VERTICAL} />
|
||||
</MenuButton>
|
||||
<MenuList className="menu__list">
|
||||
{!claimIsMine && (
|
||||
{hasExperimentalUi && (
|
||||
<>
|
||||
{/* WATCH LATER */}
|
||||
{isPlayable && !collectionId && (
|
||||
<>
|
||||
<MenuItem
|
||||
className="comment__menu-option"
|
||||
onSelect={() => {
|
||||
doToast({
|
||||
message: __('Item %action% Watch Later', {
|
||||
action: hasClaimInWatchLater ? __('removed from') : __('added to'),
|
||||
}),
|
||||
});
|
||||
doCollectionEdit(COLLECTIONS_CONSTS.WATCH_LATER_ID, {
|
||||
claims: [claim],
|
||||
remove: hasClaimInWatchLater,
|
||||
type: 'playlist',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={hasClaimInWatchLater ? ICONS.DELETE : ICONS.TIME} />
|
||||
{hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{/* COLLECTION OPERATIONS */}
|
||||
{collectionId && collectionName && isCollectionClaim && (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.VIEW} />
|
||||
{__('View List')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="comment__menu-option"
|
||||
onSelect={() => doOpenModal(MODALS.COLLECTION_DELETE, { collectionId })}
|
||||
>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.DELETE} />
|
||||
{__('Delete List')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */}
|
||||
{isPlayable && (
|
||||
<MenuItem
|
||||
className="comment__menu-option"
|
||||
onSelect={() => doOpenModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })}
|
||||
>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.STACK} />
|
||||
{__('Add to Lists')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<hr className="menu__separator" />
|
||||
{channelUri && !claimIsMine && !isMyCollection && (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={handleToggleBlock}>
|
||||
<div className="menu__link">
|
||||
|
@ -111,7 +199,7 @@ function ClaimMenuList(props: Props) {
|
|||
</div>
|
||||
</MenuItem>
|
||||
|
||||
{!claimIsMine && (
|
||||
{!claimIsMine && !isMyCollection && (
|
||||
<MenuItem className="comment__menu-option" onSelect={handleReportContent}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.REPORT} />
|
||||
|
|
|
@ -11,6 +11,10 @@ import {
|
|||
makeSelectClaimWasPurchased,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectClaimIsStreamPlaceholder,
|
||||
makeSelectCollectionIsMine,
|
||||
doCollectionEdit,
|
||||
makeSelectUrlsForCollectionId,
|
||||
makeSelectIndexForUrlInCollection,
|
||||
} from 'lbry-redux';
|
||||
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||
|
@ -40,11 +44,15 @@ const select = (state, props) => ({
|
|||
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
|
||||
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||
collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state),
|
||||
collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||
getFile: (uri) => dispatch(doFileGet(uri, false)),
|
||||
editCollection: (id, params) => dispatch(doCollectionEdit(id, params)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ClaimPreview);
|
||||
|
|
|
@ -3,12 +3,12 @@ import type { Node } from 'react';
|
|||
import React, { useEffect, forwardRef } from 'react';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import classnames from 'classnames';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { isEmpty } from 'util/object';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import FileProperties from 'component/fileProperties';
|
||||
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
||||
import ClaimTags from 'component/claimTags';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
|
@ -25,10 +25,12 @@ import ClaimPreviewLoading from './claim-preview-loading';
|
|||
import ClaimPreviewHidden from './claim-preview-no-mature';
|
||||
import ClaimPreviewNoContent from './claim-preview-no-content';
|
||||
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: ?Claim,
|
||||
claim: ?Claim, // maybe?
|
||||
obscureNsfw: boolean,
|
||||
showUserBlocked: boolean,
|
||||
claimIsMine: boolean,
|
||||
|
@ -71,6 +73,11 @@ type Props = {
|
|||
hideMenu?: boolean,
|
||||
isLivestream?: boolean,
|
||||
live?: boolean,
|
||||
collectionId?: string,
|
||||
editCollection: (string, CollectionEditParams) => void,
|
||||
isCollectionMine: boolean,
|
||||
collectionUris: Array<Collection>,
|
||||
collectionIndex?: number,
|
||||
};
|
||||
|
||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||
|
@ -120,15 +127,22 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
renderActions,
|
||||
hideMenu = false,
|
||||
// repostUrl,
|
||||
isLivestream,
|
||||
isLivestream, // need both? CHECK
|
||||
live,
|
||||
collectionId,
|
||||
collectionIndex,
|
||||
editCollection,
|
||||
isCollectionMine,
|
||||
collectionUris,
|
||||
} = props;
|
||||
const WrapperElement = wrapperElement || 'li';
|
||||
const shouldFetch =
|
||||
claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending);
|
||||
const abandoned = !isResolvingUri && !claim;
|
||||
const shouldHideActions = hideActions || type === 'small' || type === 'tooltip';
|
||||
const isMyCollection = collectionId && (isCollectionMine || collectionId.includes('-'));
|
||||
const shouldHideActions = hideActions || isMyCollection || type === 'small' || type === 'tooltip';
|
||||
const canonicalUrl = claim && claim.canonical_url;
|
||||
const lastCollectionIndex = collectionUris ? collectionUris.length - 1 : 0;
|
||||
let isValid = false;
|
||||
if (uri) {
|
||||
try {
|
||||
|
@ -138,12 +152,15 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
isValid = false;
|
||||
}
|
||||
}
|
||||
const isRepost = claim && claim.repost_url;
|
||||
|
||||
const contentUri = claim && isRepost ? claim.canonical_url || claim.permanent_url : uri;
|
||||
const isChannelUri = isValid ? parseURI(contentUri).isChannel : false;
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
const isChannelUri = isValid ? parseURI(uri).isChannel : false;
|
||||
const signingChannel = claim && claim.signing_channel;
|
||||
const navigateUrl = formatLbryUrlForWeb((claim && claim.canonical_url) || uri || '/');
|
||||
let navigateUrl = formatLbryUrlForWeb((claim && claim.canonical_url) || uri || '/');
|
||||
if (collectionId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
navigateUrl = navigateUrl + `?` + collectionParams.toString();
|
||||
}
|
||||
const navLinkProps = {
|
||||
to: navigateUrl,
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
|
@ -188,7 +205,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
|
||||
// Weird placement warning
|
||||
// Make sure this happens after we figure out if this claim needs to be hidden
|
||||
const thumbnailUrl = useGetThumbnail(contentUri, claim, streamingUrl, getFile, shouldHide);
|
||||
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, shouldHide);
|
||||
|
||||
function handleOnClick(e) {
|
||||
if (onClick) {
|
||||
|
@ -232,10 +249,10 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
return empty || <ClaimPreviewNoContent isChannel={isChannelUri} type={type} />;
|
||||
}
|
||||
|
||||
if (!shouldFetch && showUnresolvedClaim && !isResolvingUri && claim === null) {
|
||||
return <AbandonedChannelPreview uri={contentUri} type />;
|
||||
if (!shouldFetch && showUnresolvedClaim && !isResolvingUri && isChannelUri && claim === null) {
|
||||
return <AbandonedChannelPreview uri={uri} type />;
|
||||
}
|
||||
if (placeholder === 'publish' && !claim && contentUri.startsWith('lbry://@')) {
|
||||
if (placeholder === 'publish' && !claim && uri.startsWith('lbry://@')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -271,8 +288,8 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
})}
|
||||
>
|
||||
{isChannelUri && claim ? (
|
||||
<UriIndicator uri={contentUri} link>
|
||||
<ChannelThumbnail uri={contentUri} />
|
||||
<UriIndicator uri={uri} link>
|
||||
<ChannelThumbnail uri={uri} />
|
||||
</UriIndicator>
|
||||
) : (
|
||||
<>
|
||||
|
@ -280,15 +297,15 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
<NavLink {...navLinkProps}>
|
||||
<FileThumbnail thumbnail={thumbnailUrl}>
|
||||
{/* @if TARGET='app' */}
|
||||
{claim && (
|
||||
{claim && !isCollection && (
|
||||
<div className="claim-preview__hover-actions">
|
||||
<FileDownloadLink uri={canonicalUrl} hideOpenButton hideDownloadStatus />
|
||||
</div>
|
||||
)}
|
||||
{/* @endif */}
|
||||
{!isRepost && !isChannelUri && (
|
||||
{!isLivestream && (
|
||||
<div className="claim-preview__file-property-overlay">
|
||||
<FileProperties uri={contentUri} small properties={liveProperty} />
|
||||
<PreviewOverlayProperties uri={uri} small={type === 'small'} properties={liveProperty} />
|
||||
</div>
|
||||
)}
|
||||
</FileThumbnail>
|
||||
|
@ -303,7 +320,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
<div className="claim-preview-metadata">
|
||||
<div className="claim-preview-info">
|
||||
{pending ? (
|
||||
<ClaimPreviewTitle uri={contentUri} />
|
||||
<ClaimPreviewTitle uri={uri} />
|
||||
) : (
|
||||
<NavLink {...navLinkProps}>
|
||||
<ClaimPreviewTitle uri={uri} />
|
||||
|
@ -318,6 +335,58 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
{!pending && (
|
||||
<>
|
||||
{renderActions && claim && renderActions(claim)}
|
||||
{Boolean(isMyCollection && collectionId) && (
|
||||
<>
|
||||
<div className="collection-preview__edit-buttons">
|
||||
<div className="collection-preview__edit-group">
|
||||
<Button
|
||||
button="alt"
|
||||
className={'button-collection-order'}
|
||||
disabled={collectionIndex === 0}
|
||||
icon={ICONS.UP}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (editCollection) {
|
||||
// $FlowFixMe
|
||||
editCollection(collectionId, {
|
||||
order: { from: collectionIndex, to: Number(collectionIndex || 0) + 1 },
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
button="alt"
|
||||
className={'button-collection-order'}
|
||||
icon={ICONS.DOWN}
|
||||
disabled={collectionIndex === lastCollectionIndex}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (editCollection) {
|
||||
// $FlowFixMe
|
||||
editCollection(collectionId, {
|
||||
order: { from: collectionIndex, to: Number(collectionIndex + 1) },
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="collection-preview__edit-group">
|
||||
<Button
|
||||
button="alt"
|
||||
icon={ICONS.DELETE}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// $FlowFixMe
|
||||
if (editCollection) editCollection(collectionId, { claims: [claim], remove: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{shouldHideActions || renderActions ? null : actions !== undefined ? (
|
||||
actions
|
||||
) : (
|
||||
|
@ -329,9 +398,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
)}
|
||||
|
||||
{isChannelUri && !channelIsBlocked && !claimIsMine && (
|
||||
<SubscribeButton
|
||||
uri={contentUri.startsWith('lbry://') ? contentUri : `lbry://${contentUri}`}
|
||||
/>
|
||||
<SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />
|
||||
)}
|
||||
|
||||
{includeSupportAction && <ClaimSupportButton uri={uri} />}
|
||||
|
@ -354,7 +421,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!hideMenu && <ClaimMenuList uri={uri} />}
|
||||
{!hideMenu && <ClaimMenuList uri={uri} collectionId={collectionId} />}
|
||||
</>
|
||||
</WrapperElement>
|
||||
);
|
||||
|
|
|
@ -10,11 +10,12 @@ import ChannelThumbnail from 'component/channelThumbnail';
|
|||
import SubscribeButton from 'component/subscribeButton';
|
||||
import useGetThumbnail from 'effects/use-get-thumbnail';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import FileProperties from 'component/fileProperties';
|
||||
import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
||||
import FileDownloadLink from 'component/fileDownloadLink';
|
||||
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
||||
import ClaimMenuList from 'component/claimMenuList';
|
||||
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -43,6 +44,7 @@ type Props = {
|
|||
properties?: (Claim) => void,
|
||||
live?: boolean,
|
||||
channelIsMine?: boolean,
|
||||
collectionId?: string,
|
||||
};
|
||||
|
||||
function ClaimPreviewTile(props: Props) {
|
||||
|
@ -66,13 +68,22 @@ function ClaimPreviewTile(props: Props) {
|
|||
properties,
|
||||
live,
|
||||
channelIsMine,
|
||||
collectionId,
|
||||
} = props;
|
||||
const isRepost = claim && claim.repost_channel_url;
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
const isStream = claim && claim.value_type === 'stream';
|
||||
const collectionClaimId = isCollection && claim && claim.claim_id;
|
||||
const shouldFetch = claim === undefined;
|
||||
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail;
|
||||
const canonicalUrl = claim && claim.canonical_url;
|
||||
const navigateUrl = formatLbryUrlForWeb(canonicalUrl || uri || '/');
|
||||
|
||||
let navigateUrl = formatLbryUrlForWeb(canonicalUrl || uri || '/');
|
||||
const listId = collectionId || collectionClaimId;
|
||||
if (listId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
|
||||
navigateUrl = navigateUrl + `?` + collectionParams.toString();
|
||||
}
|
||||
const navLinkProps = {
|
||||
to: navigateUrl,
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
|
@ -178,12 +189,22 @@ function ClaimPreviewTile(props: Props) {
|
|||
{!isChannel && (
|
||||
<React.Fragment>
|
||||
{/* @if TARGET='app' */}
|
||||
<div className="claim-preview__hover-actions">
|
||||
<FileDownloadLink uri={canonicalUrl} hideOpenButton />
|
||||
</div>
|
||||
{isStream && (
|
||||
<div className="claim-preview__hover-actions">
|
||||
<FileDownloadLink uri={canonicalUrl} hideOpenButton />
|
||||
</div>
|
||||
)}
|
||||
{/* @endif */}
|
||||
|
||||
<div className="claim-preview__file-property-overlay">
|
||||
<FileProperties uri={uri} small properties={liveProperty || properties} />
|
||||
<PreviewOverlayProperties uri={uri} properties={liveProperty || properties} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{isCollection && (
|
||||
<React.Fragment>
|
||||
<div className="claim-preview__collection-wrapper">
|
||||
<CollectionPreviewOverlay collectionId={listId} uri={uri} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
@ -197,7 +218,8 @@ function ClaimPreviewTile(props: Props) {
|
|||
<UriIndicator uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
<ClaimMenuList uri={uri} channelIsMine={channelIsMine} isRepost={isRepost} />
|
||||
{/* CHECK CLAIM MENU LIST PARAMS (IS REPOST?) */}
|
||||
<ClaimMenuList uri={uri} collectionId={listId} channelIsMine={channelIsMine} isRepost={isRepost} />
|
||||
</h2>
|
||||
</NavLink>
|
||||
<div>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import FileProperties from './view';
|
||||
import ClaimProperties from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
downloaded: makeSelectFilePartlyDownloaded(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(FileProperties);
|
||||
export default connect(select, null)(ClaimProperties);
|
38
ui/component/claimProperties/view.jsx
Normal file
38
ui/component/claimProperties/view.jsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'component/common/icon';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import ClaimType from 'component/claimType';
|
||||
import * as COL from 'constants/collections';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
isSubscribed: boolean,
|
||||
small: boolean,
|
||||
claim: Claim | CollectionClaim,
|
||||
iconOnly: boolean,
|
||||
};
|
||||
export default function ClaimProperties(props: Props) {
|
||||
const { uri, isSubscribed, small = false, claim, iconOnly } = props;
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
const size = small ? COL.ICON_SIZE : undefined;
|
||||
// $FlowFixMe
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('claim-preview__overlay-properties', { 'claim-preview__overlay-properties--small': small })}
|
||||
>
|
||||
{
|
||||
<>
|
||||
<ClaimType uri={uri} small />
|
||||
{/* // $FlowFixMe */}
|
||||
{isCollection && claim && claim.value.claims && !iconOnly && <div>{claim.value.claims.length}</div>}
|
||||
{isSubscribed && <Icon size={size} tooltip icon={ICONS.SUBSCRIBE} />}
|
||||
<FilePrice hideFree uri={uri} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -52,7 +52,7 @@ export default function ClaimTags(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classnames('file-properties--small', { 'file-properties--large': type === 'large' })}>
|
||||
<div className={classnames('claim__tags', { 'claim__tags--large': type === 'large' })}>
|
||||
{tagsToDisplay.map((tag) => (
|
||||
<Tag key={tag} title={tag} name={tag} />
|
||||
))}
|
||||
|
|
10
ui/component/claimType/index.js
Normal file
10
ui/component/claimType/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, makeSelectClaimIsStreamPlaceholder } from 'lbry-redux';
|
||||
import FileType from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select)(FileType);
|
29
ui/component/claimType/view.jsx
Normal file
29
ui/component/claimType/view.jsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as COL from 'constants/collections';
|
||||
|
||||
type Props = {
|
||||
claim: Claim,
|
||||
small: boolean,
|
||||
};
|
||||
|
||||
function ClaimType(props: Props) {
|
||||
const { claim, small } = props;
|
||||
const { value_type: claimType } = claim || {};
|
||||
|
||||
const size = small ? COL.ICON_SIZE : undefined;
|
||||
|
||||
if (claimType === 'collection') {
|
||||
return <Icon size={size} icon={ICONS.STACK} />;
|
||||
} else if (claimType === 'channel') {
|
||||
return <Icon size={size} icon={ICONS.CHANNEL} />;
|
||||
} else if (claimType === 'repost') {
|
||||
return <Icon size={size} icon={ICONS.REPOST} />;
|
||||
}
|
||||
|
||||
return <Icon icon={ICONS.DOWNLOADABLE} />;
|
||||
}
|
||||
|
||||
export default ClaimType;
|
30
ui/component/collectionActions/index.js
Normal file
30
ui/component/collectionActions/index.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimForUri,
|
||||
selectMyChannelClaims,
|
||||
makeSelectClaimIsPending,
|
||||
makeSelectCollectionIsMine,
|
||||
makeSelectEditedCollectionForId,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import CollectionActions from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(props.collectionId)(state)),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CollectionActions);
|
130
ui/component/collectionActions/view.jsx
Normal file
130
ui/component/collectionActions/view.jsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
// @flow
|
||||
import * as PAGES from 'constants/pages';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import ClaimSupportButton from 'component/claimSupportButton';
|
||||
import FileReactions from 'component/fileReactions';
|
||||
import { useHistory } from 'react-router';
|
||||
import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view';
|
||||
import classnames from 'classnames';
|
||||
import { ENABLE_FILE_REACTIONS } from 'config';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: StreamClaim,
|
||||
openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
doToast: ({ message: string }) => void,
|
||||
claimIsPending: boolean,
|
||||
isMyCollection: boolean,
|
||||
collectionId: string,
|
||||
showInfo: boolean,
|
||||
setShowInfo: (boolean) => void,
|
||||
collectionHasEdits: boolean,
|
||||
};
|
||||
|
||||
function CollectionActions(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
openModal,
|
||||
claim,
|
||||
claimIsPending,
|
||||
isMyCollection,
|
||||
collectionId,
|
||||
showInfo,
|
||||
setShowInfo,
|
||||
collectionHasEdits,
|
||||
} = props;
|
||||
const { push } = useHistory();
|
||||
const isMobile = useIsMobile();
|
||||
const claimId = claim && claim.claim_id;
|
||||
const webShareable = true; // collections have cost?
|
||||
const lhsSection = (
|
||||
<>
|
||||
{ENABLE_FILE_REACTIONS && uri && <FileReactions uri={uri} />}
|
||||
{uri && <ClaimSupportButton uri={uri} fileAction />}
|
||||
{/* TODO Add ClaimRepostButton component */}
|
||||
{uri && (
|
||||
<Button
|
||||
className="button--file-action"
|
||||
icon={ICONS.SHARE}
|
||||
label={__('Share')}
|
||||
title={__('Share')}
|
||||
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const rhsSection = (
|
||||
<>
|
||||
{isMyCollection && (
|
||||
<Button
|
||||
title={uri ? __('Update') : __('Publish')}
|
||||
label={uri ? __('Update') : __('Publish')}
|
||||
className={classnames('button--file-action')}
|
||||
onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)}
|
||||
icon={ICONS.PUBLISH}
|
||||
iconColor={collectionHasEdits && 'red'}
|
||||
iconSize={18}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
)}
|
||||
{isMyCollection && (
|
||||
<Button
|
||||
className={classnames('button--file-action')}
|
||||
title={__('Delete List')}
|
||||
onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })}
|
||||
icon={ICONS.DELETE}
|
||||
iconSize={18}
|
||||
description={__('Delete List')}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
)}
|
||||
{!isMyCollection && (
|
||||
<Button
|
||||
title={__('Report content')}
|
||||
className="button--file-action"
|
||||
icon={ICONS.REPORT}
|
||||
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const infoButton = (
|
||||
<Button
|
||||
title={__('Info')}
|
||||
className={classnames('button-toggle', {
|
||||
'button-toggle--active': showInfo,
|
||||
})}
|
||||
icon={ICONS.MORE}
|
||||
onClick={() => setShowInfo(!showInfo)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="media__actions">
|
||||
{lhsSection}
|
||||
{rhsSection}
|
||||
{uri && <span>{infoButton}</span>}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="media__subtitle--between">
|
||||
<div className="section__actions">
|
||||
{lhsSection}
|
||||
{rhsSection}
|
||||
</div>
|
||||
{uri && <>{infoButton}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectionActions;
|
24
ui/component/collectionContentSidebar/index.js
Normal file
24
ui/component/collectionContentSidebar/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import CollectionContent from './view';
|
||||
import {
|
||||
makeSelectUrlsForCollectionId,
|
||||
makeSelectNameForCollectionId,
|
||||
makeSelectCollectionForId,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectClaimIsMine,
|
||||
} from 'lbry-redux';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForClaimId(props.id)(state);
|
||||
const url = claim && claim.permanent_url;
|
||||
|
||||
return {
|
||||
collection: makeSelectCollectionForId(props.id)(state),
|
||||
collectionUrls: makeSelectUrlsForCollectionId(props.id)(state),
|
||||
collectionName: makeSelectNameForCollectionId(props.id)(state),
|
||||
claim,
|
||||
isMine: makeSelectClaimIsMine(url)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select)(CollectionContent);
|
42
ui/component/collectionContentSidebar/view.jsx
Normal file
42
ui/component/collectionContentSidebar/view.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import ClaimList from 'component/claimList';
|
||||
import Card from 'component/common/card';
|
||||
import Button from 'component/button';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
||||
type Props = {
|
||||
collectionUrls: Array<Claim>,
|
||||
collectionName: string,
|
||||
collection: any,
|
||||
createUnpublishedCollection: (string, Array<any>, ?string) => void,
|
||||
id: string,
|
||||
claim: Claim,
|
||||
isMine: boolean,
|
||||
};
|
||||
|
||||
export default function CollectionContent(props: Props) {
|
||||
const { collectionUrls, collectionName, id } = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
isBodyList
|
||||
className="file-page__recommended"
|
||||
title={
|
||||
<span>
|
||||
<Icon icon={ICONS.STACK} className="icon--margin-right" />
|
||||
{collectionName}
|
||||
</span>
|
||||
}
|
||||
titleActions={
|
||||
<>
|
||||
{/* TODO: BUTTON TO SAVE COLLECTION - Probably save/copy modal */}
|
||||
<Button label={'View List'} button="link" navigate={`/$/${PAGES.LIST}/${id}`} />
|
||||
</>
|
||||
}
|
||||
body={<ClaimList isCardBody type="small" uris={collectionUrls} collectionId={id} empty={__('List is Empty')} />}
|
||||
/>
|
||||
);
|
||||
}
|
53
ui/component/collectionEdit/index.js
Normal file
53
ui/component/collectionEdit/index.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectTitleForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectMetadataItemForUri,
|
||||
doCollectionPublish,
|
||||
doCollectionPublishUpdate,
|
||||
makeSelectAmountForUri,
|
||||
makeSelectClaimForUri,
|
||||
selectUpdateCollectionError,
|
||||
selectUpdatingCollection,
|
||||
selectCreateCollectionError,
|
||||
selectBalance,
|
||||
doClearCollectionErrors,
|
||||
selectCreatingCollection,
|
||||
makeSelectCollectionForId,
|
||||
makeSelectUrlsForCollectionId,
|
||||
makeSelectClaimIdsForCollectionId,
|
||||
} from 'lbry-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
|
||||
import CollectionPage from './view';
|
||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
thumbnailUrl: makeSelectThumbnailForUri(props.uri)(state),
|
||||
description: makeSelectMetadataItemForUri(props.uri, 'description')(state),
|
||||
tags: makeSelectMetadataItemForUri(props.uri, 'tags')(state),
|
||||
locations: makeSelectMetadataItemForUri(props.uri, 'locations')(state),
|
||||
languages: makeSelectMetadataItemForUri(props.uri, 'languages')(state),
|
||||
amount: makeSelectAmountForUri(props.uri)(state),
|
||||
updateError: selectUpdateCollectionError(state),
|
||||
updatingCollection: selectUpdatingCollection(state),
|
||||
createError: selectCreateCollectionError(state),
|
||||
creatingCollection: selectCreatingCollection(state),
|
||||
balance: selectBalance(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
incognito: selectIncognito(state),
|
||||
collection: makeSelectCollectionForId(props.collectionId)(state),
|
||||
collectionUrls: makeSelectUrlsForCollectionId(props.collectionId)(state),
|
||||
collectionClaimIds: makeSelectClaimIdsForCollectionId(props.collectionId)(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
publishCollectionUpdate: (params) => dispatch(doCollectionPublishUpdate(params)),
|
||||
publishCollection: (params, collectionId) => dispatch(doCollectionPublish(params, collectionId)),
|
||||
clearCollectionErrors: () => dispatch(doClearCollectionErrors()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CollectionPage);
|
441
ui/component/collectionEdit/view.jsx
Normal file
441
ui/component/collectionEdit/view.jsx
Normal file
|
@ -0,0 +1,441 @@
|
|||
// @flow
|
||||
import { DOMAIN } from 'config';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Button from 'component/button';
|
||||
import TagsSearch from 'component/tagsSearch';
|
||||
import ErrorText from 'component/common/error-text';
|
||||
import ClaimAbandonButton from 'component/claimAbandonButton';
|
||||
import ChannelSelector from 'component/channelSelector';
|
||||
import ClaimList from 'component/claimList';
|
||||
import Card from 'component/common/card';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import ThumbnailPicker from 'component/thumbnailPicker';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isNameValid, regexInvalidURI } from 'lbry-redux';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { handleBidChange } from 'util/publish';
|
||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import analytics from 'analytics';
|
||||
const LANG_NONE = 'none';
|
||||
const MAX_TAG_SELECT = 5;
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: CollectionClaim,
|
||||
balance: number,
|
||||
disabled: boolean,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
incognito: boolean,
|
||||
// params
|
||||
title: string,
|
||||
amount: number,
|
||||
thumbnailUrl: string,
|
||||
description: string,
|
||||
tags: Array<string>,
|
||||
locations: Array<string>,
|
||||
languages: Array<string>,
|
||||
|
||||
collectionId: string,
|
||||
collection: Collection,
|
||||
collectionClaimIds: Array<string>,
|
||||
collectionUrls: Array<string>,
|
||||
publishCollectionUpdate: (CollectionUpdateParams) => Promise<any>,
|
||||
updatingCollection: boolean,
|
||||
updateError: string,
|
||||
publishCollection: (CollectionPublishParams, string) => Promise<any>,
|
||||
createError: string,
|
||||
creatingCollection: boolean,
|
||||
clearCollectionErrors: () => void,
|
||||
onDone: (string) => void,
|
||||
openModal: (
|
||||
id: string,
|
||||
{ onUpdate: (string) => void, assetName: string, helpText: string, currentValue: string, title: string }
|
||||
) => void,
|
||||
};
|
||||
|
||||
function CollectionForm(props: Props) {
|
||||
const {
|
||||
uri, // collection uri
|
||||
claim,
|
||||
balance,
|
||||
// publish params
|
||||
amount,
|
||||
title,
|
||||
description,
|
||||
thumbnailUrl,
|
||||
tags,
|
||||
locations,
|
||||
languages = [],
|
||||
// rest
|
||||
onDone,
|
||||
publishCollectionUpdate,
|
||||
updateError,
|
||||
updatingCollection,
|
||||
publishCollection,
|
||||
creatingCollection,
|
||||
createError,
|
||||
disabled,
|
||||
activeChannelClaim,
|
||||
incognito,
|
||||
collectionId,
|
||||
collection,
|
||||
collectionUrls,
|
||||
collectionClaimIds,
|
||||
} = props;
|
||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
let prefix = IS_WEB ? `${DOMAIN}/` : 'lbry://';
|
||||
if (activeChannelName && !incognito) {
|
||||
prefix += `${activeChannelName}/`;
|
||||
}
|
||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
const collectionName = (claim && claim.name) || (collection && collection.name);
|
||||
|
||||
const [nameError, setNameError] = React.useState(undefined);
|
||||
const [bidError, setBidError] = React.useState('');
|
||||
const [params, setParams]: [any, (any) => void] = React.useState(getCollectionParams());
|
||||
const name = params.name;
|
||||
const isNewCollection = !uri;
|
||||
const { replace } = useHistory();
|
||||
const languageParam = params.languages;
|
||||
const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0];
|
||||
const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1];
|
||||
|
||||
const collectionClaimIdsString = JSON.stringify(collectionClaimIds);
|
||||
|
||||
function parseName(newName) {
|
||||
let INVALID_URI_CHARS = new RegExp(regexInvalidURI, 'gu');
|
||||
return newName.replace(INVALID_URI_CHARS, '-');
|
||||
}
|
||||
|
||||
function setParam(paramObj) {
|
||||
setParams({ ...params, ...paramObj });
|
||||
}
|
||||
|
||||
function getCollectionParams() {
|
||||
const collectionParams: {
|
||||
thumbnail_url?: string,
|
||||
name?: string,
|
||||
description?: string,
|
||||
title?: string,
|
||||
bid: string,
|
||||
languages?: ?Array<string>,
|
||||
locations?: ?Array<string>,
|
||||
tags?: ?Array<{ name: string }>,
|
||||
claim_id?: string,
|
||||
channel_id?: string,
|
||||
claims: ?Array<string>,
|
||||
} = {
|
||||
thumbnail_url: thumbnailUrl,
|
||||
description,
|
||||
bid: String(amount || 0.001),
|
||||
languages: languages || [],
|
||||
locations: locations || [],
|
||||
tags: tags
|
||||
? tags.map((tag) => {
|
||||
return { name: tag };
|
||||
})
|
||||
: [],
|
||||
claims: collectionClaimIds,
|
||||
};
|
||||
collectionParams['name'] = parseName(collectionName);
|
||||
|
||||
if (activeChannelId) {
|
||||
collectionParams['channel_id'] = activeChannelId;
|
||||
}
|
||||
|
||||
if (!claim) {
|
||||
collectionParams['title'] = collectionName;
|
||||
}
|
||||
|
||||
if (claim) {
|
||||
collectionParams['claim_id'] = claim.claim_id;
|
||||
collectionParams['title'] = title;
|
||||
}
|
||||
|
||||
return collectionParams;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const collectionClaimIds = JSON.parse(collectionClaimIdsString);
|
||||
setParams({ ...params, claims: collectionClaimIds });
|
||||
}, [collectionClaimIdsString, setParams]);
|
||||
|
||||
function handleLanguageChange(index, code) {
|
||||
let langs = [...languageParam];
|
||||
if (index === 0) {
|
||||
if (code === LANG_NONE) {
|
||||
// clear all
|
||||
langs = [];
|
||||
} else {
|
||||
langs[0] = code;
|
||||
}
|
||||
} else {
|
||||
if (code === LANG_NONE || code === langs[0]) {
|
||||
langs.splice(1, 1);
|
||||
} else {
|
||||
langs[index] = code;
|
||||
}
|
||||
}
|
||||
setParams({ ...params, languages: langs });
|
||||
}
|
||||
|
||||
function handleThumbnailChange(thumbnailUrl: string) {
|
||||
setParams({ ...params, thumbnail_url: thumbnailUrl });
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (uri) {
|
||||
publishCollectionUpdate(params).then((pendingClaim) => {
|
||||
if (pendingClaim) {
|
||||
const claimId = pendingClaim.claim_id;
|
||||
analytics.apiLogPublish(pendingClaim);
|
||||
onDone(claimId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
publishCollection(params, collectionId).then((pendingClaim) => {
|
||||
if (pendingClaim) {
|
||||
const claimId = pendingClaim.claim_id;
|
||||
analytics.apiLogPublish(pendingClaim);
|
||||
onDone(claimId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
let nameError;
|
||||
if (!name && name !== undefined) {
|
||||
nameError = __('A name is required for your url');
|
||||
} else if (!isNameValid(name, false)) {
|
||||
nameError = INVALID_NAME_ERROR;
|
||||
}
|
||||
|
||||
setNameError(nameError);
|
||||
}, [name]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (incognito) {
|
||||
const newParams = Object.assign({}, params);
|
||||
delete newParams.channel_id;
|
||||
setParams(newParams);
|
||||
} else if (activeChannelId) {
|
||||
setParams({ ...params, channel_id: activeChannelId });
|
||||
}
|
||||
}, [activeChannelId, incognito, setParams]);
|
||||
const itemError = !params.claims.length ? __('Cannot publish empty collection') : '';
|
||||
const submitError = nameError || bidError || itemError || updateError || createError;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classnames('main--contained', { 'card--disabled': disabled })}>
|
||||
<Tabs>
|
||||
<TabList className="tabs__list--channel-page">
|
||||
<Tab>{__('General')}</Tab>
|
||||
<Tab>{__('Items')}</Tab>
|
||||
<Tab>{__('Credits')}</Tab>
|
||||
<Tab>{__('Tags')}</Tab>
|
||||
<Tab>{__('Other')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className={'card-stack'}>
|
||||
<ChannelSelector disabled={disabled} />
|
||||
<Card
|
||||
body={
|
||||
<>
|
||||
<fieldset-group className="fieldset-group--smushed fieldset-group--disabled-prefix">
|
||||
<fieldset-section>
|
||||
<label htmlFor="channel_name">{__('Channel')}</label>
|
||||
</fieldset-section>
|
||||
</fieldset-group>
|
||||
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
|
||||
<fieldset-section>
|
||||
<label htmlFor="channel_name">{__('Name')}</label>
|
||||
<div className="form-field__prefix">{prefix}</div>
|
||||
</fieldset-section>
|
||||
|
||||
<FormField
|
||||
autoFocus={isNewCollection}
|
||||
type="text"
|
||||
name="channel_name"
|
||||
placeholder={__('MyAwesomeCollection')}
|
||||
value={params.name}
|
||||
error={nameError}
|
||||
disabled={!isNewCollection}
|
||||
onChange={(e) => setParams({ ...params, name: e.target.value || '' })}
|
||||
/>
|
||||
</fieldset-group>
|
||||
{!isNewCollection && (
|
||||
<span className="form-field__help">{__('This field cannot be changed.')}</span>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
type="text"
|
||||
name="channel_title2"
|
||||
label={__('Title')}
|
||||
placeholder={__('My Awesome Collection')}
|
||||
value={params.title}
|
||||
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
||||
/>
|
||||
<ThumbnailPicker
|
||||
inline
|
||||
thumbnailParam={params.thumbnail_url}
|
||||
updateThumbnailParam={handleThumbnailChange}
|
||||
/>
|
||||
<FormField
|
||||
type="markdown"
|
||||
name="content_description2"
|
||||
label={__('Description')}
|
||||
placeholder={__('Description of your content')}
|
||||
value={params.description}
|
||||
onChange={(text) => setParams({ ...params, description: text })}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ClaimList
|
||||
uris={collectionUrls}
|
||||
collectionId={collectionId}
|
||||
empty={__('This collection has no items.')}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Card
|
||||
body={
|
||||
<FormField
|
||||
className="form-field--price-amount"
|
||||
type="number"
|
||||
name="content_bid2"
|
||||
step="any"
|
||||
label={<LbcSymbol postfix={__('Deposit')} size={14} />}
|
||||
value={params.bid}
|
||||
error={bidError}
|
||||
min="0.0"
|
||||
disabled={false}
|
||||
onChange={(event) =>
|
||||
handleBidChange(parseFloat(event.target.value), amount, balance, setBidError, setParam)
|
||||
}
|
||||
placeholder={0.1}
|
||||
helper={__('Increasing your deposit can help your channel be discovered more easily.')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Card
|
||||
body={
|
||||
<TagsSearch
|
||||
suggestMature
|
||||
disableAutoFocus
|
||||
limitSelect={MAX_TAG_SELECT}
|
||||
tagsPassedIn={params.tags || []}
|
||||
label={__('Selected Tags')}
|
||||
onRemove={(clickedTag) => {
|
||||
const newTags = params.tags.slice().filter((tag) => tag.name !== clickedTag.name);
|
||||
setParams({ ...params, tags: newTags });
|
||||
}}
|
||||
onSelect={(newTags) => {
|
||||
newTags.forEach((newTag) => {
|
||||
if (!params.tags.map((savedTag) => savedTag.name).includes(newTag.name)) {
|
||||
setParams({ ...params, tags: [...params.tags, newTag] });
|
||||
} else {
|
||||
// If it already exists and the user types it in, remove it
|
||||
setParams({ ...params, tags: params.tags.filter((tag) => tag.name !== newTag.name) });
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Card
|
||||
body={
|
||||
<>
|
||||
<FormField
|
||||
name="language_select"
|
||||
type="select"
|
||||
label={__('Primary Language')}
|
||||
onChange={(event) => handleLanguageChange(0, event.target.value)}
|
||||
value={primaryLanguage}
|
||||
helper={__('Your main content language')}
|
||||
>
|
||||
<option key={'pri-langNone'} value={LANG_NONE}>
|
||||
{__('None selected')}
|
||||
</option>
|
||||
{Object.keys(SUPPORTED_LANGUAGES).map((language) => (
|
||||
<option key={language} value={language}>
|
||||
{SUPPORTED_LANGUAGES[language]}
|
||||
</option>
|
||||
))}
|
||||
</FormField>
|
||||
<FormField
|
||||
name="language_select2"
|
||||
type="select"
|
||||
label={__('Secondary Language')}
|
||||
onChange={(event) => handleLanguageChange(1, event.target.value)}
|
||||
value={secondaryLanguage}
|
||||
disabled={!languageParam[0]}
|
||||
helper={__('Your other content language')}
|
||||
>
|
||||
<option key={'sec-langNone'} value={LANG_NONE}>
|
||||
{__('None selected')}
|
||||
</option>
|
||||
{Object.keys(SUPPORTED_LANGUAGES)
|
||||
.filter((lang) => lang !== languageParam[0])
|
||||
.map((language) => (
|
||||
<option key={language} value={language}>
|
||||
{SUPPORTED_LANGUAGES[language]}
|
||||
</option>
|
||||
))}
|
||||
</FormField>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<Card
|
||||
className="card--after-tabs"
|
||||
actions={
|
||||
<>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
disabled={creatingCollection || updatingCollection || nameError || bidError || !params.claims.length}
|
||||
label={creatingCollection || updatingCollection ? __('Submitting') : __('Submit')}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={() => onDone(collectionId)} />
|
||||
</div>
|
||||
{submitError ? (
|
||||
<ErrorText>{submitError}</ErrorText>
|
||||
) : (
|
||||
<p className="help">
|
||||
{__('After submitting, it will take a few minutes for your changes to be live for everyone.')}
|
||||
</p>
|
||||
)}
|
||||
{!isNewCollection && (
|
||||
<div className="section__actions">
|
||||
<ClaimAbandonButton uri={uri} abandonActionCallback={() => replace(`/$/${PAGES.LIBRARY}`)} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CollectionForm;
|
16
ui/component/collectionMenuList/index.js
Normal file
16
ui/component/collectionMenuList/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doCollectionEdit, makeSelectNameForCollectionId, doCollectionDelete } from 'lbry-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import CollectionMenuList from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
return {
|
||||
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, {
|
||||
doCollectionEdit,
|
||||
doOpenModal,
|
||||
doCollectionDelete,
|
||||
})(CollectionMenuList);
|
59
ui/component/collectionMenuList/view.jsx
Normal file
59
ui/component/collectionMenuList/view.jsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
type Props = {
|
||||
inline?: boolean,
|
||||
doOpenModal: (string, {}) => void,
|
||||
collectionName?: string,
|
||||
collectionId: string,
|
||||
};
|
||||
|
||||
function ClaimMenuList(props: Props) {
|
||||
const { inline = false, collectionId, collectionName, doOpenModal } = props;
|
||||
|
||||
const { push } = useHistory();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
className={classnames('menu__button', { 'claim__menu-button': !inline, 'claim__menu-button--inline': inline })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Icon size={20} icon={ICONS.MORE_VERTICAL} />
|
||||
</MenuButton>
|
||||
<MenuList className="menu__list">
|
||||
{collectionId && collectionName && (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.VIEW} />
|
||||
{__('Edit List')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="comment__menu-option"
|
||||
onSelect={() => doOpenModal(MODALS.COLLECTION_DELETE, { collectionId })}
|
||||
>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.DELETE} />
|
||||
{__('Delete List')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaimMenuList;
|
30
ui/component/collectionPreviewOverlay/index.js
Normal file
30
ui/component/collectionPreviewOverlay/index.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectIsUriResolving,
|
||||
makeSelectClaimIdForUri,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectUrlsForCollectionId,
|
||||
makeSelectNameForCollectionId,
|
||||
makeSelectPendingCollectionForId,
|
||||
makeSelectCountForCollectionId,
|
||||
} from 'lbry-redux';
|
||||
import CollectionPreviewOverlay from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const collectionId = props.collectionId || (props.uri && makeSelectClaimIdForUri(props.uri));
|
||||
const claim = props.collectionId && makeSelectClaimForClaimId(props.collectionId)(state);
|
||||
const collectionUri = props.uri || (claim && (claim.canonical_url || claim.permanent_url)) || null;
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
uri: collectionUri,
|
||||
collectionCount: makeSelectCountForCollectionId(collectionId)(state),
|
||||
collectionName: makeSelectNameForCollectionId(collectionId)(state),
|
||||
collectionItemUrls: makeSelectUrlsForCollectionId(collectionId)(state), // ForId || ForUri
|
||||
pendingCollection: makeSelectPendingCollectionForId(collectionId)(state),
|
||||
claim,
|
||||
isResolvingUri: collectionUri && makeSelectIsUriResolving(collectionUri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select)(CollectionPreviewOverlay);
|
42
ui/component/collectionPreviewOverlay/view.jsx
Normal file
42
ui/component/collectionPreviewOverlay/view.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
collectionId: string,
|
||||
collectionName: string,
|
||||
collectionCount: number,
|
||||
editedCollection?: Collection,
|
||||
pendingCollection?: Collection,
|
||||
claim: ?Claim,
|
||||
collectionItemUrls: Array<string>,
|
||||
};
|
||||
|
||||
function CollectionPreviewTileOverlay(props: Props) {
|
||||
const { collectionItemUrls } = props;
|
||||
|
||||
if (collectionItemUrls && collectionItemUrls.length > 0) {
|
||||
return (
|
||||
<div className="collection-preview__overlay-thumbs">
|
||||
<div className="collection-preview__overlay-side" />
|
||||
<div className="collection-preview__overlay-grid">
|
||||
{collectionItemUrls &&
|
||||
collectionItemUrls.map((item, index) => {
|
||||
if (index < 2) {
|
||||
return (
|
||||
<div className="collection-preview__overlay-grid-items">
|
||||
<FileThumbnail uri={item} key={item} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default withRouter(CollectionPreviewTileOverlay);
|
19
ui/component/collectionPreviewTile/collectionCount.jsx
Normal file
19
ui/component/collectionPreviewTile/collectionCount.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Icon from 'component/common/icon';
|
||||
type Props = {
|
||||
count: number,
|
||||
};
|
||||
|
||||
export default function collectionCount(props: Props) {
|
||||
const { count = 0 } = props;
|
||||
|
||||
return (
|
||||
<div className={classnames('claim-preview__overlay-properties', 'claim-preview__overlay-properties--small')}>
|
||||
<Icon icon={ICONS.STACK} />
|
||||
<div>{count}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
14
ui/component/collectionPreviewTile/collectionPrivate.jsx
Normal file
14
ui/component/collectionPreviewTile/collectionPrivate.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Icon from 'component/common/icon';
|
||||
|
||||
export default function collectionCount() {
|
||||
return (
|
||||
<div className={classnames('claim-preview__overlay-properties', 'claim-preview__overlay-properties--small')}>
|
||||
<Icon icon={ICONS.LOCK} />
|
||||
<div>{__('Private')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
58
ui/component/collectionPreviewTile/index.js
Normal file
58
ui/component/collectionPreviewTile/index.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectIsUriResolving,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectTitleForUri,
|
||||
makeSelectChannelForClaimUri,
|
||||
makeSelectClaimIsNsfw,
|
||||
makeSelectClaimIdForUri,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectUrlsForCollectionId,
|
||||
makeSelectNameForCollectionId,
|
||||
doLocalCollectionDelete,
|
||||
doFetchItemsInCollection,
|
||||
makeSelectEditedCollectionForId,
|
||||
makeSelectPendingCollectionForId,
|
||||
makeSelectCountForCollectionId,
|
||||
makeSelectIsResolvingCollectionForId,
|
||||
} from 'lbry-redux';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import CollectionPreviewTile from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const collectionId = props.collectionId || (props.uri && makeSelectClaimIdForUri(props.uri));
|
||||
const claim = props.collectionId && makeSelectClaimForClaimId(props.collectionId)(state);
|
||||
const collectionUri = props.uri || (claim && (claim.canonical_url || claim.permanent_url)) || null;
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
uri: collectionUri,
|
||||
collectionCount: makeSelectCountForCollectionId(collectionId)(state),
|
||||
collectionName: makeSelectNameForCollectionId(collectionId)(state),
|
||||
collectionItemUrls: makeSelectUrlsForCollectionId(collectionId)(state), // ForId || ForUri
|
||||
editedCollection: makeSelectEditedCollectionForId(collectionId)(state),
|
||||
pendingCollection: makeSelectPendingCollectionForId(collectionId)(state),
|
||||
claim,
|
||||
isResolvingCollectionClaims: makeSelectIsResolvingCollectionForId(collectionId)(state),
|
||||
channelClaim: collectionUri && makeSelectChannelForClaimUri(collectionUri)(state),
|
||||
isResolvingUri: collectionUri && makeSelectIsUriResolving(collectionUri)(state),
|
||||
thumbnail: collectionUri && makeSelectThumbnailForUri(collectionUri)(state),
|
||||
title: collectionUri && makeSelectTitleForUri(collectionUri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
filteredOutpoints: selectFilteredOutpoints(state),
|
||||
blockedChannelUris: selectMutedChannels(state),
|
||||
showMature: selectShowMatureContent(state),
|
||||
isMature: makeSelectClaimIsNsfw(collectionUri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||
resolveCollectionItems: (options) => doFetchItemsInCollection(options),
|
||||
deleteCollection: (id) => dispatch(doLocalCollectionDelete(id)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CollectionPreviewTile);
|
174
ui/component/collectionPreviewTile/view.jsx
Normal file
174
ui/component/collectionPreviewTile/view.jsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { NavLink, useHistory } from 'react-router-dom';
|
||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
|
||||
import TruncatedText from 'component/common/truncated-text';
|
||||
import CollectionCount from './collectionCount';
|
||||
import CollectionPrivate from './collectionPrivate';
|
||||
import CollectionMenuList from 'component/collectionMenuList';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
collectionId: string,
|
||||
collectionName: string,
|
||||
collectionCount: number,
|
||||
editedCollection?: Collection,
|
||||
pendingCollection?: Collection,
|
||||
claim: ?Claim,
|
||||
channelClaim: ?ChannelClaim,
|
||||
collectionItemUrls: Array<string>,
|
||||
resolveUri: (string) => void,
|
||||
isResolvingUri: boolean,
|
||||
thumbnail?: string,
|
||||
title?: string,
|
||||
placeholder: boolean,
|
||||
blackListedOutpoints: Array<{
|
||||
txid: string,
|
||||
nout: number,
|
||||
}>,
|
||||
filteredOutpoints: Array<{
|
||||
txid: string,
|
||||
nout: number,
|
||||
}>,
|
||||
blockedChannelUris: Array<string>,
|
||||
isMature?: boolean,
|
||||
showMature: boolean,
|
||||
collectionId: string,
|
||||
deleteCollection: (string) => void,
|
||||
resolveCollectionItems: (any) => void,
|
||||
isResolvingCollectionClaims: boolean,
|
||||
};
|
||||
|
||||
function CollectionPreviewTile(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
collectionId,
|
||||
collectionName,
|
||||
collectionCount,
|
||||
isResolvingUri,
|
||||
isResolvingCollectionClaims,
|
||||
collectionItemUrls,
|
||||
claim,
|
||||
resolveCollectionItems,
|
||||
} = props;
|
||||
|
||||
const { push } = useHistory();
|
||||
const hasClaim = Boolean(claim);
|
||||
React.useEffect(() => {
|
||||
if (collectionId && hasClaim && resolveCollectionItems) {
|
||||
resolveCollectionItems({ collectionId, page_size: 5 });
|
||||
}
|
||||
}, [collectionId, hasClaim]);
|
||||
|
||||
// const signingChannel = claim && claim.signing_channel;
|
||||
|
||||
let navigateUrl = formatLbryUrlForWeb(collectionItemUrls[0] || '/');
|
||||
if (collectionId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
navigateUrl = navigateUrl + `?` + collectionParams.toString();
|
||||
}
|
||||
|
||||
function handleClick(e) {
|
||||
if (navigateUrl) {
|
||||
push(navigateUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const navLinkProps = {
|
||||
to: navigateUrl,
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
};
|
||||
|
||||
/* REMOVE IF WORKS
|
||||
let shouldHide = false;
|
||||
|
||||
if (isMature && !showMature) {
|
||||
// Unfortunately needed until this is resolved
|
||||
// https://github.com/lbryio/lbry-sdk/issues/2785
|
||||
shouldHide = true;
|
||||
}
|
||||
|
||||
// This will be replaced once blocking is done at the wallet server level
|
||||
if (claim && !shouldHide && blackListedOutpoints) {
|
||||
shouldHide = blackListedOutpoints.some(
|
||||
(outpoint) =>
|
||||
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
|
||||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
|
||||
);
|
||||
}
|
||||
// We're checking to see if the stream outpoint
|
||||
// or signing channel outpoint is in the filter list
|
||||
if (claim && !shouldHide && filteredOutpoints) {
|
||||
shouldHide = filteredOutpoints.some(
|
||||
(outpoint) =>
|
||||
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
|
||||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
|
||||
);
|
||||
}
|
||||
|
||||
// block stream claims
|
||||
if (claim && !shouldHide && blockedChannelUris.length && signingChannel) {
|
||||
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
||||
}
|
||||
// block channel claims if we can't control for them in claim search
|
||||
// e.g. fetchRecommendedSubscriptions
|
||||
|
||||
if (shouldHide) {
|
||||
return null;
|
||||
}
|
||||
*/
|
||||
|
||||
if (isResolvingUri || isResolvingCollectionClaims) {
|
||||
return (
|
||||
<li className={classnames('claim-preview--tile', {})}>
|
||||
<div className="placeholder media__thumb" />
|
||||
<div className="placeholder__wrapper">
|
||||
<div className="placeholder claim-tile__title" />
|
||||
<div className="placeholder claim-tile__info" />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (uri) {
|
||||
return <ClaimPreviewTile uri={uri} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<li role="link" onClick={handleClick} className={'card claim-preview--tile'}>
|
||||
<NavLink {...navLinkProps}>
|
||||
<div className={classnames('media__thumb')}>
|
||||
<React.Fragment>
|
||||
<div className="claim-preview__collection-wrapper">
|
||||
<CollectionPreviewOverlay collectionId={collectionId} />
|
||||
</div>
|
||||
<div className="claim-preview__claim-property-overlay">
|
||||
<CollectionCount count={collectionCount} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
</NavLink>
|
||||
<NavLink {...navLinkProps}>
|
||||
<h2 className="claim-tile__title">
|
||||
<TruncatedText text={collectionName} lines={1} />
|
||||
<CollectionMenuList collectionId={collectionId} />
|
||||
</h2>
|
||||
</NavLink>
|
||||
<div>
|
||||
<div className="claim-tile__info">
|
||||
<React.Fragment>
|
||||
<div className="claim-tile__about">
|
||||
<CollectionPrivate />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default CollectionPreviewTile;
|
24
ui/component/collectionSelectItem/index.js
Normal file
24
ui/component/collectionSelectItem/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doCollectionEdit,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectCollectionForId,
|
||||
makeSelectClaimIsPending,
|
||||
makeSelectCollectionForIdHasClaimUrl,
|
||||
} from 'lbry-redux';
|
||||
import CollectionSelectItem from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
return {
|
||||
collection: makeSelectCollectionForId(props.collectionId)(state),
|
||||
hasClaim: makeSelectCollectionForIdHasClaimUrl(props.collectionId, props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
collectionPending: makeSelectClaimIsPending(props.collectionId)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
editCollection: (id, params) => dispatch(doCollectionEdit(id, params)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CollectionSelectItem);
|
59
ui/component/collectionSelectItem/view.jsx
Normal file
59
ui/component/collectionSelectItem/view.jsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Icon from 'component/common/icon';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
hasClaim: boolean,
|
||||
category: string,
|
||||
edited: boolean,
|
||||
editCollection: (string, CollectionEditParams) => void,
|
||||
claim: Claim,
|
||||
collectionPending: Collection,
|
||||
};
|
||||
|
||||
function CollectionSelectItem(props: Props) {
|
||||
const { collection, hasClaim, category, editCollection, claim, collectionPending } = props;
|
||||
const { name, id } = collection;
|
||||
const handleChange = (e) => {
|
||||
editCollection(id, { claims: [claim], remove: hasClaim });
|
||||
};
|
||||
|
||||
let icon;
|
||||
switch (category) {
|
||||
case 'builtin':
|
||||
icon = id === COLLECTIONS_CONSTS.WATCH_LATER_ID ? ICONS.TIME : ICONS.STACK;
|
||||
break;
|
||||
case 'published':
|
||||
icon = ICONS.STACK;
|
||||
break;
|
||||
default:
|
||||
// 'unpublished'
|
||||
icon = ICONS.LOCK;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'collection-select__item'}>
|
||||
<FormField
|
||||
checked={hasClaim}
|
||||
disabled={collectionPending}
|
||||
icon={icon}
|
||||
type="checkbox"
|
||||
name={`select-${id}`}
|
||||
onChange={handleChange} // edit the collection
|
||||
label={
|
||||
<span>
|
||||
<Icon icon={icon} className={'icon-collection-select'} />
|
||||
{`${name}`}
|
||||
</span>
|
||||
} // the collection name
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CollectionSelectItem;
|
17
ui/component/collectionsListMine/index.js
Normal file
17
ui/component/collectionsListMine/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectBuiltinCollections,
|
||||
selectMyPublishedPlaylistCollections,
|
||||
selectMyUnpublishedCollections, // should probably distinguish types
|
||||
// selectSavedCollections,
|
||||
} from 'lbry-redux';
|
||||
import CollectionsListMine from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
builtinCollections: selectBuiltinCollections(state),
|
||||
publishedPlaylists: selectMyPublishedPlaylistCollections(state),
|
||||
unpublishedCollections: selectMyUnpublishedCollections(state),
|
||||
// savedCollections: selectSavedCollections(state),
|
||||
});
|
||||
|
||||
export default connect(select)(CollectionsListMine);
|
91
ui/component/collectionsListMine/view.jsx
Normal file
91
ui/component/collectionsListMine/view.jsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import CollectionPreviewTile from 'component/collectionPreviewTile';
|
||||
import ClaimList from 'component/claimList';
|
||||
import Button from 'component/button';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
import * as PAGES from 'constants/pages';
|
||||
|
||||
type Props = {
|
||||
builtinCollections: CollectionGroup,
|
||||
publishedCollections: CollectionGroup,
|
||||
publishedPlaylists: CollectionGroup,
|
||||
unpublishedCollections: CollectionGroup,
|
||||
// savedCollections: CollectionGroup,
|
||||
};
|
||||
|
||||
export default function CollectionsListMine(props: Props) {
|
||||
const {
|
||||
builtinCollections,
|
||||
publishedPlaylists,
|
||||
unpublishedCollections,
|
||||
// savedCollections, these are resolved on startup from sync'd claimIds or urls
|
||||
} = props;
|
||||
|
||||
const builtinCollectionsList = (Object.values(builtinCollections || {}): any);
|
||||
const unpublishedCollectionsList = (Object.keys(unpublishedCollections || {}): any);
|
||||
const publishedList = (Object.keys(publishedPlaylists || {}): any);
|
||||
const hasCollections = unpublishedCollectionsList.length || publishedList.length;
|
||||
|
||||
const watchLater = builtinCollectionsList.find((list) => list.id === COLLECTIONS_CONSTS.WATCH_LATER_ID);
|
||||
const favorites = builtinCollectionsList.find((list) => list.id === COLLECTIONS_CONSTS.FAVORITES_ID);
|
||||
const builtin = [watchLater, favorites];
|
||||
return (
|
||||
<>
|
||||
{builtin.map((list: Collection) => {
|
||||
const { items: itemUrls } = list;
|
||||
return (
|
||||
<div className="claim-grid__wrapper" key={list.name}>
|
||||
<>
|
||||
{Boolean(itemUrls && itemUrls.length) && (
|
||||
<>
|
||||
<h1 className="claim-grid__header">
|
||||
<Button
|
||||
className="claim-grid__title"
|
||||
button="link"
|
||||
navigate={`/$/${PAGES.LIST}/${list.id}`}
|
||||
label={list.name}
|
||||
/>
|
||||
</h1>
|
||||
<ClaimList
|
||||
tileLayout
|
||||
key={list.name}
|
||||
uris={itemUrls}
|
||||
collectionId={list.id}
|
||||
empty={__('Nothing in %collection_name%', { collection_name: list.name })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!(itemUrls && itemUrls.length) && (
|
||||
<h1 className="claim-grid__header claim-grid__title">
|
||||
{__('%collection_name%', { collection_name: list.name })}{' '}
|
||||
<div className="claim-grid__title--empty">(Empty)</div>
|
||||
</h1>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Boolean(hasCollections) && (
|
||||
<div className="claim-grid__wrapper">
|
||||
<h1 className="claim-grid__header">
|
||||
<span className="claim-grid__title">{__('Playlists')}</span>
|
||||
</h1>
|
||||
|
||||
<>
|
||||
<div className="claim-grid">
|
||||
{unpublishedCollectionsList &&
|
||||
unpublishedCollectionsList.length > 0 &&
|
||||
unpublishedCollectionsList.map((key) => (
|
||||
<CollectionPreviewTile tileLayout collectionId={key} key={key} />
|
||||
))}
|
||||
{publishedList &&
|
||||
publishedList.length > 0 &&
|
||||
publishedList.map((key) => <CollectionPreviewTile tileLayout collectionId={key} key={key} />)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -82,7 +82,7 @@ export default function Card(props: Props) {
|
|||
{subtitle && <div className="card__subtitle">{subtitle}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="card__title-actions-container">
|
||||
{titleActions && (
|
||||
<div
|
||||
className={classnames('card__title-actions', {
|
||||
|
|
|
@ -555,6 +555,13 @@ export const icons = {
|
|||
<circle cx="12" cy="19" r="1" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.MORE]: buildIcon(
|
||||
<g transform="rotate(90 12 12)">
|
||||
<circle cx="12" cy="5" r="1" />
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
<circle cx="12" cy="19" r="1" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.VALIDATED]: buildIcon(
|
||||
<g>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
|
@ -743,22 +750,6 @@ export const icons = {
|
|||
viewBox: '0 0 60 60',
|
||||
}
|
||||
),
|
||||
[ICONS.MORE]: buildIcon(
|
||||
<g>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0 30C0 13.4315 13.4315 0 30 0C46.5685 0 60 13.4315 60 30C60 46.5685 46.5685 60 30 60C13.4315 60 0 46.5685 0 30Z"
|
||||
fill="#eee"
|
||||
/>
|
||||
<circle cx="20" cy="30" r="2" stroke="black" fill="black" />
|
||||
<circle cx="30" cy="30" r="2" stroke="black" fill="black" />
|
||||
<circle cx="40" cy="30" r="2" stroke="black" fill="black" />
|
||||
</g>,
|
||||
{
|
||||
viewBox: '0 0 60 60',
|
||||
}
|
||||
),
|
||||
[ICONS.SHARE_LINK]: buildIcon(
|
||||
<g>
|
||||
<path
|
||||
|
@ -1786,7 +1777,8 @@ export const icons = {
|
|||
d="M-506.9,1532.1c1.8-1.8,2.9-4.3,2.9-7.1c0-2.6-1-4.9-2.5-6.7L-506.9,1532.1z"
|
||||
/>
|
||||
</g>
|
||||
<rect id="XMLID_125_" x="0" y="0" fill="none" width="36" height="36" stroke="none" /> {/* }//fill="#FFFFFF" */}
|
||||
<rect id="XMLID_125_" x="0" y="0" fill="none" width="36" height="36" stroke="none" />
|
||||
{/* }//fill="#FFFFFF" */}
|
||||
<linearGradient
|
||||
id="XMLID_421_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
|
@ -2048,7 +2040,8 @@ export const icons = {
|
|||
d="M-506.9,1532.1c1.8-1.8,2.9-4.3,2.9-7.1c0-2.6-1-4.9-2.5-6.7L-506.9,1532.1z"
|
||||
/>
|
||||
</g>
|
||||
<rect id="XMLID_125_" x="0" y="0" fill="none" width="36" height="36" stroke="none" /> {/* }//fill="#FFFFFF" */}
|
||||
<rect id="XMLID_125_" x="0" y="0" fill="none" width="36" height="36" stroke="none" />
|
||||
{/* }//fill="#FFFFFF" */}
|
||||
<linearGradient
|
||||
id="XMLID_421_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
|
@ -2259,4 +2252,44 @@ export const icons = {
|
|||
/>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.STACK]: (props: CustomProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
width={props.size || '18'}
|
||||
height={props.size || '18'}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path d="M22.91,6.953,12.7,1.672a1.543,1.543,0,0,0-1.416,0L1.076,6.953a.615.615,0,0,0,0,1.094l10.209,5.281a1.543,1.543,0,0,0,1.416,0L22.91,8.047a.616.616,0,0,0,0-1.094Z" />
|
||||
<path d="M.758,12.75l10.527,5.078a1.543,1.543,0,0,0,1.416,0L23.258,12.75" />
|
||||
<path d="M.758,17.25l10.527,5.078a1.543,1.543,0,0,0,1.416,0L23.258,17.25" />
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.TIME]: (props: CustomProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
width={props.size || '18'}
|
||||
height={props.size || '18'}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path d="M1.500 12.000 A10.500 10.500 0 1 0 22.500 12.000 A10.500 10.500 0 1 0 1.500 12.000 Z" />
|
||||
<path d="M12 12L12 8.25" />
|
||||
<path d="M12 12L16.687 16.688" />
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -71,7 +71,7 @@ class IconComponent extends React.PureComponent<Props> {
|
|||
<Icon
|
||||
title={tooltipText}
|
||||
size={size || (sectionIcon ? 20 : 16)}
|
||||
className={classnames(`icon icon--${icon}`, className)}
|
||||
className={classnames(`icon icon--${icon}`, className, { 'color-override': iconColor })}
|
||||
color={color}
|
||||
{...rest}
|
||||
/>
|
||||
|
|
|
@ -9,12 +9,14 @@ import {
|
|||
makeSelectTagInClaimOrChannelForUri,
|
||||
} from 'lbry-redux';
|
||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||
import { makeSelectUserPropForProp } from 'redux/selectors/user';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doOpenModal, doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
|
||||
import fs from 'fs';
|
||||
import FileActions from './view';
|
||||
import * as USER from 'constants/user';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
@ -26,6 +28,7 @@ const select = (state, props) => ({
|
|||
myChannels: selectMyChannelClaims(state),
|
||||
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||
reactionsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
||||
hasExperimentalUi: makeSelectUserPropForProp(USER.EXPERIMENTAL_UI)(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
|
|
|
@ -10,6 +10,7 @@ import { buildURI } from 'lbry-redux';
|
|||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import ClaimSupportButton from 'component/claimSupportButton';
|
||||
import ClaimCollectionAddButton from 'component/claimCollectionAddButton';
|
||||
import { useHistory } from 'react-router';
|
||||
import FileReactions from 'component/fileReactions';
|
||||
|
||||
|
@ -27,6 +28,7 @@ type Props = {
|
|||
clearPlayingUri: () => void,
|
||||
isLivestreamClaim: boolean,
|
||||
reactionsDisabled: boolean,
|
||||
hasExperimentalUi: boolean,
|
||||
};
|
||||
|
||||
function FileActions(props: Props) {
|
||||
|
@ -44,6 +46,7 @@ function FileActions(props: Props) {
|
|||
doToast,
|
||||
isLivestreamClaim,
|
||||
reactionsDisabled,
|
||||
hasExperimentalUi,
|
||||
} = props;
|
||||
const {
|
||||
push,
|
||||
|
@ -86,6 +89,7 @@ function FileActions(props: Props) {
|
|||
<>
|
||||
{ENABLE_FILE_REACTIONS && !reactionsDisabled && <FileReactions uri={uri} />}
|
||||
<ClaimSupportButton uri={uri} fileAction />
|
||||
{hasExperimentalUi && <ClaimCollectionAddButton uri={uri} fileAction />}
|
||||
<Button
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
|
|
|
@ -20,10 +20,11 @@ type Props = {
|
|||
pendingAmount: number,
|
||||
doOpenModal: (id: string, {}) => void,
|
||||
claimIsMine: boolean,
|
||||
expandOverride: boolean,
|
||||
};
|
||||
|
||||
function FileDescription(props: Props) {
|
||||
const { uri, claim, metadata, pendingAmount, doOpenModal, claimIsMine } = props;
|
||||
const { uri, claim, metadata, pendingAmount, doOpenModal, claimIsMine, expandOverride } = props;
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [showCreditDetails, setShowCreditDetails] = React.useState(false);
|
||||
const amount = parseFloat(claim.amount) + parseFloat(pendingAmount || claim.meta.support_amount);
|
||||
|
@ -40,9 +41,9 @@ function FileDescription(props: Props) {
|
|||
<div>
|
||||
<div
|
||||
className={classnames({
|
||||
'media__info-text--contracted': !expanded,
|
||||
'media__info-text--contracted': !expanded && !expandOverride,
|
||||
'media__info-text--expanded': expanded,
|
||||
'media__info-text--fade': !expanded,
|
||||
'media__info-text--fade': !expanded && !expandOverride,
|
||||
})}
|
||||
>
|
||||
{description && <MarkdownPreview className="markdown-preview--description" content={description} simpleLinks />}
|
||||
|
@ -51,10 +52,14 @@ function FileDescription(props: Props) {
|
|||
</div>
|
||||
|
||||
<div className="card__bottom-actions">
|
||||
{expanded ? (
|
||||
<Button button="link" label={__('Less')} onClick={() => setExpanded(!expanded)} />
|
||||
) : (
|
||||
<Button button="link" label={__('More')} onClick={() => setExpanded(!expanded)} />
|
||||
{!expandOverride && (
|
||||
<>
|
||||
{expanded ? (
|
||||
<Button button="link" label={__('Less')} onClick={() => setExpanded(!expanded)} />
|
||||
) : (
|
||||
<Button button="link" label={__('More')} onClick={() => setExpanded(!expanded)} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="section__actions--no-margin">
|
||||
|
|
|
@ -9,7 +9,7 @@ type Props = {
|
|||
claim: StreamClaim,
|
||||
fileInfo: FileListItem,
|
||||
metadata: StreamMetadata,
|
||||
openFolder: string => void,
|
||||
openFolder: (string) => void,
|
||||
contentType: string,
|
||||
user: ?any,
|
||||
};
|
||||
|
@ -58,7 +58,7 @@ class FileDetails extends PureComponent<Props> {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{languages && (
|
||||
{mediaType && (
|
||||
<div className="media__details">
|
||||
<span>{__('Media Type')}</span>
|
||||
<span>{mediaType}</span>
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'component/common/icon';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import VideoDuration from 'component/videoDuration';
|
||||
import FileType from 'component/fileType';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
downloaded: boolean,
|
||||
claimIsMine: boolean,
|
||||
isSubscribed: boolean,
|
||||
small: boolean,
|
||||
claim: Claim,
|
||||
properties?: (Claim) => ?Node,
|
||||
};
|
||||
|
||||
export default function FileProperties(props: Props) {
|
||||
const { uri, downloaded, claimIsMine, isSubscribed, small = false, properties, claim } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('file-properties', {
|
||||
'file-properties--small': small,
|
||||
})}
|
||||
>
|
||||
{typeof properties === 'function' ? (
|
||||
properties(claim)
|
||||
) : (
|
||||
<>
|
||||
<VideoDuration uri={uri} />
|
||||
<FileType uri={uri} />
|
||||
{isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
|
||||
{!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
|
||||
|
||||
<FilePrice hideFree uri={uri} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -8,9 +8,9 @@ import NudgeFloating from 'component/nudgeFloating';
|
|||
|
||||
type Props = {
|
||||
claim: StreamClaim,
|
||||
doFetchReactions: string => void,
|
||||
doReactionLike: string => void,
|
||||
doReactionDislike: string => void,
|
||||
doFetchReactions: (string) => void,
|
||||
doReactionLike: (string) => void,
|
||||
doReactionDislike: (string) => void,
|
||||
uri: string,
|
||||
likeCount: number,
|
||||
dislikeCount: number,
|
||||
|
@ -21,7 +21,7 @@ function FileReactions(props: Props) {
|
|||
const { claim, uri, doFetchReactions, doReactionLike, doReactionDislike, likeCount, dislikeCount } = props;
|
||||
const claimId = claim && claim.claim_id;
|
||||
const channel = claim && claim.signing_channel && claim.signing_channel.name;
|
||||
|
||||
const isCollection = claim && claim.value_type === 'collection'; // hack because nudge gets cut off by card on cols.
|
||||
React.useEffect(() => {
|
||||
if (claimId) {
|
||||
doFetchReactions(claimId);
|
||||
|
@ -30,7 +30,7 @@ function FileReactions(props: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
{channel && (
|
||||
{channel && !isCollection && (
|
||||
<NudgeFloating
|
||||
name="nudge:support-acknowledge"
|
||||
text={__('Let %channel% know you enjoyed this!', { channel })}
|
||||
|
|
|
@ -39,11 +39,11 @@ const select = (state, props) => ({
|
|||
authenticated: selectUserVerifiedEmail(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
play: uri => {
|
||||
const perform = (dispatch) => ({
|
||||
play: (uri) => {
|
||||
dispatch(doSetPrimaryUri(uri));
|
||||
dispatch(doSetPlayingUri({ uri }));
|
||||
dispatch(doPlayUri(uri, undefined, undefined, fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
||||
dispatch(doPlayUri(uri, undefined, undefined, (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -52,6 +52,19 @@ function FileThumbnail(props: Props) {
|
|||
|
||||
const thumnailUrl = url ? url.replace(/'/g, "\\'") : '';
|
||||
|
||||
if (!hasResolvedClaim) {
|
||||
return (
|
||||
<div
|
||||
ref={thumbnailRef}
|
||||
data-background-image={thumnailUrl}
|
||||
className={classnames('media__thumb', className, {
|
||||
'media__thumb--resolving': !hasResolvedClaim,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={thumbnailRef}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { normalizeURI } from 'lbry-redux';
|
|||
import FilePrice from 'component/filePrice';
|
||||
import ClaimInsufficientCredits from 'component/claimInsufficientCredits';
|
||||
import FileSubtitle from 'component/fileSubtitle';
|
||||
import FileAuthor from 'component/fileAuthor';
|
||||
import ClaimAuthor from 'component/claimAuthor';
|
||||
import Card from 'component/common/card';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Icon from 'component/common/icon';
|
||||
|
@ -81,7 +81,7 @@ function FileTitleSection(props: Props) {
|
|||
</div>
|
||||
) : (
|
||||
<div className="section">
|
||||
<FileAuthor uri={uri} />
|
||||
<ClaimAuthor uri={uri} />
|
||||
<FileDescription uri={uri} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -2,24 +2,27 @@
|
|||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as COL from 'constants/collections';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
mediaType: string,
|
||||
isLivestream: boolean,
|
||||
small: boolean,
|
||||
};
|
||||
|
||||
function FileType(props: Props) {
|
||||
const { mediaType, isLivestream } = props;
|
||||
const { mediaType, isLivestream, small } = props;
|
||||
const size = small ? COL.ICON_SIZE : undefined;
|
||||
|
||||
if (mediaType === 'image') {
|
||||
return <Icon icon={ICONS.IMAGE} />;
|
||||
return <Icon size={size} icon={ICONS.IMAGE} />;
|
||||
} else if (mediaType === 'audio') {
|
||||
return <Icon icon={ICONS.AUDIO} />;
|
||||
return <Icon size={size} icon={ICONS.AUDIO} />;
|
||||
} else if (mediaType === 'video' || isLivestream) {
|
||||
return <Icon icon={ICONS.VIDEO} />;
|
||||
return <Icon size={size} icon={ICONS.VIDEO} />;
|
||||
} else if (mediaType === 'text') {
|
||||
return <Icon icon={ICONS.TEXT} />;
|
||||
return <Icon size={size} icon={ICONS.TEXT} />;
|
||||
}
|
||||
|
||||
return <Icon icon={ICONS.DOWNLOADABLE} />;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { doUserInviteNew } from 'redux/actions/user';
|
|||
import { selectMyChannelClaims, selectFetchingMyChannels, doFetchChannelListMine } from 'lbry-redux';
|
||||
import InviteNew from './view';
|
||||
|
||||
const select = state => ({
|
||||
const select = (state) => ({
|
||||
errorMessage: selectUserInviteNewErrorMessage(state),
|
||||
invitesRemaining: selectUserInvitesRemaining(state),
|
||||
referralLink: selectUserInviteReferralLink(state),
|
||||
|
@ -20,8 +20,8 @@ const select = state => ({
|
|||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
inviteNew: email => dispatch(doUserInviteNew(email)),
|
||||
const perform = (dispatch) => ({
|
||||
inviteNew: (email) => dispatch(doUserInviteNew(email)),
|
||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||
});
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import * as React from 'react';
|
|||
import * as ICONS from 'constants/icons';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import { formatCredits } from 'lbry-redux';
|
||||
import FileAuthor from 'component/fileAuthor';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
import ClaimAuthor from 'component/claimAuthor';
|
||||
import FileTitle from 'component/fileTitle';
|
||||
import FileActions from 'component/fileActions';
|
||||
import FileRenderInitiator from 'component/fileRenderInitiator';
|
||||
|
@ -102,7 +102,7 @@ function PostViewer(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<FileAuthor uri={uri} />
|
||||
<ClaimAuthor uri={uri} />
|
||||
|
||||
<FileRenderInitiator uri={uri} />
|
||||
<FileRenderInline uri={uri} />
|
||||
|
|
23
ui/component/previewOverlayProperties/index.js
Normal file
23
ui/component/previewOverlayProperties/index.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectFilePartlyDownloaded,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectEditedCollectionForId,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import PreviewOverlayProperties from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||
const claimId = claim && claim.claim_id;
|
||||
return {
|
||||
claim,
|
||||
editedCollection: makeSelectEditedCollectionForId(claimId)(state),
|
||||
downloaded: makeSelectFilePartlyDownloaded(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, null)(PreviewOverlayProperties);
|
75
ui/component/previewOverlayProperties/view.jsx
Normal file
75
ui/component/previewOverlayProperties/view.jsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'component/common/icon';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import VideoDuration from 'component/videoDuration';
|
||||
import FileType from 'component/fileType';
|
||||
import ClaimType from 'component/claimType';
|
||||
import * as COL from 'constants/collections';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
downloaded: boolean,
|
||||
claimIsMine: boolean,
|
||||
isSubscribed: boolean,
|
||||
small: boolean,
|
||||
claim: Claim,
|
||||
properties?: (Claim) => ?Node,
|
||||
iconOnly: boolean,
|
||||
editedCollection: Collection,
|
||||
};
|
||||
|
||||
export default function PreviewOverlayProperties(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
downloaded,
|
||||
claimIsMine,
|
||||
isSubscribed,
|
||||
small = false,
|
||||
properties,
|
||||
claim,
|
||||
iconOnly,
|
||||
editedCollection,
|
||||
} = props;
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
// $FlowFixMe
|
||||
const claimLength = claim && claim.value.claims && claim.value.claims.length;
|
||||
|
||||
const claimCount = editedCollection ? editedCollection.items.length : claimLength;
|
||||
const isStream = claim && claim.value_type === 'stream';
|
||||
const size = small ? COL.ICON_SIZE : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('claim-preview__overlay-properties', {
|
||||
'.claim-preview__overlay-properties--small': small,
|
||||
})}
|
||||
>
|
||||
{typeof properties === 'function' ? (
|
||||
properties(claim)
|
||||
) : (
|
||||
<>
|
||||
{!isStream && <ClaimType uri={uri} small={small} />}
|
||||
{editedCollection && (
|
||||
<Icon
|
||||
customTooltipText={__('Unpublished Edits')}
|
||||
tooltip
|
||||
iconColor="red"
|
||||
size={size}
|
||||
icon={ICONS.PUBLISH}
|
||||
/>
|
||||
)}
|
||||
{isCollection && claim && !iconOnly && <div>{claimCount}</div>}
|
||||
{!iconOnly && isStream && <VideoDuration uri={uri} />}
|
||||
{isStream && <FileType uri={uri} small={small} />}
|
||||
{isSubscribed && <Icon tooltip size={size} icon={ICONS.SUBSCRIBE} />}
|
||||
{!claimIsMine && downloaded && <Icon size={size} tooltip icon={ICONS.LIBRARY} />}
|
||||
<FilePrice hideFree uri={uri} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -24,6 +24,7 @@ import RewardsPage from 'page/rewards';
|
|||
import FileListPublished from 'page/fileListPublished';
|
||||
import InvitePage from 'page/invite';
|
||||
import SearchPage from 'page/search';
|
||||
import ListsPage from 'page/lists';
|
||||
import LibraryPage from 'page/library';
|
||||
import WalletPage from 'page/wallet';
|
||||
import TagsFollowingPage from 'page/tagsFollowing';
|
||||
|
@ -55,6 +56,7 @@ import SwapPage from 'page/swap';
|
|||
import NotificationsPage from 'page/notifications';
|
||||
import SignInWalletPasswordPage from 'page/signInWalletPassword';
|
||||
import YoutubeSyncPage from 'page/youtubeSync';
|
||||
import CollectionPage from 'page/collection';
|
||||
|
||||
import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
|
||||
import { parseURI, isURIValid } from 'lbry-redux';
|
||||
|
@ -256,6 +258,7 @@ function AppRouter(props: Props) {
|
|||
<Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} />
|
||||
<Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} />
|
||||
<Route path={`/$/${PAGES.REPORT_CONTENT}`} exact component={ReportContentPage} />
|
||||
<Route {...props} path={`/$/${PAGES.LIST}/:collectionId`} component={CollectionPage} />
|
||||
|
||||
<PrivateRoute {...props} exact path={`/$/${PAGES.YOUTUBE_SYNC}`} component={YoutubeSyncPage} />
|
||||
<PrivateRoute {...props} exact path={`/$/${PAGES.TAGS_FOLLOWING}`} component={TagsFollowingPage} />
|
||||
|
@ -282,6 +285,7 @@ function AppRouter(props: Props) {
|
|||
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.LISTS}`} component={ListsPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />
|
||||
|
|
|
@ -8,6 +8,7 @@ import Button from 'component/button';
|
|||
import Card from 'component/common/card';
|
||||
import { generateThumbnailName } from 'util/generate-thumbnail-name';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const accept = '.png, .jpg, .jpeg, .gif';
|
||||
const SPEECH_READY = 'READY';
|
||||
|
@ -16,14 +17,15 @@ const SPEECH_UPLOADING = 'UPLOADING';
|
|||
type Props = {
|
||||
assetName: string,
|
||||
currentValue: ?string,
|
||||
onUpdate: string => void,
|
||||
onUpdate: (string) => void,
|
||||
recommended: string,
|
||||
title: string,
|
||||
onDone?: () => void,
|
||||
inline?: boolean,
|
||||
};
|
||||
|
||||
function SelectAsset(props: Props) {
|
||||
const { onUpdate, onDone, assetName, recommended, title } = props;
|
||||
const { onUpdate, onDone, assetName, recommended, title, inline } = props;
|
||||
const [pathSelected, setPathSelected] = React.useState('');
|
||||
const [fileSelected, setFileSelected] = React.useState<any>(null);
|
||||
const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY);
|
||||
|
@ -37,12 +39,19 @@ function SelectAsset(props: Props) {
|
|||
}
|
||||
}, [pathSelected, fileSelected]);
|
||||
|
||||
function handleToggleMode(useUrl) {
|
||||
setPathSelected('');
|
||||
setFileSelected(null);
|
||||
setUrl('');
|
||||
setUseUrl(useUrl);
|
||||
}
|
||||
|
||||
function doUploadAsset() {
|
||||
const uploadError = (error = '') => {
|
||||
setError(error);
|
||||
};
|
||||
|
||||
const onSuccess = thumbnailUrl => {
|
||||
const onSuccess = (thumbnailUrl) => {
|
||||
setUploadStatus(SPEECH_READY);
|
||||
onUpdate(thumbnailUrl);
|
||||
|
||||
|
@ -62,9 +71,9 @@ function SelectAsset(props: Props) {
|
|||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => (json.success ? onSuccess(`${json.data.serveUrl}`) : uploadError(json.message)))
|
||||
.catch(err => {
|
||||
.then((response) => response.json())
|
||||
.then((json) => (json.success ? onSuccess(`${json.data.serveUrl}`) : uploadError(json.message)))
|
||||
.catch((err) => {
|
||||
uploadError(err.message);
|
||||
setUploadStatus(SPEECH_READY);
|
||||
});
|
||||
|
@ -72,66 +81,90 @@ function SelectAsset(props: Props) {
|
|||
|
||||
// Note for translators: e.g. "Thumbnail (1:1)"
|
||||
const label = __('%image_type% %recommended_ratio%', { image_type: assetName, recommended_ratio: recommended });
|
||||
const selectFileLabel = __('Select File');
|
||||
const selectedLabel = pathSelected ? __('URL Selected') : __('File Selected');
|
||||
|
||||
let fileSelectorLabel;
|
||||
if (uploadStatus === SPEECH_UPLOADING) {
|
||||
fileSelectorLabel = __('Uploading...');
|
||||
} else {
|
||||
// Include the same label/recommendation for both 'URL' and 'UPLOAD'.
|
||||
fileSelectorLabel = __('%label% • File to upload', { label: label });
|
||||
fileSelectorLabel = __('%label% • %status%', {
|
||||
label: label,
|
||||
status: fileSelected || pathSelected ? selectedLabel : selectFileLabel,
|
||||
});
|
||||
}
|
||||
const formBody = (
|
||||
<>
|
||||
<div className={'section__header--actions'}>
|
||||
<div>
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle', {
|
||||
'button-toggle--active': useUrl, // disable on upload status
|
||||
})}
|
||||
label={__('Url')}
|
||||
onClick={() => {
|
||||
handleToggleMode(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle', {
|
||||
'button-toggle--active': !useUrl, // disable on upload status
|
||||
})}
|
||||
label={__('Upload')}
|
||||
onClick={() => {
|
||||
handleToggleMode(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset-section>
|
||||
{error && <div className="error__text">{error}</div>}
|
||||
{useUrl ? (
|
||||
<FormField
|
||||
autoFocus
|
||||
type={'text'}
|
||||
name={'thumbnail'}
|
||||
label={label}
|
||||
placeholder={`https://example.com/image.png`}
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
onUpdate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FileSelector
|
||||
autoFocus
|
||||
disabled={uploadStatus === SPEECH_UPLOADING}
|
||||
label={fileSelectorLabel}
|
||||
name="assetSelector"
|
||||
currentPath={pathSelected}
|
||||
onFileChosen={(file) => {
|
||||
if (file.name) {
|
||||
setFileSelected(file);
|
||||
// what why? why not target=WEB this?
|
||||
// file.path is undefined in web but available in electron
|
||||
setPathSelected(file.name || file.path);
|
||||
}
|
||||
}}
|
||||
accept={accept}
|
||||
/>
|
||||
)}
|
||||
</fieldset-section>
|
||||
</>
|
||||
);
|
||||
|
||||
if (inline) {
|
||||
return <fieldset-section>{formBody}</fieldset-section>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={title || __('Choose image')}
|
||||
actions={
|
||||
<Form onSubmit={onDone}>
|
||||
{error && <div className="error__text">{error}</div>}
|
||||
{useUrl ? (
|
||||
<FormField
|
||||
autoFocus
|
||||
type={'text'}
|
||||
name={'thumbnail'}
|
||||
label={label}
|
||||
placeholder={'https://example.com/image.png'}
|
||||
value={url}
|
||||
onChange={e => {
|
||||
setUrl(e.target.value);
|
||||
onUpdate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FileSelector
|
||||
autoFocus
|
||||
disabled={uploadStatus === SPEECH_UPLOADING}
|
||||
label={fileSelectorLabel}
|
||||
name="assetSelector"
|
||||
currentPath={pathSelected}
|
||||
onFileChosen={file => {
|
||||
if (file.name) {
|
||||
setFileSelected(file);
|
||||
// file.path is undefined in web but available in electron
|
||||
setPathSelected(file.name || file.path);
|
||||
}
|
||||
}}
|
||||
accept={accept}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="section__actions">
|
||||
{onDone && (
|
||||
<Button button="primary" type="submit" label={__('Done')} disabled={uploadStatus === SPEECH_UPLOADING} />
|
||||
)}
|
||||
<FormField
|
||||
name="toggle-upload"
|
||||
type="checkbox"
|
||||
label={__('Use a URL')}
|
||||
checked={useUrl}
|
||||
onChange={() => setUseUrl(!useUrl)}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
}
|
||||
title={title || __('Choose %asset%', { asset: __(`${assetName}`) })}
|
||||
actions={<Form onSubmit={onDone}>{formBody}</Form>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@ import { connect } from 'react-redux';
|
|||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { selectPurchaseUriSuccess, doClearPurchasedUriSuccess } from 'lbry-redux';
|
||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||
import { selectUserVerifiedEmail, selectUser, makeSelectUserPropForProp } from 'redux/selectors/user';
|
||||
import { selectHomepageData, selectLanguage } from 'redux/selectors/settings';
|
||||
import { doSignOut } from 'redux/actions/app';
|
||||
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
||||
import * as USER from 'constants/user';
|
||||
|
||||
import SideNavigation from './view';
|
||||
|
||||
|
@ -18,6 +19,7 @@ const select = (state) => ({
|
|||
unseenCount: selectUnseenNotificationCount(state),
|
||||
user: selectUser(state),
|
||||
homepageData: selectHomepageData(state),
|
||||
hasExperimentalUi: makeSelectUserPropForProp(USER.EXPERIMENTAL_UI)(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
|
|
|
@ -46,6 +46,7 @@ type Props = {
|
|||
doClearPurchasedUriSuccess: () => void,
|
||||
user: ?User,
|
||||
homepageData: any,
|
||||
hasExperimentalUi: boolean,
|
||||
};
|
||||
|
||||
type SideNavLink = {
|
||||
|
@ -73,6 +74,7 @@ function SideNavigation(props: Props) {
|
|||
homepageData,
|
||||
user,
|
||||
followedTags,
|
||||
hasExperimentalUi,
|
||||
} = props;
|
||||
|
||||
const { EXTRA_SIDEBAR_LINKS } = homepageData;
|
||||
|
@ -219,15 +221,29 @@ function SideNavigation(props: Props) {
|
|||
|
||||
SIDE_LINKS.push(HOME);
|
||||
SIDE_LINKS.push(RECENT_FROM_FOLLOWING);
|
||||
if (!SIMPLE_SITE && hasExperimentalUi) {
|
||||
FULL_LINKS.push({
|
||||
title: 'Lists',
|
||||
link: `/$/${PAGES.LISTS}`,
|
||||
icon: ICONS.STACK,
|
||||
hideForUnauth: true,
|
||||
});
|
||||
}
|
||||
if (!SIMPLE_SITE) {
|
||||
SIDE_LINKS.push(...FULL_LINKS);
|
||||
} else if (SIMPLE_SITE && hasExperimentalUi) {
|
||||
SIDE_LINKS.push({
|
||||
title: 'Lists',
|
||||
link: `/$/${PAGES.LISTS}`,
|
||||
icon: ICONS.STACK,
|
||||
hideForUnauth: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (EXTRA_SIDEBAR_LINKS) {
|
||||
SIDE_LINKS.push(...EXTRA_SIDEBAR_LINKS);
|
||||
}
|
||||
|
||||
if (!SIMPLE_SITE) {
|
||||
SIDE_LINKS.push(...FULL_LINKS);
|
||||
}
|
||||
|
||||
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
||||
const isPersonalized = !IS_WEB || isAuthenticated;
|
||||
const isAbsolute = isOnFilePage || isMediumScreen;
|
||||
|
|
|
@ -83,7 +83,7 @@ function SocialShare(props: Props) {
|
|||
name="share_start_at"
|
||||
value={startTime}
|
||||
disabled={!includeStartTime}
|
||||
onChange={event => setStartTime(event.target.value)}
|
||||
onChange={(event) => setStartTime(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -166,7 +166,7 @@ function SocialShare(props: Props) {
|
|||
{showClaimLinks && (
|
||||
<div className="section">
|
||||
<CopyableText label={__('LBRY URL')} copyable={`lbry://${lbryUrl}`} />
|
||||
{!isChannel && <CopyableText label={__('Download Link')} copyable={downloadUrl} />}
|
||||
{Boolean(isStream) && <CopyableText label={__('Download Link')} copyable={downloadUrl} />}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
|
13
ui/component/thumbnailPicker/index.js
Normal file
13
ui/component/thumbnailPicker/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import ThumbnailPicker from './view';
|
||||
import { makeSelectThumbnailForUri } from 'lbry-redux';
|
||||
|
||||
const select = (state, props) => ({
|
||||
thumbnailForUri: makeSelectThumbnailForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
// const perform = dispatch => ({
|
||||
// });
|
||||
|
||||
export default connect(select)(ThumbnailPicker);
|
BIN
ui/component/thumbnailPicker/thumbnail-broken.png
Normal file
BIN
ui/component/thumbnailPicker/thumbnail-broken.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
ui/component/thumbnailPicker/thumbnail-missing.png
Normal file
BIN
ui/component/thumbnailPicker/thumbnail-missing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
116
ui/component/thumbnailPicker/view.jsx
Normal file
116
ui/component/thumbnailPicker/view.jsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import Card from 'component/common/card';
|
||||
import ThumbnailMissingImage from './thumbnail-missing.png';
|
||||
import SelectAsset from 'component/selectAsset';
|
||||
|
||||
type Props = {
|
||||
// uploadThumbnailStatus: string,
|
||||
thumbnailParam: string,
|
||||
thumbnailForUri: string,
|
||||
updateThumbnailParam: (string) => void,
|
||||
inline?: boolean,
|
||||
};
|
||||
|
||||
const ThumbnailPicker = (props: Props) => {
|
||||
const { thumbnailForUri, thumbnailParam, updateThumbnailParam, inline } = props;
|
||||
|
||||
// uploadThumbnailStatus
|
||||
// {status === THUMBNAIL_STATUSES.API_DOWN || status === THUMBNAIL_STATUSES.MANUAL ? (
|
||||
// {status === THUMBNAIL_STATUSES.READY && (
|
||||
// {status === THUMBNAIL_STATUSES.COMPLETE && thumbnail && (
|
||||
const [thumbError, setThumbError] = React.useState(false); // possibly existing URL
|
||||
|
||||
const updateThumb = (thumb: string) => {
|
||||
setThumbError(false);
|
||||
updateThumbnailParam(thumb);
|
||||
};
|
||||
if (inline) {
|
||||
return (
|
||||
<fieldset-section>
|
||||
<label>{__('Thumbnail')}</label>
|
||||
<div className="column">
|
||||
{thumbError && (
|
||||
<div
|
||||
className="column__item thumbnail-picker__preview"
|
||||
style={{ backgroundImage: `url(${ThumbnailMissingImage})` }}
|
||||
/>
|
||||
)}
|
||||
{!thumbError && (
|
||||
<div
|
||||
className="column__item thumbnail-picker__preview"
|
||||
style={{ backgroundImage: `url(${thumbnailParam || thumbnailForUri})` }}
|
||||
>
|
||||
<img
|
||||
style={{ display: 'none' }}
|
||||
src={thumbnailParam || thumbnailForUri}
|
||||
alt={__('Thumbnail Preview')}
|
||||
onError={() => {
|
||||
if (thumbnailParam) {
|
||||
setThumbError(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="column__item">
|
||||
{/* if upload */}
|
||||
<SelectAsset
|
||||
inline
|
||||
onUpdate={updateThumb}
|
||||
currentValue={thumbnailParam}
|
||||
assetName={'Image'}
|
||||
recommended={'(16:9)'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
body={
|
||||
<div className="column">
|
||||
{thumbError && (
|
||||
<div
|
||||
className="column__item thumbnail-preview"
|
||||
style={{ backgroundImage: `url(${ThumbnailMissingImage})` }}
|
||||
/>
|
||||
)}
|
||||
{!thumbError && (
|
||||
<div
|
||||
className="column__item thumbnail-preview"
|
||||
style={{ backgroundImage: `url(${thumbnailParam || thumbnailForUri})` }}
|
||||
>
|
||||
<img
|
||||
style={{ display: 'none' }}
|
||||
src={thumbnailParam || thumbnailForUri}
|
||||
alt={__('Thumbnail Preview')}
|
||||
onError={() => {
|
||||
if (thumbnailParam) {
|
||||
setThumbError(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="column__item">
|
||||
{/* if upload */}
|
||||
<SelectAsset
|
||||
inline
|
||||
onUpdate={updateThumb}
|
||||
currentValue={thumbnailParam}
|
||||
assetName={'Thumbnail'}
|
||||
recommended={'(16:9)'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThumbnailPicker;
|
|
@ -15,7 +15,7 @@ type Props = {
|
|||
|
||||
function TransactionListTable(props: Props) {
|
||||
const { emptyMessage, rewards, loading, txos } = props;
|
||||
const REVOCABLE_TYPES = ['channel', 'stream', 'repost', 'support', 'claim'];
|
||||
const REVOCABLE_TYPES = ['channel', 'stream', 'repost', 'support', 'claim', 'collection'];
|
||||
function revokeClaim(tx: any, cb: (string) => void) {
|
||||
props.openModal(MODALS.CONFIRM_CLAIM_REVOKE, { tx, cb });
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import classnames from 'classnames';
|
|||
import { ComboboxOption } from '@reach/combobox';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import FileProperties from 'component/previewOverlayProperties';
|
||||
import ClaimProperties from 'component/claimProperties';
|
||||
|
||||
type Props = {
|
||||
claim: ?Claim,
|
||||
|
@ -18,6 +20,7 @@ export default function WunderbarSuggestion(props: Props) {
|
|||
}
|
||||
|
||||
const isChannel = claim.value_type === 'channel';
|
||||
const isCollection = claim.value_type === 'collection';
|
||||
|
||||
return (
|
||||
<ComboboxOption value={uri}>
|
||||
|
@ -26,7 +29,23 @@ export default function WunderbarSuggestion(props: Props) {
|
|||
'wunderbar__suggestion--channel': isChannel,
|
||||
})}
|
||||
>
|
||||
{isChannel ? <ChannelThumbnail uri={uri} /> : <FileThumbnail uri={uri} />}
|
||||
{isChannel && <ChannelThumbnail uri={uri} />}
|
||||
{!isChannel && (
|
||||
<FileThumbnail uri={uri}>
|
||||
{/* @if TARGET='app' */}
|
||||
{!isCollection && (
|
||||
<div className="claim-preview__file-property-overlay">
|
||||
<FileProperties uri={uri} small iconOnly />
|
||||
</div>
|
||||
)}
|
||||
{/* @endif */}
|
||||
{isCollection && (
|
||||
<div className="claim-preview__claim-property-overlay">
|
||||
<ClaimProperties uri={uri} small iconOnly />
|
||||
</div>
|
||||
)}
|
||||
</FileThumbnail>
|
||||
)}
|
||||
<span className="wunderbar__suggestion-label">
|
||||
<div className="wunderbar__suggestion-title">{claim.value.title}</div>
|
||||
<div className="wunderbar__suggestion-name">
|
||||
|
|
|
@ -51,8 +51,9 @@ export const CLAIM_TYPE = 'claim_type';
|
|||
export const CLAIM_CHANNEL = 'channel';
|
||||
export const CLAIM_STREAM = 'stream';
|
||||
export const CLAIM_REPOST = 'repost';
|
||||
export const CLAIM_TYPES = [CLAIM_CHANNEL, CLAIM_REPOST, CLAIM_STREAM];
|
||||
export const CLAIM_COLLECTION = 'collection';
|
||||
export const CLAIM_TYPES = [CLAIM_CHANNEL, CLAIM_REPOST, CLAIM_STREAM, CLAIM_COLLECTION];
|
||||
|
||||
export const CONTENT_ALL = 'all';
|
||||
export const CONTENT_TYPES = [CONTENT_ALL, CLAIM_CHANNEL, CLAIM_REPOST, ...FILE_TYPES];
|
||||
export const CONTENT_TYPES = [CONTENT_ALL, CLAIM_CHANNEL, CLAIM_REPOST, CLAIM_COLLECTION, ...FILE_TYPES];
|
||||
export const KEYS = [ORDER_BY_KEY, TAGS_KEY, FRESH_KEY, CONTENT_KEY, DURATION_KEY];
|
||||
|
|
2
ui/constants/collections.js
Normal file
2
ui/constants/collections.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
// in repo constants for collections ui
|
||||
export const ICON_SIZE = 12;
|
|
@ -158,3 +158,5 @@ export const MIND_BLOWN = 'MindBlown';
|
|||
export const LIVESTREAM = 'Livestream';
|
||||
export const LIVESTREAM_SOLID = 'LivestreamSolid';
|
||||
export const LIVESTREAM_MONOCHROME = 'LivestreamMono';
|
||||
export const STACK = 'stack';
|
||||
export const TIME = 'time';
|
||||
|
|
|
@ -43,3 +43,5 @@ export const IMAGE_UPLOAD = 'image_upload';
|
|||
export const MOBILE_SEARCH = 'mobile_search';
|
||||
export const VIEW_IMAGE = 'view_image';
|
||||
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
|
||||
export const COLLECTION_ADD = 'collection_add';
|
||||
export const COLLECTION_DELETE = 'collection_delete';
|
||||
|
|
|
@ -24,6 +24,7 @@ exports.WILD_WEST = 'wildwest';
|
|||
exports.HOME = 'home';
|
||||
exports.HELP = 'help';
|
||||
exports.LIBRARY = 'library';
|
||||
exports.LISTS = 'lists';
|
||||
exports.INVITE = 'invite';
|
||||
exports.DEPRECATED__PUBLISH = 'publish';
|
||||
exports.DEPRECATED__PUBLISHED = 'published';
|
||||
|
@ -70,3 +71,4 @@ exports.YOUTUBE_SYNC = 'youtube';
|
|||
exports.LIVESTREAM = 'livestream';
|
||||
exports.LIVESTREAM_CURRENT = 'live';
|
||||
exports.GENERAL = 'general';
|
||||
exports.LIST = 'list';
|
||||
|
|
26
ui/constants/user.js
Normal file
26
ui/constants/user.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
export const ID = 'id';
|
||||
export const LANGUAGE = 'language';
|
||||
export const GIVEN_NAME = 'given_name';
|
||||
export const FAMILY_NAME = 'family_name';
|
||||
export const CREATED_AT = 'created_at';
|
||||
export const UPDATED_AT = 'updated_at';
|
||||
export const INVITED_BY_ID = 'invited_by_id';
|
||||
export const INVITED_AT = 'invited_at';
|
||||
export const INVITES_REMANING = 'invites_remaining';
|
||||
export const INVITE_REWARD_CLAIMED = 'invite_reward_claimed';
|
||||
export const IS_EMAIL_ENABLED = 'is_email_enabled';
|
||||
export const PUBLISH_ID = 'publish_id';
|
||||
export const COUNTRY = 'country';
|
||||
export const IS_ODYSEE_USER = 'is_odysee_user';
|
||||
export const LOCATION = 'location';
|
||||
export const YOUTUBE_CHANNELS = 'youtube_channels';
|
||||
export const PRIMARY_EMAIL = 'primary_email';
|
||||
export const PASSWORD_SET = 'password_set';
|
||||
export const LATEST_CLAIMED_EMAIL = 'latest_claimed_email';
|
||||
export const HAS_VERIFIED_EMAIL = 'has_verified_email';
|
||||
export const IS_IDENTITY_VERIFIED = 'is_identity_verified';
|
||||
export const IS_REWARD_APPROVED = 'is_reward_approved';
|
||||
export const ODYSEE_LIVE_ENABLED = 'odysee_live_enabled';
|
||||
export const ODYSEE_LIVE_DISABLED = 'odysee_live_disabled';
|
||||
export const GLOBAL_MOD = 'global_mod';
|
||||
export const EXPERIMENTAL_UI = 'experimental_ui';
|
|
@ -15,6 +15,7 @@ export default function useGetThumbnail(
|
|||
const isImage = claim && claim.value && claim.value.stream_type === 'image';
|
||||
// $FlowFixMe
|
||||
const isFree = claim && claim.value && (!claim.value.fee || Number(claim.value.fee.amount) <= 0);
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
const thumbnailInClaim = claim && claim.value && claim.value.thumbnail && claim.value.thumbnail.url;
|
||||
|
||||
// @if TARGET='web'
|
||||
|
@ -22,6 +23,8 @@ export default function useGetThumbnail(
|
|||
thumbnailToUse = thumbnailInClaim;
|
||||
} else if (claim && isImage && isFree) {
|
||||
thumbnailToUse = generateStreamUrl(claim.name, claim.claim_id);
|
||||
} else if (isCollection) {
|
||||
thumbnailToUse = 'http://spee.ch/default-thumb-odysee:e.jpg';
|
||||
}
|
||||
// @endif
|
||||
|
||||
|
|
11
ui/modal/modalClaimCollectionAdd/index.js
Normal file
11
ui/modal/modalClaimCollectionAdd/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doHideModal } from 'redux/actions/app';
|
||||
import ClaimCollectionAdd from './view';
|
||||
|
||||
const select = (state) => ({});
|
||||
|
||||
const perform = {
|
||||
doHideModal,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(ClaimCollectionAdd);
|
19
ui/modal/modalClaimCollectionAdd/view.jsx
Normal file
19
ui/modal/modalClaimCollectionAdd/view.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import ClaimCollectionAdd from 'component/claimCollectionAdd';
|
||||
import { Modal } from 'modal/modal';
|
||||
|
||||
type Props = {
|
||||
doHideModal: () => void,
|
||||
uri: string,
|
||||
};
|
||||
|
||||
const ModalClaimCollectionAdd = (props: Props) => {
|
||||
const { doHideModal, uri } = props;
|
||||
return (
|
||||
<Modal isOpen type="card" onAborted={doHideModal}>
|
||||
<ClaimCollectionAdd uri={uri} closeModal={doHideModal} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default ModalClaimCollectionAdd;
|
29
ui/modal/modalRemoveCollection/index.js
Normal file
29
ui/modal/modalRemoveCollection/index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectIsAbandoningClaimForUri,
|
||||
doCollectionDelete,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectNameForCollectionId,
|
||||
} from 'lbry-redux';
|
||||
import { doHideModal } from 'redux/actions/app';
|
||||
import ModalRemoveCollection from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForClaimId(props.collectionId)(state);
|
||||
const uri = (claim && (claim.canonical_url || claim.permanent_url)) || null;
|
||||
return {
|
||||
claim,
|
||||
uri,
|
||||
claimIsMine: makeSelectClaimIsMine(uri)(state),
|
||||
isAbandoning: makeSelectIsAbandoningClaimForUri(uri)(state),
|
||||
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
collectionDelete: (id) => dispatch(doCollectionDelete(id)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ModalRemoveCollection);
|
65
ui/modal/modalRemoveCollection/view.jsx
Normal file
65
ui/modal/modalRemoveCollection/view.jsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
// @flow
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from 'modal/modal';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FormField } from 'component/common/form';
|
||||
|
||||
type Props = {
|
||||
closeModal: () => void,
|
||||
collectionDelete: (string, ?string) => void,
|
||||
claim: Claim,
|
||||
collectionId: string,
|
||||
collectionName: string,
|
||||
uri: ?string,
|
||||
redirect: ?string,
|
||||
};
|
||||
|
||||
function ModalRemoveCollection(props: Props) {
|
||||
const { closeModal, claim, collectionDelete, collectionId, collectionName, uri, redirect } = props;
|
||||
const title = claim && claim.value && claim.value.title;
|
||||
const { replace } = useHistory();
|
||||
const [confirmName, setConfirmName] = useState('');
|
||||
|
||||
return (
|
||||
<Modal isOpen contentLabel={__('Confirm Collection Unpublish')} type="card" onAborted={closeModal}>
|
||||
<Card
|
||||
title={__('Delete List')}
|
||||
body={
|
||||
uri ? (
|
||||
<React.Fragment>
|
||||
<p>{__('This will permanently delete the list.')}</p>
|
||||
<p>{__('Type "%name%" to confirm.', { name: collectionName })}</p>
|
||||
<FormField value={confirmName} type={'text'} onChange={(e) => setConfirmName(e.target.value)} />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<I18nMessage tokens={{ title: <cite>{uri && title ? `"${title}"` : `"${collectionName}"`}</cite> }}>
|
||||
Are you sure you'd like to remove collection %title%?
|
||||
</I18nMessage>
|
||||
)
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Delete')}
|
||||
disabled={uri && collectionName !== confirmName}
|
||||
onClick={() => {
|
||||
if (redirect) replace(redirect);
|
||||
collectionDelete(collectionId, uri ? 'resolved' : undefined);
|
||||
closeModal();
|
||||
}}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={closeModal} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalRemoveCollection;
|
|
@ -40,6 +40,8 @@ import ModalMobileSearch from 'modal/modalMobileSearch';
|
|||
import ModalViewImage from 'modal/modalViewImage';
|
||||
import ModalMassTipsUnlock from 'modal/modalMassTipUnlock';
|
||||
import ModalRemoveBtcSwapAddress from 'modal/modalRemoveBtcSwapAddress';
|
||||
import ModalClaimCollectionAdd from 'modal/modalClaimCollectionAdd';
|
||||
import ModalDeleteCollection from 'modal/modalRemoveCollection';
|
||||
|
||||
type Props = {
|
||||
modal: { id: string, modalProps: {} },
|
||||
|
@ -143,6 +145,10 @@ function ModalRouter(props: Props) {
|
|||
return <ModalMassTipsUnlock {...modalProps} />;
|
||||
case MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS:
|
||||
return <ModalRemoveBtcSwapAddress {...modalProps} />;
|
||||
case MODALS.COLLECTION_ADD:
|
||||
return <ModalClaimCollectionAdd {...modalProps} />;
|
||||
case MODALS.COLLECTION_DELETE:
|
||||
return <ModalDeleteCollection {...modalProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import ClaimMenuList from 'component/claimMenuList';
|
|||
import Yrbl from 'component/yrbl';
|
||||
|
||||
export const PAGE_VIEW_QUERY = `view`;
|
||||
const CONTENT_PAGE = 'content';
|
||||
const LISTS_PAGE = 'lists';
|
||||
const ABOUT_PAGE = `about`;
|
||||
export const DISCUSSION_PAGE = `discussion`;
|
||||
const EDIT_PAGE = 'edit';
|
||||
|
@ -57,7 +59,7 @@ function ChannelPage(props: Props) {
|
|||
claim,
|
||||
title,
|
||||
cover,
|
||||
page,
|
||||
// page, ?page= may come back some day?
|
||||
channelIsMine,
|
||||
isSubscribed,
|
||||
blackListedOutpoints,
|
||||
|
@ -107,15 +109,34 @@ function ChannelPage(props: Props) {
|
|||
// If a user changes tabs, update the url so it stays on the same page if they refresh.
|
||||
// We don't want to use links here because we can't animate the tab change and using links
|
||||
// would alter the Tab label's role attribute, which should stay role="tab" to work with keyboards/screen readers.
|
||||
const tabIndex = currentView === ABOUT_PAGE || editing ? 1 : currentView === DISCUSSION_PAGE ? 2 : 0;
|
||||
let tabIndex;
|
||||
switch (currentView) {
|
||||
case CONTENT_PAGE:
|
||||
tabIndex = 0;
|
||||
break;
|
||||
case LISTS_PAGE:
|
||||
tabIndex = 1;
|
||||
break;
|
||||
case ABOUT_PAGE:
|
||||
tabIndex = 2;
|
||||
break;
|
||||
case DISCUSSION_PAGE:
|
||||
tabIndex = 3;
|
||||
break;
|
||||
default:
|
||||
tabIndex = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
function onTabChange(newTabIndex) {
|
||||
let url = formatLbryUrlForWeb(uri);
|
||||
let search = '?';
|
||||
|
||||
if (newTabIndex === 0) {
|
||||
search += `page=${page}`;
|
||||
search += `${PAGE_VIEW_QUERY}=${CONTENT_PAGE}`;
|
||||
} else if (newTabIndex === 1) {
|
||||
search += `${PAGE_VIEW_QUERY}=${LISTS_PAGE}`;
|
||||
} else if (newTabIndex === 2) {
|
||||
search += `${PAGE_VIEW_QUERY}=${ABOUT_PAGE}`;
|
||||
} else {
|
||||
search += `${PAGE_VIEW_QUERY}=${DISCUSSION_PAGE}`;
|
||||
|
@ -164,6 +185,7 @@ function ChannelPage(props: Props) {
|
|||
{!channelIsBlackListed && <ShareButton uri={uri} />}
|
||||
{!(isBlocked || isMuted) && <ClaimSupportButton uri={uri} />}
|
||||
{!(isBlocked || isMuted) && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
|
||||
{/* TODO: add channel collections <ClaimCollectionAddButton uri={uri} fileAction /> */}
|
||||
<ClaimMenuList uri={claim.permanent_url} inline />
|
||||
</div>
|
||||
{cover && <img className={classnames('channel-cover__custom')} src={cover} />}
|
||||
|
@ -228,13 +250,27 @@ function ChannelPage(props: Props) {
|
|||
) : (
|
||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||
<TabList className="tabs__list--channel-page">
|
||||
<Tab disabled={editing}>{__('Content')}</Tab>
|
||||
<Tab disabled={editing}>{__('Publishes')}</Tab>
|
||||
<Tab disabled={editing}>{__('Playlists')}</Tab>
|
||||
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab>
|
||||
<Tab disabled={editing}>{__('Community')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<ChannelContent uri={uri} channelIsBlackListed={channelIsBlackListed} viewHiddenChannels />
|
||||
<ChannelContent
|
||||
claimType={'stream'}
|
||||
uri={uri}
|
||||
channelIsBlackListed={channelIsBlackListed}
|
||||
viewHiddenChannels
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ChannelContent
|
||||
claimType={'collection'}
|
||||
uri={uri}
|
||||
channelIsBlackListed={channelIsBlackListed}
|
||||
viewHiddenChannels
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ChannelAbout uri={uri} />
|
||||
|
|
57
ui/page/collection/index.js
Normal file
57
ui/page/collection/index.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import CollectionPage from './view';
|
||||
import {
|
||||
doFetchItemsInCollection,
|
||||
makeSelectCollectionForId,
|
||||
makeSelectUrlsForCollectionId,
|
||||
makeSelectIsResolvingCollectionForId,
|
||||
makeSelectTitleForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimIsPending,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectCollectionIsMine,
|
||||
doLocalCollectionDelete,
|
||||
doCollectionEdit,
|
||||
makeSelectChannelForClaimUri,
|
||||
makeSelectCountForCollectionId,
|
||||
makeSelectEditedCollectionForId,
|
||||
} from 'lbry-redux';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { match } = props;
|
||||
const { params } = match;
|
||||
const { collectionId } = params;
|
||||
|
||||
const claim = collectionId && makeSelectClaimForClaimId(collectionId)(state);
|
||||
const uri = (claim && (claim.canonical_url || claim.permanent_url)) || null;
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
claim,
|
||||
collection: makeSelectCollectionForId(collectionId)(state),
|
||||
collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state),
|
||||
collectionCount: makeSelectCountForCollectionId(collectionId)(state),
|
||||
isResolvingCollection: makeSelectIsResolvingCollectionForId(collectionId)(state),
|
||||
title: makeSelectTitleForUri(uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(uri)(state),
|
||||
isMyClaim: makeSelectClaimIsMine(uri)(state), // or collection is mine?
|
||||
isMyCollection: makeSelectCollectionIsMine(collectionId)(state),
|
||||
claimIsPending: makeSelectClaimIsPending(uri)(state),
|
||||
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(collectionId)(state)),
|
||||
uri,
|
||||
user: selectUser(state),
|
||||
channel: uri && makeSelectChannelForClaimUri(uri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
fetchCollectionItems: (claimId, cb) => dispatch(doFetchItemsInCollection({ collectionId: claimId }, cb)), // if this collection is not resolved, resolve it
|
||||
deleteCollection: (id) => dispatch(doLocalCollectionDelete(id)),
|
||||
editCollection: (id, params) => dispatch(doCollectionEdit(id, params)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(CollectionPage));
|
175
ui/page/collection/view.jsx
Normal file
175
ui/page/collection/view.jsx
Normal file
|
@ -0,0 +1,175 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import ClaimList from 'component/claimList';
|
||||
import Page from 'component/page';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import CollectionEdit from 'component/collectionEdit';
|
||||
import Card from 'component/common/card';
|
||||
import CollectionActions from 'component/collectionActions';
|
||||
import classnames from 'classnames';
|
||||
import ClaimAuthor from 'component/claimAuthor';
|
||||
import FileDescription from 'component/fileDescription';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
export const PAGE_VIEW_QUERY = 'view';
|
||||
export const EDIT_PAGE = 'edit';
|
||||
|
||||
type Props = {
|
||||
collectionId: string,
|
||||
uri: string,
|
||||
claim: Claim,
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
collection: Collection,
|
||||
collectionUrls: Array<string>,
|
||||
collectionCount: number,
|
||||
isResolvingCollection: boolean,
|
||||
isMyClaim: boolean,
|
||||
isMyCollection: boolean,
|
||||
claimIsPending: boolean,
|
||||
collectionHasEdits: boolean,
|
||||
deleteCollection: (string) => void,
|
||||
editCollection: (string, CollectionEditParams) => void,
|
||||
fetchCollectionItems: (string, () => void) => void,
|
||||
resolveUris: (string) => void,
|
||||
user: ?User,
|
||||
};
|
||||
|
||||
export default function CollectionPage(props: Props) {
|
||||
const {
|
||||
collectionId,
|
||||
uri,
|
||||
claim,
|
||||
collection,
|
||||
collectionUrls,
|
||||
collectionCount,
|
||||
collectionHasEdits,
|
||||
claimIsPending,
|
||||
isResolvingCollection,
|
||||
fetchCollectionItems,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
replace,
|
||||
location: { search },
|
||||
} = useHistory();
|
||||
|
||||
const [didTryResolve, setDidTryResolve] = React.useState(false);
|
||||
const [showInfo, setShowInfo] = React.useState(false);
|
||||
const { name, totalItems } = collection || {};
|
||||
const isBuiltin = COLLECTIONS_CONSTS.BUILTIN_LISTS.includes(collectionId);
|
||||
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const editing = urlParams.get(PAGE_VIEW_QUERY) === EDIT_PAGE;
|
||||
|
||||
const urlsReady =
|
||||
collectionUrls && (totalItems === undefined || (totalItems && totalItems === collectionUrls.length));
|
||||
const shouldFetch = !claim && !collection;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (collectionId && !urlsReady && !didTryResolve && shouldFetch) {
|
||||
fetchCollectionItems(collectionId, () => setDidTryResolve(true));
|
||||
}
|
||||
}, [collectionId, urlsReady, didTryResolve, shouldFetch, setDidTryResolve, fetchCollectionItems]);
|
||||
|
||||
const pending = (
|
||||
<div className="help card__title--help">
|
||||
<Spinner type={'small'} />
|
||||
{__('Your publish is being confirmed and will be live soon')}
|
||||
</div>
|
||||
);
|
||||
|
||||
const unpublished = <span className="help">{__('Unpublished Edit')}</span>;
|
||||
|
||||
let titleActions;
|
||||
if (collectionHasEdits) {
|
||||
titleActions = unpublished;
|
||||
} else if (claimIsPending) {
|
||||
titleActions = pending;
|
||||
}
|
||||
|
||||
const subTitle = (
|
||||
<div>
|
||||
{uri ? <span>{collectionCount} items</span> : <span>{collectionCount} items</span>}
|
||||
{uri && <ClaimAuthor uri={uri} />}
|
||||
</div>
|
||||
);
|
||||
const info = (
|
||||
<Card
|
||||
title={
|
||||
<span>
|
||||
<Icon icon={ICONS.STACK} className="icon--margin-right" />
|
||||
{claim ? claim.value.title || claim.name : collection && collection.name}
|
||||
{collectionHasEdits && <span className={'collection-title__hasEdits'}>(*)</span>}
|
||||
</span>
|
||||
}
|
||||
titleActions={titleActions}
|
||||
subtitle={subTitle}
|
||||
body={
|
||||
!isBuiltin && (
|
||||
<CollectionActions uri={uri} collectionId={collectionId} setShowInfo={setShowInfo} showInfo={showInfo} />
|
||||
)
|
||||
}
|
||||
actions={
|
||||
showInfo &&
|
||||
uri && (
|
||||
<div className="section">
|
||||
<FileDescription uri={uri} expandOverride />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!collection && (isResolvingCollection || !didTryResolve)) {
|
||||
return (
|
||||
<Page>
|
||||
<h2 className="main--empty empty">{__('Loading...')}</h2>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (!collection && !isResolvingCollection && didTryResolve) {
|
||||
return (
|
||||
<Page>
|
||||
<h2 className="main--empty empty">{__('Nothing here')}</h2>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<Page
|
||||
noFooter
|
||||
noSideNavigation={editing}
|
||||
backout={{
|
||||
title: __('%action% %collection%', { collection: name, action: uri ? __('Editing') : __('Publishing') }),
|
||||
simpleTitle: uri ? __('Editing') : __('Publishing'),
|
||||
}}
|
||||
>
|
||||
<CollectionEdit
|
||||
uri={uri}
|
||||
collectionId={collectionId}
|
||||
onDone={(id) => {
|
||||
replace(`/$/${PAGES.LIST}/${id}`);
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (urlsReady) {
|
||||
return (
|
||||
<Page>
|
||||
<div className={classnames('section card-stack')}>
|
||||
{info}
|
||||
<ClaimList uris={collectionUrls} collectionId={collectionId} />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,8 @@ import {
|
|||
makeSelectTagInClaimOrChannelForUri,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimIsStreamPlaceholder,
|
||||
makeSelectCollectionForId,
|
||||
COLLECTIONS_CONSTS,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
||||
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
|
@ -23,6 +25,7 @@ const select = (state, props) => {
|
|||
const { search } = props.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const linkedCommentId = urlParams.get('lc');
|
||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
||||
|
||||
return {
|
||||
linkedComment: makeSelectCommentForCommentId(linkedCommentId)(state),
|
||||
|
@ -36,6 +39,8 @@ const select = (state, props) => {
|
|||
commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||
collection: makeSelectCollectionForId(collectionId)(state),
|
||||
collectionId,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import FileRenderInitiator from 'component/fileRenderInitiator';
|
|||
import FileRenderInline from 'component/fileRenderInline';
|
||||
import FileRenderDownload from 'component/fileRenderDownload';
|
||||
import RecommendedContent from 'component/recommendedContent';
|
||||
import CollectionContent from 'component/collectionContentSidebar';
|
||||
import CommentsList from 'component/commentsList';
|
||||
import PostViewer from 'component/postViewer';
|
||||
import Empty from 'component/common/empty';
|
||||
|
@ -26,6 +27,8 @@ type Props = {
|
|||
isMature: boolean,
|
||||
linkedComment: any,
|
||||
setPrimaryUri: (?string) => void,
|
||||
collection?: Collection,
|
||||
collectionId: string,
|
||||
videoTheaterMode: boolean,
|
||||
commentsDisabled: boolean,
|
||||
};
|
||||
|
@ -45,6 +48,8 @@ function FilePage(props: Props) {
|
|||
setPrimaryUri,
|
||||
videoTheaterMode,
|
||||
commentsDisabled,
|
||||
collection,
|
||||
collectionId,
|
||||
} = props;
|
||||
const cost = costInfo ? costInfo.cost : null;
|
||||
const hasFileInfo = fileInfo !== undefined;
|
||||
|
@ -115,8 +120,12 @@ function FilePage(props: Props) {
|
|||
|
||||
if (obscureNsfw && isMature) {
|
||||
return (
|
||||
<Page>
|
||||
<FileTitleSection uri={uri} isNsfwBlocked />
|
||||
<Page className="file-page" filePage isMarkdown={isMarkdown}>
|
||||
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
|
||||
<FileTitleSection uri={uri} isNsfwBlocked />
|
||||
</div>
|
||||
{collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
@ -133,12 +142,13 @@ function FilePage(props: Props) {
|
|||
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
|
||||
{!commentsDisabled && <CommentsList uri={uri} linkedComment={linkedComment} />}
|
||||
</div>
|
||||
{videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{!collection && !isMarkdown && videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
{isMarkdown && (
|
||||
<div className="file-page__post-comments">
|
||||
<CommentsList uri={uri} linkedComment={linkedComment} />
|
||||
|
|
|
@ -19,7 +19,7 @@ type Props = {
|
|||
fetchingFileList: boolean,
|
||||
downloadedUrls: Array<string>,
|
||||
downloadedUrlsCount: ?number,
|
||||
history: { replace: string => void },
|
||||
history: { replace: (string) => void },
|
||||
query: string,
|
||||
doPurchaseList: () => void,
|
||||
myDownloads: Array<string>,
|
||||
|
|
31
ui/page/lists/index.js
Normal file
31
ui/page/lists/index.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectDownloadUrlsCount,
|
||||
selectIsFetchingFileList,
|
||||
selectMyPurchases,
|
||||
selectIsFetchingMyPurchases,
|
||||
doPurchaseList,
|
||||
selectBuiltinCollections,
|
||||
selectMyPublishedMixedCollections,
|
||||
selectMyPublishedPlaylistCollections,
|
||||
selectMyUnpublishedCollections, // should probably distinguish types
|
||||
// selectSavedCollections, // TODO: implement saving and copying collections
|
||||
} from 'lbry-redux';
|
||||
|
||||
import ListsPage from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
allDownloadedUrlsCount: selectDownloadUrlsCount(state),
|
||||
fetchingFileList: selectIsFetchingFileList(state),
|
||||
myPurchases: selectMyPurchases(state),
|
||||
fetchingMyPurchases: selectIsFetchingMyPurchases(state),
|
||||
builtinCollections: selectBuiltinCollections(state),
|
||||
publishedCollections: selectMyPublishedMixedCollections(state),
|
||||
publishedPlaylists: selectMyPublishedPlaylistCollections(state),
|
||||
unpublishedCollections: selectMyUnpublishedCollections(state),
|
||||
// savedCollections: selectSavedCollections(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doPurchaseList,
|
||||
})(ListsPage);
|
22
ui/page/lists/view.jsx
Normal file
22
ui/page/lists/view.jsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import Page from 'component/page';
|
||||
import CollectionsListMine from 'component/collectionsListMine';
|
||||
import Icon from 'component/common/icon';
|
||||
|
||||
function ListsPage() {
|
||||
return (
|
||||
<Page>
|
||||
<label className="claim-list__header-label">
|
||||
<span>
|
||||
<Icon icon={ICONS.STACK} size={10} />
|
||||
{__('Lists')}
|
||||
</span>
|
||||
</label>
|
||||
<CollectionsListMine />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListsPage;
|
|
@ -1,6 +1,7 @@
|
|||
import * as PAGES from 'constants/pages';
|
||||
import { DOMAIN } from 'config';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { PAGE_SIZE } from 'constants/claim';
|
||||
import {
|
||||
doResolveUri,
|
||||
|
@ -14,6 +15,11 @@ import {
|
|||
makeSelectClaimIsStreamPlaceholder,
|
||||
doClearPublish,
|
||||
doPrepareEdit,
|
||||
doFetchItemsInCollection,
|
||||
makeSelectCollectionForId,
|
||||
makeSelectUrlsForCollectionId,
|
||||
makeSelectIsResolvingCollectionForId,
|
||||
COLLECTIONS_CONSTS,
|
||||
} from 'lbry-redux';
|
||||
import { push } from 'connected-react-router';
|
||||
import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions';
|
||||
|
@ -23,6 +29,8 @@ import ShowPage from './view';
|
|||
const select = (state, props) => {
|
||||
const { pathname, hash, search } = props.location;
|
||||
const urlPath = pathname + hash;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
|
||||
// Remove the leading "/" added by the browser
|
||||
let path = urlPath.slice(1).replace(/:/g, '#');
|
||||
|
||||
|
@ -54,10 +62,15 @@ const select = (state, props) => {
|
|||
props.history.replace(`/${path.slice(0, match.index)}`);
|
||||
}
|
||||
}
|
||||
const claim = makeSelectClaimForUri(uri)(state);
|
||||
const collectionId =
|
||||
urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) ||
|
||||
(claim && claim.value_type === 'collection' && claim.claim_id) ||
|
||||
null;
|
||||
|
||||
return {
|
||||
uri,
|
||||
claim: makeSelectClaimForUri(uri)(state),
|
||||
claim,
|
||||
isResolvingUri: makeSelectIsUriResolving(uri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
totalPages: makeSelectTotalPagesForChannel(uri, PAGE_SIZE)(state),
|
||||
|
@ -66,6 +79,10 @@ const select = (state, props) => {
|
|||
claimIsMine: makeSelectClaimIsMine(uri)(state),
|
||||
claimIsPending: makeSelectClaimIsPending(uri)(state),
|
||||
isLivestream: makeSelectClaimIsStreamPlaceholder(uri)(state),
|
||||
collection: makeSelectCollectionForId(collectionId)(state),
|
||||
collectionId: collectionId,
|
||||
collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state),
|
||||
isResolvingCollection: makeSelectIsResolvingCollectionForId(collectionId)(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -76,6 +93,7 @@ const perform = (dispatch) => ({
|
|||
dispatch(doPrepareEdit({ name }));
|
||||
dispatch(push(`/$/${PAGES.UPLOAD}`));
|
||||
},
|
||||
fetchCollectionItems: (claimId) => dispatch(doFetchItemsInCollection({ collectionId: claimId })),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ShowPage);
|
||||
export default withRouter(connect(select, perform)(ShowPage));
|
||||
|
|
|
@ -13,7 +13,7 @@ import Card from 'component/common/card';
|
|||
import AbandonedChannelPreview from 'component/abandonedChannelPreview';
|
||||
import Yrbl from 'component/yrbl';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
isResolvingUri: boolean,
|
||||
|
@ -31,6 +31,11 @@ type Props = {
|
|||
claimIsPending: boolean,
|
||||
isLivestream: boolean,
|
||||
beginPublish: (string) => void,
|
||||
collectionId: string,
|
||||
collection: Collection,
|
||||
collectionUrls: Array<string>,
|
||||
isResolvingCollection: boolean,
|
||||
fetchCollectionItems: (string) => void,
|
||||
};
|
||||
|
||||
function ShowPage(props: Props) {
|
||||
|
@ -46,8 +51,15 @@ function ShowPage(props: Props) {
|
|||
claimIsPending,
|
||||
isLivestream,
|
||||
beginPublish,
|
||||
fetchCollectionItems,
|
||||
collectionId,
|
||||
collection,
|
||||
collectionUrls,
|
||||
isResolvingCollection,
|
||||
} = props;
|
||||
|
||||
const { search } = location;
|
||||
|
||||
const signingChannel = claim && claim.signing_channel;
|
||||
const canonicalUrl = claim && claim.canonical_url;
|
||||
const claimExists = claim !== null && claim !== undefined;
|
||||
|
@ -55,6 +67,15 @@ function ShowPage(props: Props) {
|
|||
const isMine = claim && claim.is_my_output;
|
||||
const { contentName, isChannel } = parseURI(uri);
|
||||
const { push } = useHistory();
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
const resolvedCollection = collection && collection.id; // not null
|
||||
|
||||
// changed this from 'isCollection' to resolve strangers' collections.
|
||||
React.useEffect(() => {
|
||||
if (collectionId && !resolvedCollection) {
|
||||
fetchCollectionItems(collectionId);
|
||||
}
|
||||
}, [isCollection, resolvedCollection, collectionId, fetchCollectionItems]);
|
||||
|
||||
useEffect(() => {
|
||||
// @if TARGET='web'
|
||||
|
@ -84,12 +105,24 @@ function ShowPage(props: Props) {
|
|||
const newUrl = formatLbryUrlForWeb(claim.canonical_url);
|
||||
return <Redirect to={newUrl} />;
|
||||
}
|
||||
let urlForCollectionZero;
|
||||
if (claim && claim.value_type === 'collection' && collectionUrls && collectionUrls.length) {
|
||||
urlForCollectionZero = collectionUrls && collectionUrls[0];
|
||||
const claimId = claim.claim_id;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
urlParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, claimId);
|
||||
const newUrl = formatLbryUrlForWeb(`${urlForCollectionZero}?${urlParams.toString()}`);
|
||||
return <Redirect to={newUrl} />;
|
||||
}
|
||||
|
||||
let innerContent = '';
|
||||
if (!claim || (claim && !claim.name)) {
|
||||
innerContent = (
|
||||
<Page>
|
||||
{(claim === undefined || isResolvingUri) && (
|
||||
{(claim === undefined ||
|
||||
isResolvingUri ||
|
||||
isResolvingCollection || // added for collection
|
||||
(claim && claim.value_type === 'collection' && !urlForCollectionZero)) && ( // added for collection - make sure we accept urls = []
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import { connectRouter } from 'connected-react-router';
|
||||
import { claimsReducer, fileInfoReducer, walletReducer, publishReducer } from 'lbry-redux';
|
||||
import { claimsReducer, fileInfoReducer, walletReducer, publishReducer, collectionsReducer } from 'lbry-redux';
|
||||
import { costInfoReducer, blacklistReducer, filteredReducer, homepageReducer, statsReducer, webReducer } from 'lbryinc';
|
||||
import appReducer from 'redux/reducers/app';
|
||||
import tagsReducer from 'redux/reducers/tags';
|
||||
|
@ -48,4 +48,5 @@ export default (history) =>
|
|||
wallet: walletReducer,
|
||||
sync: syncReducer,
|
||||
web: webReducer,
|
||||
collections: collectionsReducer,
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
makeSelectClaimIsMine,
|
||||
doPopulateSharedUserState,
|
||||
doFetchChannelListMine,
|
||||
doFetchCollectionListMine,
|
||||
doClearPublish,
|
||||
doPreferenceGet,
|
||||
doClearSupport,
|
||||
|
@ -552,6 +553,7 @@ export function doSignIn() {
|
|||
// @if TARGET='web'
|
||||
dispatch(doBalanceSubscribe());
|
||||
dispatch(doFetchChannelListMine());
|
||||
dispatch(doFetchCollectionListMine());
|
||||
// @endif
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,26 +1,29 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
export const selectState = state => state.user || {};
|
||||
export const selectState = (state) => state.user || {};
|
||||
|
||||
export const selectAuthenticationIsPending = createSelector(selectState, state => state.authenticationIsPending);
|
||||
export const selectAuthenticationIsPending = createSelector(selectState, (state) => state.authenticationIsPending);
|
||||
|
||||
export const selectUserIsPending = createSelector(selectState, state => state.userIsPending);
|
||||
export const selectUserIsPending = createSelector(selectState, (state) => state.userIsPending);
|
||||
|
||||
export const selectUser = createSelector(selectState, state => state.user);
|
||||
export const selectUser = createSelector(selectState, (state) => state.user);
|
||||
|
||||
export const selectEmailAlreadyExists = createSelector(selectState, state => state.emailAlreadyExists);
|
||||
export const selectEmailAlreadyExists = createSelector(selectState, (state) => state.emailAlreadyExists);
|
||||
|
||||
export const selectEmailDoesNotExist = createSelector(selectState, state => state.emailDoesNotExist);
|
||||
export const selectEmailDoesNotExist = createSelector(selectState, (state) => state.emailDoesNotExist);
|
||||
|
||||
export const selectResendingVerificationEmail = createSelector(selectState, state => state.resendingVerificationEmail);
|
||||
export const selectResendingVerificationEmail = createSelector(
|
||||
selectState,
|
||||
(state) => state.resendingVerificationEmail
|
||||
);
|
||||
|
||||
export const selectUserEmail = createSelector(selectUser, user =>
|
||||
export const selectUserEmail = createSelector(selectUser, (user) =>
|
||||
user ? user.primary_email || user.latest_claimed_email : null
|
||||
);
|
||||
|
||||
export const selectUserPhone = createSelector(selectUser, user => (user ? user.phone_number : null));
|
||||
export const selectUserPhone = createSelector(selectUser, (user) => (user ? user.phone_number : null));
|
||||
|
||||
export const selectUserCountryCode = createSelector(selectUser, user => (user ? user.country_code : null));
|
||||
export const selectUserCountryCode = createSelector(selectUser, (user) => (user ? user.country_code : null));
|
||||
|
||||
export const selectEmailToVerify = createSelector(
|
||||
selectState,
|
||||
|
@ -34,92 +37,95 @@ export const selectPhoneToVerify = createSelector(
|
|||
(state, userPhone) => state.phoneToVerify || userPhone
|
||||
);
|
||||
|
||||
export const selectYoutubeChannels = createSelector(selectUser, user => (user ? user.youtube_channels : null));
|
||||
export const selectYoutubeChannels = createSelector(selectUser, (user) => (user ? user.youtube_channels : null));
|
||||
|
||||
export const selectUserIsRewardApproved = createSelector(selectUser, user => user && user.is_reward_approved);
|
||||
export const selectUserIsRewardApproved = createSelector(selectUser, (user) => user && user.is_reward_approved);
|
||||
|
||||
export const selectEmailNewIsPending = createSelector(selectState, state => state.emailNewIsPending);
|
||||
export const selectEmailNewIsPending = createSelector(selectState, (state) => state.emailNewIsPending);
|
||||
|
||||
export const selectEmailNewErrorMessage = createSelector(selectState, state => {
|
||||
export const selectEmailNewErrorMessage = createSelector(selectState, (state) => {
|
||||
const error = state.emailNewErrorMessage;
|
||||
return typeof error === 'object' && error !== null ? error.message : error;
|
||||
});
|
||||
|
||||
export const selectPasswordExists = createSelector(selectState, state => state.passwordExistsForUser);
|
||||
export const selectPasswordExists = createSelector(selectState, (state) => state.passwordExistsForUser);
|
||||
|
||||
export const selectPasswordResetIsPending = createSelector(selectState, state => state.passwordResetPending);
|
||||
export const selectPasswordResetIsPending = createSelector(selectState, (state) => state.passwordResetPending);
|
||||
|
||||
export const selectPasswordResetSuccess = createSelector(selectState, state => state.passwordResetSuccess);
|
||||
export const selectPasswordResetSuccess = createSelector(selectState, (state) => state.passwordResetSuccess);
|
||||
|
||||
export const selectPasswordResetError = createSelector(selectState, state => {
|
||||
export const selectPasswordResetError = createSelector(selectState, (state) => {
|
||||
const error = state.passwordResetError;
|
||||
return typeof error === 'object' && error !== null ? error.message : error;
|
||||
});
|
||||
|
||||
export const selectPasswordSetIsPending = createSelector(selectState, state => state.passwordSetPending);
|
||||
export const selectPasswordSetIsPending = createSelector(selectState, (state) => state.passwordSetPending);
|
||||
|
||||
export const selectPasswordSetSuccess = createSelector(selectState, state => state.passwordSetSuccess);
|
||||
export const selectPasswordSetSuccess = createSelector(selectState, (state) => state.passwordSetSuccess);
|
||||
|
||||
export const selectPasswordSetError = createSelector(selectState, state => {
|
||||
export const selectPasswordSetError = createSelector(selectState, (state) => {
|
||||
const error = state.passwordSetError;
|
||||
return typeof error === 'object' && error !== null ? error.message : error;
|
||||
});
|
||||
|
||||
export const selectPhoneNewErrorMessage = createSelector(selectState, state => state.phoneNewErrorMessage);
|
||||
export const selectPhoneNewErrorMessage = createSelector(selectState, (state) => state.phoneNewErrorMessage);
|
||||
|
||||
export const selectEmailVerifyIsPending = createSelector(selectState, state => state.emailVerifyIsPending);
|
||||
export const selectEmailVerifyIsPending = createSelector(selectState, (state) => state.emailVerifyIsPending);
|
||||
|
||||
export const selectEmailVerifyErrorMessage = createSelector(selectState, state => state.emailVerifyErrorMessage);
|
||||
export const selectEmailVerifyErrorMessage = createSelector(selectState, (state) => state.emailVerifyErrorMessage);
|
||||
|
||||
export const selectPhoneNewIsPending = createSelector(selectState, state => state.phoneNewIsPending);
|
||||
export const selectPhoneNewIsPending = createSelector(selectState, (state) => state.phoneNewIsPending);
|
||||
|
||||
export const selectPhoneVerifyIsPending = createSelector(selectState, state => state.phoneVerifyIsPending);
|
||||
export const selectPhoneVerifyIsPending = createSelector(selectState, (state) => state.phoneVerifyIsPending);
|
||||
|
||||
export const selectPhoneVerifyErrorMessage = createSelector(selectState, state => state.phoneVerifyErrorMessage);
|
||||
export const selectPhoneVerifyErrorMessage = createSelector(selectState, (state) => state.phoneVerifyErrorMessage);
|
||||
|
||||
export const selectIdentityVerifyIsPending = createSelector(selectState, state => state.identityVerifyIsPending);
|
||||
export const selectIdentityVerifyIsPending = createSelector(selectState, (state) => state.identityVerifyIsPending);
|
||||
|
||||
export const selectIdentityVerifyErrorMessage = createSelector(selectState, state => state.identityVerifyErrorMessage);
|
||||
export const selectIdentityVerifyErrorMessage = createSelector(
|
||||
selectState,
|
||||
(state) => state.identityVerifyErrorMessage
|
||||
);
|
||||
|
||||
export const selectUserVerifiedEmail = createSelector(selectUser, user => user && user.has_verified_email);
|
||||
export const selectUserVerifiedEmail = createSelector(selectUser, (user) => user && user.has_verified_email);
|
||||
|
||||
export const selectUserIsVerificationCandidate = createSelector(
|
||||
selectUser,
|
||||
user => user && (!user.has_verified_email || !user.is_identity_verified)
|
||||
(user) => user && (!user.has_verified_email || !user.is_identity_verified)
|
||||
);
|
||||
|
||||
export const selectAccessToken = createSelector(selectState, state => state.accessToken);
|
||||
export const selectAccessToken = createSelector(selectState, (state) => state.accessToken);
|
||||
|
||||
export const selectUserInviteStatusIsPending = createSelector(selectState, state => state.inviteStatusIsPending);
|
||||
export const selectUserInviteStatusIsPending = createSelector(selectState, (state) => state.inviteStatusIsPending);
|
||||
|
||||
export const selectUserInvitesRemaining = createSelector(selectState, state => state.invitesRemaining);
|
||||
export const selectUserInvitesRemaining = createSelector(selectState, (state) => state.invitesRemaining);
|
||||
|
||||
export const selectUserInvitees = createSelector(selectState, state => state.invitees);
|
||||
export const selectUserInvitees = createSelector(selectState, (state) => state.invitees);
|
||||
|
||||
export const selectUserInviteStatusFailed = createSelector(
|
||||
selectUserInvitesRemaining,
|
||||
() => selectUserInvitesRemaining === null
|
||||
);
|
||||
|
||||
export const selectUserInviteNewIsPending = createSelector(selectState, state => state.inviteNewIsPending);
|
||||
export const selectUserInviteNewIsPending = createSelector(selectState, (state) => state.inviteNewIsPending);
|
||||
|
||||
export const selectUserInviteNewErrorMessage = createSelector(selectState, state => state.inviteNewErrorMessage);
|
||||
export const selectUserInviteNewErrorMessage = createSelector(selectState, (state) => state.inviteNewErrorMessage);
|
||||
|
||||
export const selectUserInviteReferralLink = createSelector(selectState, state => state.referralLink);
|
||||
export const selectUserInviteReferralLink = createSelector(selectState, (state) => state.referralLink);
|
||||
|
||||
export const selectUserInviteReferralCode = createSelector(selectState, state =>
|
||||
export const selectUserInviteReferralCode = createSelector(selectState, (state) =>
|
||||
state.referralCode ? state.referralCode[0] : ''
|
||||
);
|
||||
|
||||
export const selectYouTubeImportPending = createSelector(selectState, state => state.youtubeChannelImportPending);
|
||||
export const selectYouTubeImportPending = createSelector(selectState, (state) => state.youtubeChannelImportPending);
|
||||
|
||||
export const selectYouTubeImportError = createSelector(selectState, state => state.youtubeChannelImportErrorMessage);
|
||||
export const selectYouTubeImportError = createSelector(selectState, (state) => state.youtubeChannelImportErrorMessage);
|
||||
|
||||
export const selectSetReferrerPending = createSelector(selectState, state => state.referrerSetIsPending);
|
||||
export const selectSetReferrerPending = createSelector(selectState, (state) => state.referrerSetIsPending);
|
||||
|
||||
export const selectSetReferrerError = createSelector(selectState, state => state.referrerSetError);
|
||||
export const selectSetReferrerError = createSelector(selectState, (state) => state.referrerSetError);
|
||||
|
||||
export const selectYouTubeImportVideosComplete = createSelector(selectState, state => {
|
||||
export const selectYouTubeImportVideosComplete = createSelector(selectState, (state) => {
|
||||
const total = state.youtubeChannelImportTotal;
|
||||
const complete = state.youtubeChannelImportComplete || 0;
|
||||
|
||||
|
@ -127,3 +133,5 @@ export const selectYouTubeImportVideosComplete = createSelector(selectState, sta
|
|||
return [complete, total];
|
||||
}
|
||||
});
|
||||
|
||||
export const makeSelectUserPropForProp = (prop) => createSelector(selectUser, (user) => (user ? user[prop] : null));
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
@import 'component/card';
|
||||
@import 'component/channel';
|
||||
@import 'component/claim-list';
|
||||
@import 'component/collection';
|
||||
@import 'component/comments';
|
||||
@import 'component/content';
|
||||
@import 'component/dat-gui';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue