Compare commits

...

3 commits

Author SHA1 Message Date
saltrafael
bd1b2df5b7
Add Copy List option on Popups and Collection Content Sidebar 2021-08-24 09:54:41 -03:00
saltrafael
34913e33fc
Add Copy List functionality 2021-08-24 09:45:32 -03:00
saltrafael
975b6a3b01
Add Copy List button on Collection page 2021-08-24 09:45:16 -03:00
12 changed files with 280 additions and 87 deletions

View file

@ -2,22 +2,44 @@ import { connect } from 'react-redux';
import ClaimCollectionAdd from './view';
import { withRouter } from 'react-router';
import {
doResolveUris,
makeSelectClaimForUri,
doLocalCollectionCreate,
selectBuiltinCollections,
selectMyPublishedCollections,
selectMyUnpublishedCollections,
doCollectionEdit,
makeSelectUrlsForCollectionId,
} from 'lbry-redux';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
builtin: selectBuiltinCollections(state),
published: selectMyPublishedCollections(state),
unpublished: selectMyUnpublishedCollections(state),
});
const select = (state, props) => {
const collectionId = props.collectionId;
let items;
let itemsClaims = [];
if (collectionId) {
items = makeSelectUrlsForCollectionId(collectionId)(state);
if (items) {
items.map((uri) => {
itemsClaims.push(makeSelectClaimForUri(uri)(state));
});
}
}
return {
claim: makeSelectClaimForUri(props.uri)(state),
builtin: selectBuiltinCollections(state),
published: selectMyPublishedCollections(state),
unpublished: selectMyUnpublishedCollections(state),
items,
itemsClaims,
};
};
const perform = (dispatch) => ({
addCollection: (name, items, type) => dispatch(doLocalCollectionCreate(name, items, type)),
editCollection: (id, params) => dispatch(doCollectionEdit(id, params)),
doResolveUris: (uris) => dispatch(doResolveUris(uris)),
});
export default withRouter(connect(select, perform)(ClaimCollectionAdd));

View file

@ -13,16 +13,33 @@ type Props = {
published: any,
unpublished: any,
addCollection: (string, Array<string>, string) => void,
editCollection: (string, {}) => void,
closeModal: () => void,
uri: string,
collectionId: string,
doResolveUris: (Array<string>) => void,
items: Array<string>,
itemsClaims: Array<string>,
};
const ClaimCollectionAdd = (props: Props) => {
const { builtin, published, unpublished, addCollection, claim, closeModal, uri } = props;
const {
builtin,
published,
unpublished,
addCollection,
editCollection,
claim,
closeModal,
uri,
doResolveUris,
items,
itemsClaims,
} = props;
const buttonref: ElementRef<any> = React.useRef();
const permanentUrl = claim && claim.permanent_url;
const isChannel = claim && claim.value_type === 'channel';
const [selectedCollections, setSelectedCollections] = React.useState([]);
const [addNewCollection, setAddNewCollection] = React.useState(false);
const [newCollectionName, setNewCollectionName] = React.useState('');
@ -34,17 +51,32 @@ const ClaimCollectionAdd = (props: Props) => {
// claim.value.stream_type &&
// (claim.value.stream_type === 'audio' || claim.value.stream_type === 'video');
React.useEffect(() => {
if (items) {
doResolveUris(items);
}
}, [doResolveUris, items]);
function handleNameInput(e) {
const { value } = e.target;
setNewCollectionName(value);
}
function handleAddCollection() {
addCollection(newCollectionName, [permanentUrl], isChannel ? 'collection' : 'playlist');
addCollection(newCollectionName, items || [permanentUrl], isChannel ? 'collection' : 'playlist');
if (items) setSelectedCollections([...selectedCollections, newCollectionName]);
setNewCollectionName('');
setAddNewCollection(false);
}
function handleEditCollection() {
if (selectedCollections) {
selectedCollections.map((id) => {
editCollection(id, { claims: itemsClaims, remove: false });
});
}
}
function altEnterListener(e: SyntheticKeyboardEvent<*>) {
const KEYCODE_ENTER = 13;
if (e.keyCode === KEYCODE_ENTER) {
@ -71,7 +103,7 @@ const ClaimCollectionAdd = (props: Props) => {
title={__('Add To...')}
actions={
<div className="card__body">
{uri && (
{(uri || items) && (
<fieldset-section>
<div className={'card__body-scrollable'}>
{(Object.values(builtin): any)
@ -86,6 +118,9 @@ const ClaimCollectionAdd = (props: Props) => {
uri={permanentUrl}
key={id}
category={'builtin'}
setSelectedCollections={setSelectedCollections}
selectedCollections={selectedCollections}
items={items}
/>
);
})}
@ -102,6 +137,9 @@ const ClaimCollectionAdd = (props: Props) => {
uri={permanentUrl}
key={id}
category={'unpublished'}
setSelectedCollections={setSelectedCollections}
selectedCollections={selectedCollections}
items={items}
/>
);
})}
@ -116,6 +154,9 @@ const ClaimCollectionAdd = (props: Props) => {
uri={permanentUrl}
key={id}
category={'published'}
setSelectedCollections={setSelectedCollections}
selectedCollections={selectedCollections}
items={items}
/>
);
})}
@ -158,7 +199,17 @@ const ClaimCollectionAdd = (props: Props) => {
)}
</fieldset-section>
<div className="card__actions">
<Button button="secondary" label={__('Done')} disabled={addNewCollection} onClick={closeModal} />
<Button
button="secondary"
label={__('Done')}
disabled={addNewCollection}
onClick={() => {
if (items) {
handleEditCollection();
}
closeModal();
}}
/>
</div>
</div>
}

