[Glitch] Add more actions to collections notifications & context menus

Port 543db6d24c8764fe3975a138c777ffb1c28ae29e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion 2026-04-15 20:03:48 +02:00 committed by Claire
parent e0a34e6d89
commit 87c66c8b96
4 changed files with 147 additions and 39 deletions

View File

@ -4,6 +4,8 @@ import { defineMessages, useIntl } from 'react-intl';
import { matchPath } from 'react-router'; import { matchPath } from 'react-router';
import { showAlert } from '@/flavours/glitch/actions/alerts';
import { initBlockModal } from '@/flavours/glitch/actions/blocks';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react'; import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
@ -21,6 +23,18 @@ const messages = defineMessages({
id: 'collections.view_collection', id: 'collections.view_collection',
defaultMessage: '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: { viewOtherCollections: {
id: 'collections.view_other_collections_by_user', id: 'collections.view_other_collections_by_user',
defaultMessage: 'View other collections by this user', defaultMessage: 'View other collections by this user',
@ -33,6 +47,10 @@ const messages = defineMessages({
id: 'collections.report_collection', id: 'collections.report_collection',
defaultMessage: 'Report this collection', defaultMessage: 'Report this collection',
}, },
blockOwner: {
id: 'collections.block_collection_owner',
defaultMessage: 'Block account',
},
revoke: { revoke: {
id: 'collections.revoke_collection_inclusion', id: 'collections.revoke_collection_inclusion',
defaultMessage: 'Remove myself from this collection', defaultMessage: 'Remove myself from this collection',
@ -42,15 +60,29 @@ const messages = defineMessages({
export const CollectionMenu: React.FC<{ export const CollectionMenu: React.FC<{
collection: ApiCollectionJSON; collection: ApiCollectionJSON;
context: 'list' | 'collection'; context: 'list' | 'notifications' | 'collection';
className?: string; className?: string;
}> = ({ collection, context, className }) => { }> = ({ collection, context, className }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const { id, name, account_id } = collection; const { id, name, account_id, items } = collection;
const isOwnCollection = account_id === me;
const ownerAccount = useAccount(account_id); 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(() => { const openDeleteConfirmation = useCallback(() => {
dispatch( dispatch(
@ -75,9 +107,9 @@ export const CollectionMenu: React.FC<{
); );
}, [collection, dispatch]); }, [collection, dispatch]);
const currentAccountInCollection = collection.items.find( const openBlockModal = useCallback(() => {
(item) => item.account_id === me, dispatch(initBlockModal(ownerAccount));
); }, [ownerAccount, dispatch]);
const openRevokeConfirmation = useCallback(() => { const openRevokeConfirmation = useCallback(() => {
void dispatch( void dispatch(
@ -92,8 +124,28 @@ export const CollectionMenu: React.FC<{
}, [collection.id, currentAccountInCollection?.id, dispatch]); }, [collection.id, currentAccountInCollection?.id, dispatch]);
const menu = useMemo(() => { 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) { if (isOwnCollection) {
const commonItems: MenuItem[] = [ const ownerItems: MenuItem[] = [
...shareItems,
null,
{ {
text: intl.formatMessage(editorMessages.manageAccounts), text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`, to: `/collections/${id}/edit`,
@ -111,18 +163,14 @@ export const CollectionMenu: React.FC<{
]; ];
if (context === 'list') { if (context === 'list') {
return [ return [viewCollectionItem, ...ownerItems];
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
null,
...commonItems,
];
} else { } else {
return commonItems; return ownerItems;
} }
} else { } else {
const items: MenuItem[] = []; const nonOwnerItems: MenuItem[] = [viewCollectionItem, ...shareItems];
if (ownerAccount) { if (context !== 'notifications' && ownerAccount) {
const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`; const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`;
// Don't show menu link to featured collections while on that very page // Don't show menu link to featured collections while on that very page
if ( if (
@ -131,42 +179,50 @@ export const CollectionMenu: React.FC<{
exact: true, exact: true,
}) })
) { ) {
items.push( nonOwnerItems.push({
...[ text: intl.formatMessage(messages.viewOtherCollections),
{ to: featuredCollectionsPath,
text: intl.formatMessage(messages.viewOtherCollections), });
to: featuredCollectionsPath,
},
null,
],
);
} }
} }
if (currentAccountInCollection) { nonOwnerItems.push(null);
items.push({
// Collection notifications already have a prominent 'Remove me' button
if (currentAccountInCollection && context !== 'notifications') {
nonOwnerItems.push({
text: intl.formatMessage(messages.revoke), text: intl.formatMessage(messages.revoke),
action: openRevokeConfirmation, action: openRevokeConfirmation,
}); });
} }
items.push({ nonOwnerItems.push({
text: intl.formatMessage(messages.report), text: intl.formatMessage(messages.report),
action: openReportModal, action: openReportModal,
}); });
return items; if (currentAccountInCollection) {
nonOwnerItems.push({
text: intl.formatMessage(messages.blockOwner),
action: openBlockModal,
});
}
return nonOwnerItems;
} }
}, [ }, [
isOwnCollection,
intl, intl,
id, id,
openShareModal,
isOwnCollection,
dispatch,
openDeleteConfirmation, openDeleteConfirmation,
context, context,
currentAccountInCollection,
openRevokeConfirmation,
ownerAccount, ownerAccount,
currentAccountInCollection,
openReportModal, openReportModal,
openBlockModal,
openRevokeConfirmation,
]); ]);
return ( return (

View File

@ -115,7 +115,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
); );
const isCurrentUserInCollection = !isOwnCollection && currentUserIndex > -1; const isCurrentUserInCollection = !isOwnCollection && currentUserIndex > -1;
const handleShare = useCallback(() => { const openShareModal = useCallback(() => {
dispatch( dispatch(
openModal({ openModal({
modalType: 'SHARE_COLLECTION', modalType: 'SHARE_COLLECTION',
@ -132,9 +132,9 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
if (isNewCollection) { if (isNewCollection) {
// Replace with current pathname to clear `newCollection` state // Replace with current pathname to clear `newCollection` state
history.replace(location.pathname); history.replace(location.pathname);
handleShare(); openShareModal();
} }
}, [history, handleShare, isNewCollection, location.pathname]); }, [history, openShareModal, isNewCollection, location.pathname]);
return ( return (
<header className={classes.header}> <header className={classes.header}>
@ -150,7 +150,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
icon='share-icon' icon='share-icon'
title={intl.formatMessage(messages.share)} title={intl.formatMessage(messages.share)}
className={classes.iconButton} className={classes.iconButton}
onClick={handleShare} onClick={openShareModal}
/> />
<CollectionMenu <CollectionMenu
context='collection' 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 classNames from 'classnames';
import { DisplayNameSimple } from '@/flavours/glitch/components/display_name/simple'; import { Button } from '@/flavours/glitch/components/button';
import { LinkedDisplayName } from '@/flavours/glitch/components/display_name';
import { Icon } from '@/flavours/glitch/components/icon'; import { Icon } from '@/flavours/glitch/components/icon';
import { CollectionMenu } from '@/flavours/glitch/features/collections/components/collection_menu';
import { CollectionPreviewCard } from '@/flavours/glitch/features/collections/components/collection_preview_card';
import { useConfirmRevoke } from '@/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?react'; import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?react';
import type { import type {
@ -11,7 +15,7 @@ import type {
NotificationGroupCollectionUpdate, NotificationGroupCollectionUpdate,
} from 'flavours/glitch/models/notification_group'; } from 'flavours/glitch/models/notification_group';
import { CollectionPreviewCard } from '../../collections/components/collection_preview_card'; import classes from './notification_collection.module.scss';
export const NotificationCollection: React.FC<{ export const NotificationCollection: React.FC<{
notification: notification:
@ -21,6 +25,7 @@ export const NotificationCollection: React.FC<{
}> = ({ notification, unread }) => { }> = ({ notification, unread }) => {
const { collection, type } = notification; const { collection, type } = notification;
const collectionCreatorAccount = useAccount(collection.account_id); const collectionCreatorAccount = useAccount(collection.account_id);
const confirmRevoke = useConfirmRevoke(collection);
return ( return (
<div <div
@ -43,7 +48,12 @@ export const NotificationCollection: React.FC<{
defaultMessage='{name} added you to a collection' defaultMessage='{name} added you to a collection'
values={{ values={{
name: ( 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' defaultMessage='{name} edited a collection youre in'
values={{ values={{
name: ( name: (
<DisplayNameSimple account={collectionCreatorAccount} /> <LinkedDisplayName
displayProps={{
variant: 'simple',
account: collectionCreatorAccount,
}}
/>
), ),
}} }}
/> />
@ -63,6 +78,26 @@ export const NotificationCollection: React.FC<{
</div> </div>
<CollectionPreviewCard collection={collection} /> <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>
</div> </div>
); );