diff --git a/app/javascript/mastodon/features/collections/components/collection_menu.tsx b/app/javascript/mastodon/features/collections/components/collection_menu.tsx index 90c6315cbb..1cbb7279e9 100644 --- a/app/javascript/mastodon/features/collections/components/collection_menu.tsx +++ b/app/javascript/mastodon/features/collections/components/collection_menu.tsx @@ -4,6 +4,8 @@ import { defineMessages, useIntl } from 'react-intl'; import { matchPath } from 'react-router'; +import { showAlert } from '@/mastodon/actions/alerts'; +import { initBlockModal } from '@/mastodon/actions/blocks'; import { useAccount } from '@/mastodon/hooks/useAccount'; import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react'; import { openModal } from 'mastodon/actions/modal'; @@ -21,6 +23,18 @@ const messages = defineMessages({ id: 'collections.view_collection', defaultMessage: 'View collection', }, + share: { + id: 'collections.share_short', + defaultMessage: 'Share', + }, + copyLink: { + id: 'collections.copy_link', + defaultMessage: 'Copy link', + }, + copyLinkConfirmation: { + id: 'collections.copy_link_confirmation', + defaultMessage: 'Copied collection link to clipboard', + }, viewOtherCollections: { id: 'collections.view_other_collections_by_user', defaultMessage: 'View other collections by this user', @@ -33,6 +47,10 @@ const messages = defineMessages({ id: 'collections.report_collection', defaultMessage: 'Report this collection', }, + blockOwner: { + id: 'collections.block_collection_owner', + defaultMessage: 'Block account', + }, revoke: { id: 'collections.revoke_collection_inclusion', defaultMessage: 'Remove myself from this collection', @@ -42,15 +60,29 @@ const messages = defineMessages({ export const CollectionMenu: React.FC<{ collection: ApiCollectionJSON; - context: 'list' | 'collection'; + context: 'list' | 'notifications' | 'collection'; className?: string; }> = ({ collection, context, className }) => { const dispatch = useAppDispatch(); const intl = useIntl(); - const { id, name, account_id } = collection; - const isOwnCollection = account_id === me; + const { id, name, account_id, items } = collection; const ownerAccount = useAccount(account_id); + const isOwnCollection = account_id === me; + const currentAccountInCollection = items.find( + (item) => item.account_id === me, + ); + + const openShareModal = useCallback(() => { + dispatch( + openModal({ + modalType: 'SHARE_COLLECTION', + modalProps: { + collection, + }, + }), + ); + }, [collection, dispatch]); const openDeleteConfirmation = useCallback(() => { dispatch( @@ -75,9 +107,9 @@ export const CollectionMenu: React.FC<{ ); }, [collection, dispatch]); - const currentAccountInCollection = collection.items.find( - (item) => item.account_id === me, - ); + const openBlockModal = useCallback(() => { + dispatch(initBlockModal(ownerAccount)); + }, [ownerAccount, dispatch]); const openRevokeConfirmation = useCallback(() => { void dispatch( @@ -92,8 +124,28 @@ export const CollectionMenu: React.FC<{ }, [collection.id, currentAccountInCollection?.id, dispatch]); const menu = useMemo(() => { + const viewCollectionItem: MenuItem = { + text: intl.formatMessage(messages.view), + to: `/collections/${id}`, + }; + const shareItems: MenuItem[] = [ + { + text: intl.formatMessage(messages.share), + action: openShareModal, + }, + { + text: intl.formatMessage(messages.copyLink), + action: () => { + void navigator.clipboard.writeText(`/collections/${id}`); + dispatch(showAlert({ message: messages.copyLinkConfirmation })); + }, + }, + ]; + if (isOwnCollection) { - const commonItems: MenuItem[] = [ + const ownerItems: MenuItem[] = [ + ...shareItems, + null, { text: intl.formatMessage(editorMessages.manageAccounts), to: `/collections/${id}/edit`, @@ -111,18 +163,14 @@ export const CollectionMenu: React.FC<{ ]; if (context === 'list') { - return [ - { text: intl.formatMessage(messages.view), to: `/collections/${id}` }, - null, - ...commonItems, - ]; + return [viewCollectionItem, ...ownerItems]; } else { - return commonItems; + return ownerItems; } } else { - const items: MenuItem[] = []; + const nonOwnerItems: MenuItem[] = [viewCollectionItem, ...shareItems]; - if (ownerAccount) { + if (context !== 'notifications' && ownerAccount) { const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`; // Don't show menu link to featured collections while on that very page if ( @@ -131,42 +179,50 @@ export const CollectionMenu: React.FC<{ exact: true, }) ) { - items.push( - ...[ - { - text: intl.formatMessage(messages.viewOtherCollections), - to: featuredCollectionsPath, - }, - null, - ], - ); + nonOwnerItems.push({ + text: intl.formatMessage(messages.viewOtherCollections), + to: featuredCollectionsPath, + }); } } - if (currentAccountInCollection) { - items.push({ + nonOwnerItems.push(null); + + // Collection notifications already have a prominent 'Remove me' button + if (currentAccountInCollection && context !== 'notifications') { + nonOwnerItems.push({ text: intl.formatMessage(messages.revoke), action: openRevokeConfirmation, }); } - items.push({ + nonOwnerItems.push({ text: intl.formatMessage(messages.report), action: openReportModal, }); - return items; + if (currentAccountInCollection) { + nonOwnerItems.push({ + text: intl.formatMessage(messages.blockOwner), + action: openBlockModal, + }); + } + + return nonOwnerItems; } }, [ - isOwnCollection, intl, id, + openShareModal, + isOwnCollection, + dispatch, openDeleteConfirmation, context, - currentAccountInCollection, - openRevokeConfirmation, ownerAccount, + currentAccountInCollection, openReportModal, + openBlockModal, + openRevokeConfirmation, ]); return ( diff --git a/app/javascript/mastodon/features/collections/detail/index.tsx b/app/javascript/mastodon/features/collections/detail/index.tsx index 404cd8a074..d3fbd4c2cb 100644 --- a/app/javascript/mastodon/features/collections/detail/index.tsx +++ b/app/javascript/mastodon/features/collections/detail/index.tsx @@ -115,7 +115,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({ ); const isCurrentUserInCollection = !isOwnCollection && currentUserIndex > -1; - const handleShare = useCallback(() => { + const openShareModal = useCallback(() => { dispatch( openModal({ modalType: 'SHARE_COLLECTION', @@ -132,9 +132,9 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({ if (isNewCollection) { // Replace with current pathname to clear `newCollection` state history.replace(location.pathname); - handleShare(); + openShareModal(); } - }, [history, handleShare, isNewCollection, location.pathname]); + }, [history, openShareModal, isNewCollection, location.pathname]); return (
@@ -150,7 +150,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({ icon='share-icon' title={intl.formatMessage(messages.share)} className={classes.iconButton} - onClick={handleShare} + onClick={openShareModal} /> = ({ notification, unread }) => { const { collection, type } = notification; const collectionCreatorAccount = useAccount(collection.account_id); + const confirmRevoke = useConfirmRevoke(collection); return (
+ ), }} /> @@ -54,7 +64,12 @@ export const NotificationCollection: React.FC<{ defaultMessage='{name} edited a collection you’re in' values={{ name: ( - + ), }} /> @@ -63,6 +78,26 @@ export const NotificationCollection: React.FC<{
+ +
+ + + +
); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 8b05678fc7..3e2ce2c696 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -361,6 +361,7 @@ "collections.account_count": "{count, plural, one {# account} other {# accounts}}", "collections.accounts.empty_description": "Add up to {count} accounts you follow", "collections.accounts.empty_title": "This collection is empty", + "collections.block_collection_owner": "Block account", "collections.by_account": "by {account_handle}", "collections.collection_description": "Description", "collections.collection_language": "Language", @@ -370,6 +371,8 @@ "collections.confirm_account_removal": "Are you sure you want to remove this account from this collection?", "collections.content_warning": "Content warning", "collections.continue": "Continue", + "collections.copy_link": "Copy link", + "collections.copy_link_confirmation": "Copied collection link to clipboard", "collections.create.accounts_subtitle": "Only accounts you follow who have opted into discovery can be added.", "collections.create.accounts_title": "Who will you feature in this collection?", "collections.create.basic_details_title": "Basic details", @@ -407,6 +410,7 @@ "collections.search_accounts_label": "Search for accounts to add…", "collections.search_accounts_max_reached": "You have added the maximum number of accounts", "collections.sensitive": "Sensitive", + "collections.share_short": "Share", "collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.", "collections.topic_special_chars_hint": "Special characters will be removed when saving", "collections.unlisted_collections_description": "These don’t appear on your profile to others. Anyone with the link can discover them.",