import { useMemo } from 'react'; import type { FC } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { blockAccount, followAccount, pinAccount, unblockAccount, unmuteAccount, unpinAccount, } from '@/flavours/glitch/actions/accounts'; import { removeAccountFromFollowers } from '@/flavours/glitch/actions/accounts_typed'; import { showAlert } from '@/flavours/glitch/actions/alerts'; import { directCompose, mentionCompose, } from '@/flavours/glitch/actions/compose'; import { initDomainBlockModal, unblockDomain, } from '@/flavours/glitch/actions/domain_blocks'; import { openModal } from '@/flavours/glitch/actions/modal'; import { initMuteModal } from '@/flavours/glitch/actions/mutes'; import { initReport } from '@/flavours/glitch/actions/reports'; import { Dropdown } from '@/flavours/glitch/components/dropdown_menu'; import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useIdentity } from '@/flavours/glitch/identity_context'; import type { Account } from '@/flavours/glitch/models/account'; import type { MenuItem } from '@/flavours/glitch/models/dropdown_menu'; import type { Relationship } from '@/flavours/glitch/models/relationship'; import { PERMISSION_MANAGE_FEDERATION, PERMISSION_MANAGE_USERS, } from '@/flavours/glitch/permissions'; import type { AppDispatch } from '@/flavours/glitch/store'; import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; import BlockIcon from '@/material-icons/400-24px/block.svg?react'; import LinkIcon from '@/material-icons/400-24px/link_2.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'; import ReportIcon from '@/material-icons/400-24px/report.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import { isRedesignEnabled } from '../common'; import classes from './redesign.module.scss'; export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { const intl = useIntl(); const { signedIn, permissions } = useIdentity(); const account = useAccount(accountId); const relationship = useAppSelector((state) => state.relationships.get(accountId), ); const currentAccountId = useAppSelector( (state) => state.meta.get('me') as string, ); const isMe = currentAccountId === accountId; const dispatch = useAppDispatch(); const menuItems = useMemo(() => { if (!account) { return []; } if (isRedesignEnabled()) { return redesignMenuItems({ account, signedIn: !isMe && signedIn, permissions, intl, relationship, dispatch, }); } return currentMenuItems({ account, signedIn, permissions, intl, relationship, dispatch, }); }, [account, signedIn, isMe, permissions, intl, relationship, dispatch]); return ( ); }; interface MenuItemsParams { account: Account; signedIn: boolean; permissions: number; intl: ReturnType; relationship?: Relationship; dispatch: AppDispatch; } const messages = defineMessages({ unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, report: { id: 'account.report', defaultMessage: 'Report @{name}' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}', }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}', }, hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}', }, showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}', }, addNote: { id: 'account.add_note', defaultMessage: 'Add a personal note', }, editNote: { id: 'account.edit_note', defaultMessage: 'Edit personal note', }, endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: "Don't feature on profile", }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists', }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}', }, admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}', }, languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages', }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page', }, removeFromFollowers: { id: 'account.remove_from_followers', defaultMessage: 'Remove {name} from followers', }, confirmRemoveFromFollowersTitle: { id: 'confirmations.remove_from_followers.title', defaultMessage: 'Remove follower?', }, confirmRemoveFromFollowersMessage: { id: 'confirmations.remove_from_followers.message', defaultMessage: '{name} will stop following you. Are you sure you want to proceed?', }, confirmRemoveFromFollowersButton: { id: 'confirmations.remove_from_followers.confirm', defaultMessage: 'Remove follower', }, }); function currentMenuItems({ account, signedIn, permissions, intl, relationship, dispatch, }: MenuItemsParams): MenuItem[] { const items: MenuItem[] = []; const isRemote = account.acct !== account.username; if (signedIn && !account.suspended) { items.push( { text: intl.formatMessage(messages.mention, { name: account.username, }), action: () => { dispatch(mentionCompose(account)); }, }, { text: intl.formatMessage(messages.direct, { name: account.username, }), action: () => { dispatch(directCompose(account)); }, }, null, ); } if (isRemote) { items.push( { text: intl.formatMessage(messages.openOriginalPage), href: account.url, }, null, ); } if (!signedIn) { return items; } if (relationship?.following) { // Timeline options if (!relationship.muting) { if (relationship.showing_reblogs) { items.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.username, }), action: () => { dispatch(followAccount(account.id, { reblogs: false })); }, }); } else { items.push({ text: intl.formatMessage(messages.showReblogs, { name: account.username, }), action: () => { dispatch(followAccount(account.id, { reblogs: true })); }, }); } items.push( { text: intl.formatMessage(messages.languages), action: () => { dispatch( openModal({ modalType: 'SUBSCRIBED_LANGUAGES', modalProps: { accountId: account.id, }, }), ); }, }, null, ); } items.push( { text: intl.formatMessage( relationship.endorsed ? messages.unendorse : messages.endorse, ), action: () => { if (relationship.endorsed) { dispatch(unpinAccount(account.id)); } else { dispatch(pinAccount(account.id)); } }, }, { text: intl.formatMessage(messages.add_or_remove_from_list), action: () => { dispatch( openModal({ modalType: 'LIST_ADDER', modalProps: { accountId: account.id, }, }), ); }, }, null, ); } if (relationship?.followed_by) { const handleRemoveFromFollowers = () => { dispatch( openModal({ modalType: 'CONFIRM', modalProps: { title: intl.formatMessage(messages.confirmRemoveFromFollowersTitle), message: intl.formatMessage( messages.confirmRemoveFromFollowersMessage, { name: {account.acct} }, ), confirm: intl.formatMessage( messages.confirmRemoveFromFollowersButton, ), onConfirm: () => { void dispatch( removeAccountFromFollowers({ accountId: account.id }), ); }, }, }), ); }; items.push({ text: intl.formatMessage(messages.removeFromFollowers, { name: account.username, }), action: handleRemoveFromFollowers, dangerous: true, }); } if (relationship?.muting) { items.push({ text: intl.formatMessage(messages.unmute, { name: account.username, }), action: () => { dispatch(unmuteAccount(account.id)); }, }); } else { items.push({ text: intl.formatMessage(messages.mute, { name: account.username, }), action: () => { dispatch(initMuteModal(account)); }, dangerous: true, }); } if (relationship?.blocking) { items.push({ text: intl.formatMessage(messages.unblock, { name: account.username, }), action: () => { dispatch(unblockAccount(account.id)); }, }); } else { items.push({ text: intl.formatMessage(messages.block, { name: account.username, }), action: () => { dispatch(blockAccount(account.id)); }, dangerous: true, }); } if (!account.suspended) { items.push({ text: intl.formatMessage(messages.report, { name: account.username, }), action: () => { dispatch(initReport(account)); }, dangerous: true, }); } const remoteDomain = isRemote ? account.acct.split('@')[1] : null; if (remoteDomain) { items.push(null); if (relationship?.domain_blocking) { items.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain, }), action: () => { dispatch(unblockDomain(remoteDomain)); }, }); } else { items.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain, }), action: () => { dispatch(initDomainBlockModal(account)); }, dangerous: true, }); } } if ( (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) ) { items.push(null); if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { items.push({ text: intl.formatMessage(messages.admin_account, { name: account.username, }), href: `/admin/accounts/${account.id}`, }); } if ( isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION ) { items.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain, }), href: `/admin/instances/${remoteDomain}`, }); } } return items; } const redesignMessages = defineMessages({ share: { id: 'account.menu.share', defaultMessage: 'Share…' }, copy: { id: 'account.menu.copy', defaultMessage: 'Copy link' }, copied: { id: 'account.menu.copied', defaultMessage: 'Copied account link to clipboard', }, mention: { id: 'account.menu.mention', defaultMessage: 'Mention' }, noteDescription: { id: 'account.menu.note.description', defaultMessage: 'Visible only to you', }, direct: { id: 'account.menu.direct', defaultMessage: 'Privately mention', }, mute: { id: 'account.menu.mute', defaultMessage: 'Mute account' }, unmute: { id: 'account.menu.unmute', defaultMessage: 'Unmute account', }, block: { id: 'account.menu.block', defaultMessage: 'Block account' }, unblock: { id: 'account.menu.unblock', defaultMessage: 'Unblock account', }, domainBlock: { id: 'account.menu.block_domain', defaultMessage: 'Block {domain}', }, domainUnblock: { id: 'account.menu.unblock_domain', defaultMessage: 'Unblock {domain}', }, report: { id: 'account.menu.report', defaultMessage: 'Report account' }, hideReblogs: { id: 'account.menu.hide_reblogs', defaultMessage: 'Hide boosts in timeline', }, showReblogs: { id: 'account.menu.show_reblogs', defaultMessage: 'Show boosts in timeline', }, addToList: { id: 'account.menu.add_to_list', defaultMessage: 'Add to list…', }, openOriginalPage: { id: 'account.menu.open_original_page', defaultMessage: 'View on {domain}', }, removeFollower: { id: 'account.menu.remove_follower', defaultMessage: 'Remove follower', }, }); function redesignMenuItems({ account, signedIn, permissions, intl, relationship, dispatch, }: MenuItemsParams): MenuItem[] { const items: MenuItem[] = []; const isRemote = account.acct !== account.username; const remoteDomain = isRemote ? account.acct.split('@')[1] : null; // Share and copy link options if (account.url) { if ('share' in navigator) { items.push({ text: intl.formatMessage(redesignMessages.share), action: () => { void navigator.share({ url: account.url, }); }, icon: ShareIcon, }); } items.push({ text: intl.formatMessage(redesignMessages.copy), action: () => { void navigator.clipboard.writeText(account.url); dispatch(showAlert({ message: redesignMessages.copied })); }, icon: LinkIcon, }); } // Open on remote page. if (isRemote) { items.push({ text: intl.formatMessage(redesignMessages.openOriginalPage, { domain: remoteDomain, }), href: account.url, }); } // Mention and direct message options if (signedIn && !account.suspended) { items.push( null, { text: intl.formatMessage(redesignMessages.mention), action: () => { dispatch(mentionCompose(account)); }, }, { text: intl.formatMessage(redesignMessages.direct), action: () => { dispatch(directCompose(account)); }, }, null, ); } if (!signedIn) { return items; } // List and featuring options if (relationship?.following) { items.push( { text: intl.formatMessage(redesignMessages.addToList), action: () => { dispatch( openModal({ modalType: 'LIST_ADDER', modalProps: { accountId: account.id, }, }), ); }, }, { text: intl.formatMessage( relationship.endorsed ? messages.unendorse : messages.endorse, ), action: () => { if (relationship.endorsed) { dispatch(unpinAccount(account.id)); } else { dispatch(pinAccount(account.id)); } }, }, ); } items.push( { text: intl.formatMessage( relationship?.note ? messages.editNote : messages.addNote, ), description: intl.formatMessage(redesignMessages.noteDescription), action: () => { dispatch( openModal({ modalType: 'ACCOUNT_NOTE', modalProps: { accountId: account.id, }, }), ); }, }, null, ); // Timeline options if (relationship?.following && !relationship.muting) { items.push( { text: intl.formatMessage( relationship.showing_reblogs ? redesignMessages.hideReblogs : redesignMessages.showReblogs, ), action: () => { dispatch( followAccount(account.id, { reblogs: !relationship.showing_reblogs, }), ); }, }, { text: intl.formatMessage(messages.languages), action: () => { dispatch( openModal({ modalType: 'SUBSCRIBED_LANGUAGES', modalProps: { accountId: account.id, }, }), ); }, }, ); } items.push( { text: intl.formatMessage( relationship?.muting ? redesignMessages.unmute : redesignMessages.mute, ), action: () => { if (relationship?.muting) { dispatch(unmuteAccount(account.id)); } else { dispatch(initMuteModal(account)); } }, }, null, ); if (relationship?.followed_by) { items.push({ text: intl.formatMessage(redesignMessages.removeFollower), action: () => { dispatch( openModal({ modalType: 'CONFIRM', modalProps: { title: intl.formatMessage( messages.confirmRemoveFromFollowersTitle, ), message: intl.formatMessage( messages.confirmRemoveFromFollowersMessage, { name: {account.acct} }, ), confirm: intl.formatMessage( messages.confirmRemoveFromFollowersButton, ), onConfirm: () => { void dispatch( removeAccountFromFollowers({ accountId: account.id }), ); }, }, }), ); }, dangerous: true, icon: PersonRemoveIcon, }); } items.push({ text: intl.formatMessage( relationship?.blocking ? redesignMessages.unblock : redesignMessages.block, ), action: () => { if (relationship?.blocking) { dispatch(unblockAccount(account.id)); } else { dispatch(blockAccount(account.id)); } }, dangerous: true, icon: BlockIcon, }); if (!account.suspended) { items.push({ text: intl.formatMessage(redesignMessages.report), action: () => { dispatch(initReport(account)); }, dangerous: true, icon: ReportIcon, }); } if (remoteDomain) { items.push(null, { text: intl.formatMessage( relationship?.domain_blocking ? redesignMessages.domainUnblock : redesignMessages.domainBlock, { domain: remoteDomain, }, ), action: () => { if (relationship?.domain_blocking) { dispatch(unblockDomain(remoteDomain)); } else { dispatch(initDomainBlockModal(account)); } }, dangerous: true, icon: BlockIcon, iconId: 'domain-block', }); } if ( (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) ) { items.push(null); if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { items.push({ text: intl.formatMessage(messages.admin_account, { name: account.username, }), href: `/admin/accounts/${account.id}`, }); } if ( remoteDomain && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION ) { items.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain, }), href: `/admin/instances/${remoteDomain}`, }); } } return items; }