View file

@ -9,6 +9,8 @@ import {
COLLECTIONS_CONSTS,
makeSelectEditedCollectionForId,
makeSelectClaimIsMine,
doFetchItemsInCollection,
makeSelectUrlsForCollectionId,
} from 'lbry-redux';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { doChannelMute, doChannelUnmute } from 'redux/actions/blocked';
@ -33,6 +35,8 @@ import fs from 'fs';
const select = (state, props) => {
const claim = makeSelectClaimForUri(props.uri, false)(state);
const collectionId = props.collectionId;
const resolvedList = makeSelectUrlsForCollectionId(collectionId)(state);
const repostedClaim = claim && claim.reposted_claim;
const contentClaim = repostedClaim || claim;
const contentSigningChannel = contentClaim && contentClaim.signing_channel;
@ -60,10 +64,11 @@ const select = (state, props) => {
isSubscribed: makeSelectIsSubscribed(contentChannelUri, true)(state),
channelIsAdminBlocked: makeSelectChannelIsAdminBlocked(props.uri)(state),
isAdmin: selectHasAdminChannel(state),
claimInCollection: makeSelectCollectionForIdHasClaimUrl(props.collectionId, contentPermanentUri)(state),
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state),
claimInCollection: makeSelectCollectionForIdHasClaimUrl(collectionId, contentPermanentUri)(state),
isMyCollection: makeSelectCollectionIsMine(collectionId)(state),
editedCollection: makeSelectEditedCollectionForId(collectionId)(state),
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
resolvedList,
};
};
@ -90,6 +95,7 @@ const perform = (dispatch) => ({
doChannelSubscribe: (subscription) => dispatch(doChannelSubscribe(subscription)),
doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)),
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
fetchCollectionItems: (collectionId) => dispatch(doFetchItemsInCollection({ collectionId })),
});
export default connect(select, perform)(ClaimPreview);

View file

