Add more actions to collections notifications & context menus (#38698)
This commit is contained in:
parent
6b1e1899fd
commit
543db6d24c
@ -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 (
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 you’re 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>
|
||||
);
|
||||
|
||||
@ -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.",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user