Add more actions to collections notifications & context menus (#38698)

This commit is contained in:
diondiondion 2026-04-15 20:03:48 +02:00 committed by GitHub
parent 6b1e1899fd
commit 543db6d24c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 151 additions and 39 deletions

View File

@ -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 (

View File

@ -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 (
<header className={classes.header}>
@ -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}
/>
<CollectionMenu
context='collection'

View File

@ -0,0 +1,17 @@
.actions {
display: flex;
align-items: center;
}
.menuButton {
box-sizing: content-box;
margin-inline-start: auto;
padding: 4px;
border-radius: 4px;
border: 1px solid var(--color-border-primary);
svg {
width: 20px;
height: 20px;
}
}

View File

@ -2,8 +2,12 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { DisplayNameSimple } from '@/mastodon/components/display_name/simple';
import { Button } from '@/mastodon/components/button';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
import { Icon } from '@/mastodon/components/icon';
import { CollectionMenu } from '@/mastodon/features/collections/components/collection_menu';
import { CollectionPreviewCard } from '@/mastodon/features/collections/components/collection_preview_card';
import { useConfirmRevoke } from '@/mastodon/features/collections/detail/revoke_collection_inclusion_modal';
import { useAccount } from '@/mastodon/hooks/useAccount';
import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?react';
import type {
@ -11,7 +15,7 @@ import type {
NotificationGroupCollectionUpdate,
} from 'mastodon/models/notification_group';
import { CollectionPreviewCard } from '../../collections/components/collection_preview_card';
import classes from './notification_collection.module.scss';
export const NotificationCollection: React.FC<{
notification:
@ -21,6 +25,7 @@ export const NotificationCollection: React.FC<{
}> = ({ notification, unread }) => {
const { collection, type } = notification;
const collectionCreatorAccount = useAccount(collection.account_id);
const confirmRevoke = useConfirmRevoke(collection);
return (
<div
@ -43,7 +48,12 @@ export const NotificationCollection: React.FC<{
defaultMessage='{name} added you to a collection'
values={{
name: (
<DisplayNameSimple account={collectionCreatorAccount} />
<LinkedDisplayName
displayProps={{
variant: 'simple',
account: collectionCreatorAccount,
}}
/>
),
}}
/>
@ -54,7 +64,12 @@ export const NotificationCollection: React.FC<{
defaultMessage='{name} edited a collection youre in'
values={{
name: (
<DisplayNameSimple account={collectionCreatorAccount} />
<LinkedDisplayName
displayProps={{
variant: 'simple',
account: collectionCreatorAccount,
}}
/>
),
}}
/>
@ -63,6 +78,26 @@ export const NotificationCollection: React.FC<{
</div>
<CollectionPreviewCard collection={collection} />
<div className={classes.actions}>
<Button
compact
secondary
className='button--destructive'
onClick={confirmRevoke}
>
<FormattedMessage
id='collections.detail.revoke_inclusion'
defaultMessage='Remove me'
/>
</Button>
<CollectionMenu
context='notifications'
collection={collection}
className={classes.menuButton}
/>
</div>
</div>
</div>
);

View File

@ -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 dont appear on your profile to others. Anyone with the link can discover them.",