@ -56,6 +56,8 @@ type Props = {
isChannelPage: boolean,
editedCollection: Collection,
isAuthenticated: boolean,
fetchCollectionItems: (string) => void,
resolvedList: boolean,
};
function ClaimMenuList(props: Props) {
@ -93,6 +95,8 @@ function ClaimMenuList(props: Props) {
isChannelPage = false,
editedCollection,
isAuthenticated,
fetchCollectionItems,
resolvedList,
} = props;
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
const isChannel = !incognitoClaim && !contentSigningChannel;
@ -106,6 +110,12 @@ function ClaimMenuList(props: Props) {
? __('Unfollow')
: __('Follow');
const fetchItems = React.useCallback(() => {
if (collectionId) {
fetchCollectionItems(collectionId);
}
}, [collectionId, fetchCollectionItems]);
const { push, replace } = useHistory();
if (!claim) {
return null;
@ -251,6 +261,18 @@ function ClaimMenuList(props: Props) {
{__('View List')}
</div>
</MenuItem>
<MenuItem
className="comment__menu-option"
onSelect={() => {
fetchItems();
openModal(MODALS.COLLECTION_ADD, { collectionId });
}}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.COPY} />
{__('Copy List')}
</div>
</MenuItem>
{isMyCollection && (
<>
<MenuItem

View file

@ -15,7 +15,7 @@ import { ENABLE_FILE_REACTIONS } from 'config';
type Props = {
uri: string,
claim: StreamClaim,
openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void,
openModal: (id: string, {}) => void,
myChannels: ?Array<ChannelClaim>,
doToast: ({ message: string }) => void,
claimIsPending: boolean,
@ -24,6 +24,7 @@ type Props = {
showInfo: boolean,
setShowInfo: (boolean) => void,
collectionHasEdits: boolean,
isBuiltin: boolean,
};
function CollectionActions(props: Props) {
@ -37,6 +38,7 @@ function CollectionActions(props: Props) {
showInfo,
setShowInfo,
collectionHasEdits,
isBuiltin,
} = props;
const { push } = useHistory();
const isMobile = useIsMobile();
@ -44,54 +46,66 @@ function CollectionActions(props: Props) {
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 })}
/>
{!isBuiltin && (
<>
{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}`}
/>
)}
{!isBuiltin &&
(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}
/>
<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}
/>
</>
) : (
<Button
title={__('Report content')}
className="button--file-action"
icon={ICONS.REPORT}
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
/>
))}
<Button
className={classnames('button--file-action')}
label={__('Copy List')}
title={__('Copy List')}
onClick={() => openModal(MODALS.COLLECTION_ADD, { collectionId })}
icon={ICONS.COPY}
iconSize={18}
/>
</>
);

View file

@ -7,9 +7,8 @@ import {
makeSelectClaimForUri,
makeSelectClaimIsMine,
} from 'lbry-redux';
import {
selectPlayingUri,
} from 'redux/selectors/content';
import { selectPlayingUri } from 'redux/selectors/content';
import { doOpenModal } from 'redux/actions/app';
const select = (state, props) => {
const playingUri = selectPlayingUri(state);
@ -26,4 +25,6 @@ const select = (state, props) => {
};
};
export default connect(select)(CollectionContent);
export default connect(select, {
doOpenModal,
})(CollectionContent);

View file

@ -4,6 +4,7 @@ import ClaimList from 'component/claimList';
import Card from 'component/common/card';
import Button from 'component/button';
import * as PAGES from 'constants/pages';
import * as MODALS from 'constants/modal_types';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
import { COLLECTIONS_CONSTS } from 'lbry-redux';
@ -16,28 +17,52 @@ type Props = {
collectionName: string,
collection: any,
createUnpublishedCollection: (string, Array<any>, ?string) => void,
doOpenModal: (id: string, { collectionId: string }) => void,
};
export default function CollectionContent(props: Props) {
const { collectionUrls, collectionName, id, url } = props;
const {
collectionUrls,
collectionName,
id,
url,
doOpenModal,
} = props;
return (
<Card
isBodyList
className="file-page__recommended"
title={
<span>
<Icon
icon={(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
className="icon--margin-right" />
{collectionName}
</span>
<>
<span className="file-page__recommended-collection__row">
<Icon
icon={
(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
ICONS.STACK
}
className="icon--margin-right"
/>
{collectionName}
</span>
</>
}
titleActions={
<div className="card__title-actions--link">
{/* TODO: BUTTON TO SAVE COLLECTION - Probably save/copy modal */}
<Button label={'View List'} button="link" navigate={`/$/${PAGES.LIST}/${id}`} />
</div>
<>
<div className="card__title-actions--link">
<Button label={'View List'} button="link" navigate={`/$/${PAGES.LIST}/${id}`} />
</div>
<span>
<Button
button="alt"
title={__('Copy')}
icon={ICONS.COPY}
className="button--file-action--right"
onClick={() => doOpenModal(MODALS.COLLECTION_ADD, { collectionId: id })}
/>
</span>
</>
}
body={
<ClaimList

View file

@ -40,6 +40,15 @@ function CollectionMenuList(props: Props) {
{__('View List')}
</div>
</MenuItem>
<MenuItem
className="comment__menu-option"
onSelect={() => doOpenModal(MODALS.COLLECTION_ADD, { collectionId })}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.COPY} />
{__('Copy List')}
</div>
</MenuItem>
<MenuItem
className="comment__menu-option"
onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)}

View file

@ -13,19 +13,48 @@ type Props = {
editCollection: (string, CollectionEditParams) => void,
claim: Claim,
collectionPending: Collection,
setSelectedCollections: (any) => void,
selectedCollections: Array<string>,
items: Array<string>,
};
function CollectionSelectItem(props: Props) {
const { collection, hasClaim, category, editCollection, claim, collectionPending } = props;
const {
collection,
hasClaim,
category,
editCollection,
claim,
collectionPending,
setSelectedCollections,
selectedCollections,
items,
} = props;
const { name, id } = collection;
const handleChange = (e) => {
editCollection(id, { claims: [claim], remove: hasClaim });
const justAdded = selectedCollections.includes(name);
const [active, setActive] = React.useState(justAdded);
const handleChange = () => {
if (items) {
if (active) {
selectedCollections.splice(selectedCollections.indexOf(id), 1);
setSelectedCollections([...selectedCollections]);
} else {
setSelectedCollections([...selectedCollections, id]);
}
setActive(!active);
} else {
editCollection(id, { claims: [claim], remove: hasClaim });
}
};
let icon;
switch (category) {
case 'builtin':
icon = (id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) || (id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK;
icon =
(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
ICONS.STACK;
break;
case 'published':
icon = ICONS.STACK;
@ -39,12 +68,12 @@ function CollectionSelectItem(props: Props) {
return (
<div className={'collection-select__item'}>
<FormField
checked={hasClaim}
checked={items ? active : hasClaim}
disabled={collectionPending}
icon={icon}
type="checkbox"
name={`select-${id}`}
onChange={handleChange} // edit the collection
onChange={!justAdded && handleChange} // edit the collection
label={
<span>
<Icon icon={icon} className={'icon-collection-select'} />

View file

@ -6,13 +6,14 @@ import { Modal } from 'modal/modal';
type Props = {
doHideModal: () => void,
uri: string,
collectionId: string,
};
const ModalClaimCollectionAdd = (props: Props) => {
const { doHideModal, uri } = props;
const { doHideModal, uri, collectionId } = props;
return (
<Modal isOpen type="card" onAborted={doHideModal}>
<ClaimCollectionAdd uri={uri} closeModal={doHideModal} />
<ClaimCollectionAdd uri={uri} closeModal={doHideModal} collectionId={collectionId} />
</Modal>
);
};

View file

@ -112,18 +112,26 @@ export default function CollectionPage(props: Props) {
title={
<span>
<Icon
icon={(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
className="icon--margin-right" />
icon={
(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
ICONS.STACK
}
className="icon--margin-right"
/>
{claim ? claim.value.title || claim.name : collection && collection.name}
</span>
}
titleActions={titleActions}
subtitle={subTitle}
body={
!isBuiltin && (
<CollectionActions uri={uri} collectionId={collectionId} setShowInfo={setShowInfo} showInfo={showInfo} />
)
<CollectionActions
uri={uri}
collectionId={collectionId}
setShowInfo={setShowInfo}
showInfo={showInfo}
isBuiltin={isBuiltin}
/>
}
actions={
showInfo &&

View file

@ -240,6 +240,11 @@
}
}
.button--file-action--right {
@extend .button--file-action;
float: right;
}
.button--fire {
color: var(--color-fire);
position: relative;