Profile redesign: Follow button and menu reorg (#37707)

This commit is contained in:
Echo 2026-02-03 12:33:20 +01:00 committed by GitHub
parent 218ca36653
commit 346ca87ee8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 733 additions and 274 deletions

View File

@ -153,7 +153,8 @@ export function fetchAccountFail(id, error) {
*/
export function followAccount(id, options = { reblogs: true }) {
return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
const relationship = getState().getIn(['relationships', id]);
const alreadyFollowing = relationship?.following || relationship?.requested;
const locked = getState().getIn(['accounts', id, 'locked'], false);
dispatch(followAccountRequest({ id, locked }));

View File

@ -71,10 +71,15 @@ export const DropdownMenuItemContent: React.FC<{ item: MenuItem }> = ({
return null;
}
const { text, description, icon } = item;
const { text, description, icon, iconId } = item;
return (
<>
{icon && <Icon icon={icon} id={`${text}-icon`} />}
{icon && (
<Icon
icon={icon}
id={iconId ?? text.toLowerCase().replaceAll(/[^a-z]+/g, '-')}
/>
)}
<span className='dropdown-menu__item-content'>
{text}
{Boolean(description) && (

View File

@ -5,6 +5,7 @@ import { useIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';
import { useIdentity } from '@/mastodon/identity_context';
import { isClientFeatureEnabled } from '@/mastodon/utils/environment';
import {
fetchRelationships,
followAccount,
@ -94,7 +95,17 @@ export const FollowButton: React.FC<{
if (accountId === me) {
return;
} else if (relationship.muting) {
} else if (relationship.blocking) {
dispatch(
openModal({
modalType: 'CONFIRM_UNBLOCK',
modalProps: { account },
}),
);
} else if (
relationship.muting &&
!isClientFeatureEnabled('profile_redesign')
) {
dispatch(unmuteAccount(accountId));
} else if (account && relationship.following) {
dispatch(
@ -107,13 +118,6 @@ export const FollowButton: React.FC<{
modalProps: { account },
}),
);
} else if (relationship.blocking) {
dispatch(
openModal({
modalType: 'CONFIRM_UNBLOCK',
modalProps: { account },
}),
);
} else {
dispatch(followAccount(accountId));
}
@ -136,7 +140,10 @@ export const FollowButton: React.FC<{
label = intl.formatMessage(messages.editProfile);
} else if (!relationship) {
label = <LoadingIndicator />;
} else if (relationship.muting) {
} else if (
relationship.muting &&
!isClientFeatureEnabled('profile_redesign')
) {
label = intl.formatMessage(messages.unmute);
} else if (relationship.following) {
label = intl.formatMessage(messages.unfollow);
@ -173,7 +180,7 @@ export const FollowButton: React.FC<{
(!(relationship?.following || relationship?.requested) &&
(account?.suspended || !!account?.moved))
}
secondary={following}
secondary={following || relationship?.blocking}
compact={compact}
className={classNames(className, { 'button--destructive': following })}
>

View File

@ -1,4 +1,5 @@
import { useCallback } from 'react';
import type { RefCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
@ -77,6 +78,40 @@ export const AccountHeader: React.FC<{
[dispatch, account],
);
const [isFooterIntersecting, setIsIntersecting] = useState(false);
const handleIntersect: IntersectionObserverCallback = useCallback(
(entries) => {
const entry = entries.at(0);
if (!entry) {
return;
}
setIsIntersecting(entry.isIntersecting);
},
[],
);
const [observer] = useState(
() =>
new IntersectionObserver(handleIntersect, {
rootMargin: '0px 0px -55px 0px', // Height of bottom nav bar.
}),
);
const handleObserverRef: RefCallback<HTMLDivElement> = useCallback(
(node) => {
if (node) {
observer.observe(node);
}
},
[observer],
);
useEffect(() => {
return () => {
observer.disconnect();
};
}, [observer]);
if (!account) {
return null;
}
@ -114,7 +149,12 @@ export const AccountHeader: React.FC<{
)}
</div>
<div className='account__header__bar'>
<div
className={classNames(
'account__header__bar',
isRedesignEnabled() && redesignClasses.barWrapper,
)}
>
<div className='account__header__tabs'>
<a
className='avatar'
@ -144,7 +184,13 @@ export const AccountHeader: React.FC<{
)}
>
<AccountName accountId={accountId} />
{isRedesignEnabled() && <AccountButtons accountId={accountId} />}
{isRedesignEnabled() && (
<AccountButtons
accountId={accountId}
className={redesignClasses.buttonsDesktop}
noShare
/>
)}
</div>
<AccountBadges accountId={accountId} />
@ -153,11 +199,13 @@ export const AccountHeader: React.FC<{
<FamiliarFollowers accountId={accountId} />
)}
<AccountButtons
className='account__header__buttons--mobile'
accountId={accountId}
noShare
/>
{!isRedesignEnabled() && (
<AccountButtons
className='account__header__buttons--mobile'
accountId={accountId}
noShare
/>
)}
{!suspendedOrHidden && (
<div className='account__header__extra'>
@ -181,10 +229,22 @@ export const AccountHeader: React.FC<{
<AccountNumberFields accountId={accountId} />
</div>
)}
{isRedesignEnabled() && (
<AccountButtons
className={classNames(
redesignClasses.buttonsMobile,
!isFooterIntersecting && redesignClasses.buttonsMobileIsStuck,
)}
accountId={accountId}
noShare
/>
)}
</div>
</AnimateEmojiProvider>
{!hideTabs && !hidden && <AccountTabs acct={account.acct} />}
<div ref={handleObserverRef} />
<Helmet>
<title>{titleFromAccount(account)}</title>

View File

@ -108,48 +108,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
className={classNames(className, classes.badgeMuted)}
/>,
);
} else if (
relationship.followed_by &&
(relationship.following || relationship.requested)
) {
badges.push(
<Badge
key='mutuals-badge'
label={
<FormattedMessage
id='account.badges.mutuals'
defaultMessage='You follow each other'
/>
}
className={className}
/>,
);
} else if (relationship.followed_by) {
badges.push(
<Badge
key='follows-you-badge'
label={
<FormattedMessage
id='account.badges.follows_you'
defaultMessage='Follows you'
/>
}
className={className}
/>,
);
} else if (relationship.requested_by) {
badges.push(
<Badge
key='requested-to-follow-badge'
label={
<FormattedMessage
id='account.badges.requested_to_follow'
defaultMessage='Requested to follow you'
/>
}
className={className}
/>,
);
}
}

View File

@ -12,6 +12,7 @@ import {
unpinAccount,
} from '@/mastodon/actions/accounts';
import { removeAccountFromFollowers } from '@/mastodon/actions/accounts_typed';
import { showAlert } from '@/mastodon/actions/alerts';
import { directCompose, mentionCompose } from '@/mastodon/actions/compose';
import {
initDomainBlockModal,
@ -23,16 +24,78 @@ import { initReport } from '@/mastodon/actions/reports';
import { Dropdown } from '@/mastodon/components/dropdown_menu';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useIdentity } from '@/mastodon/identity_context';
import type { Account } from '@/mastodon/models/account';
import type { MenuItem } from '@/mastodon/models/dropdown_menu';
import type { Relationship } from '@/mastodon/models/relationship';
import {
PERMISSION_MANAGE_FEDERATION,
PERMISSION_MANAGE_USERS,
} from '@/mastodon/permissions';
import type { AppDispatch } from '@/mastodon/store';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
import EditIcon from '@/material-icons/400-24px/edit_square.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';
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 dispatch = useAppDispatch();
const menuItems = useMemo(() => {
if (!account) {
return [];
}
if (isRedesignEnabled()) {
return redesignMenuItems({
account,
signedIn,
permissions,
intl,
relationship,
dispatch,
});
}
return currentMenuItems({
account,
signedIn,
permissions,
intl,
relationship,
dispatch,
});
}, [account, signedIn, permissions, intl, relationship, dispatch]);
return (
<Dropdown
disabled={menuItems.length === 0}
items={menuItems}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
/>
);
};
interface MenuItemsParams {
account: Account;
signedIn: boolean;
permissions: number;
intl: ReturnType<typeof useIntl>;
relationship?: Relationship;
dispatch: AppDispatch;
}
const messages = defineMessages({
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
@ -109,80 +172,78 @@ const messages = defineMessages({
},
});
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl();
const { signedIn, permissions } = useIdentity();
function currentMenuItems({
account,
signedIn,
permissions,
intl,
relationship,
dispatch,
}: MenuItemsParams): MenuItem[] {
const items: MenuItem[] = [];
const isRemote = account.acct !== account.username;
const account = useAccount(accountId);
const relationship = useAppSelector((state) =>
state.relationships.get(accountId),
);
const dispatch = useAppDispatch();
const menuItems = useMemo(() => {
const arr: MenuItem[] = [];
if (!account) {
return arr;
}
const isRemote = account.acct !== account.username;
if (signedIn && !account.suspended) {
arr.push({
if (signedIn && !account.suspended) {
items.push(
{
text: intl.formatMessage(messages.mention, {
name: account.username,
}),
action: () => {
dispatch(mentionCompose(account));
},
});
arr.push({
},
{
text: intl.formatMessage(messages.direct, {
name: account.username,
}),
action: () => {
dispatch(directCompose(account));
},
});
arr.push(null);
}
},
null,
);
}
if (isRemote) {
arr.push({
if (isRemote) {
items.push(
{
text: intl.formatMessage(messages.openOriginalPage),
href: account.url,
});
arr.push(null);
}
},
null,
);
}
if (!signedIn) {
return arr;
}
if (!signedIn) {
return items;
}
if (relationship?.following) {
if (!relationship.muting) {
if (relationship.showing_reblogs) {
arr.push({
text: intl.formatMessage(messages.hideReblogs, {
name: account.username,
}),
action: () => {
dispatch(followAccount(account.id, { reblogs: false }));
},
});
} else {
arr.push({
text: intl.formatMessage(messages.showReblogs, {
name: account.username,
}),
action: () => {
dispatch(followAccount(account.id, { reblogs: true }));
},
});
}
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 }));
},
});
}
arr.push({
items.push(
{
text: intl.formatMessage(messages.languages),
action: () => {
dispatch(
@ -194,13 +255,295 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
}),
);
},
});
arr.push(null);
}
},
null,
);
}
if (isRedesignEnabled()) {
arr.push({
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: <strong>{account.acct}</strong> },
),
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' },
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,
},
null,
);
}
// Mention and direct message options
if (signedIn && !account.suspended) {
items.push(
{
text: intl.formatMessage(redesignMessages.mention),
action: () => {
dispatch(mentionCompose(account));
},
},
{
text: intl.formatMessage(redesignMessages.direct),
action: () => {
dispatch(directCompose(account));
},
},
null,
{
text: intl.formatMessage(
relationship?.note ? messages.editNote : messages.addNote,
),
@ -214,27 +557,34 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
}),
);
},
});
if (!relationship?.following) {
arr.push(null);
}
}
icon: EditIcon,
},
null,
);
}
if (relationship?.following) {
arr.push({
text: intl.formatMessage(
relationship.endorsed ? messages.unendorse : messages.endorse,
),
action: () => {
if (relationship.endorsed) {
dispatch(unpinAccount(account.id));
} else {
dispatch(pinAccount(account.id));
}
},
});
arr.push({
text: intl.formatMessage(messages.add_or_remove_from_list),
// Open on remote page.
if (isRemote) {
items.push(
{
text: intl.formatMessage(redesignMessages.openOriginalPage, {
domain: remoteDomain,
}),
href: account.url,
},
null,
);
}
if (!signedIn) {
return items;
}
// List and featuring options
if (relationship?.following) {
items.push(
{
text: intl.formatMessage(redesignMessages.addToList),
action: () => {
dispatch(
openModal({
@ -245,12 +595,76 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
}),
);
},
});
arr.push(null);
},
{
text: intl.formatMessage(
relationship.endorsed ? messages.unendorse : messages.endorse,
),
action: () => {
if (relationship.endorsed) {
dispatch(unpinAccount(account.id));
} else {
dispatch(pinAccount(account.id));
}
},
},
null,
);
// Timeline options
if (!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,
},
}),
);
},
},
);
}
if (relationship?.followed_by) {
const handleRemoveFromFollowers = () => {
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',
@ -273,134 +687,91 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
},
}),
);
};
},
dangerous: true,
icon: PersonRemoveIcon,
});
}
arr.push({
text: intl.formatMessage(messages.removeFromFollowers, {
name: account.username,
}),
action: handleRemoveFromFollowers,
dangerous: true,
});
}
if (relationship?.muting) {
arr.push({
text: intl.formatMessage(messages.unmute, {
name: account.username,
}),
action: () => {
dispatch(unmuteAccount(account.id));
},
});
} else {
arr.push({
text: intl.formatMessage(messages.mute, {
name: account.username,
}),
action: () => {
dispatch(initMuteModal(account));
},
dangerous: true,
});
}
if (relationship?.blocking) {
arr.push({
text: intl.formatMessage(messages.unblock, {
name: account.username,
}),
action: () => {
dispatch(unblockAccount(account.id));
},
});
} else {
arr.push({
text: intl.formatMessage(messages.block, {
name: account.username,
}),
action: () => {
dispatch(blockAccount(account.id));
},
dangerous: true,
});
}
if (!account.suspended) {
arr.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) {
arr.push(null);
if (relationship?.domain_blocking) {
arr.push({
text: intl.formatMessage(messages.unblockDomain, {
domain: remoteDomain,
}),
action: () => {
dispatch(unblockDomain(remoteDomain));
},
});
items.push({
text: intl.formatMessage(
relationship?.blocking
? redesignMessages.unblock
: redesignMessages.block,
),
action: () => {
if (relationship?.blocking) {
dispatch(unblockAccount(account.id));
} else {
arr.push({
text: intl.formatMessage(messages.blockDomain, {
domain: remoteDomain,
}),
action: () => {
dispatch(initDomainBlockModal(account));
},
dangerous: true,
});
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({
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 (
(permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS ||
(isRemote &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION)
remoteDomain &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION
) {
arr.push(null);
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
arr.push({
text: intl.formatMessage(messages.admin_account, {
name: account.username,
}),
href: `/admin/accounts/${account.id}`,
});
}
if (
isRemote &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION
) {
arr.push({
text: intl.formatMessage(messages.admin_domain, {
domain: remoteDomain,
}),
href: `/admin/instances/${remoteDomain}`,
});
}
items.push({
text: intl.formatMessage(messages.admin_domain, {
domain: remoteDomain,
}),
href: `/admin/instances/${remoteDomain}`,
});
}
}
return arr;
}, [account, signedIn, permissions, intl, relationship, dispatch]);
return (
<Dropdown
disabled={menuItems.length === 0}
items={menuItems}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
/>
);
};
return items;
}

View File

@ -1,3 +1,7 @@
.barWrapper {
border-bottom: none;
}
.nameWrapper {
display: flex;
gap: 16px;
@ -45,6 +49,43 @@
}
}
$button-breakpoint: 420px;
$button-fallback-breakpoint: #{$button-breakpoint} + 55px;
.buttonsDesktop {
@container (width < #{$button-breakpoint}) {
display: none;
}
@supports (not (container-type: inline-size)) {
@media (max-width: #{$button-fallback-breakpoint}) {
display: none;
}
}
}
.buttonsMobile {
position: sticky;
bottom: 55px; // Height of bottom nav bar.
@container (width >= #{$button-breakpoint}) {
display: none;
}
@supports (not (container-type: inline-size)) {
@media (min-width: (#{$button-fallback-breakpoint} + 1px)) {
display: none;
}
}
}
.buttonsMobileIsStuck {
padding: 12px 16px;
background-color: var(--color-bg-primary);
border-top: 1px solid var(--color-border-primary);
margin: 0 -20px;
}
.badge {
background-color: var(--color-bg-secondary);
border: none;

View File

@ -21,11 +21,8 @@
"account.badges.blocked": "Blocked",
"account.badges.bot": "Automated",
"account.badges.domain_blocked": "Blocked domain",
"account.badges.follows_you": "Follows you",
"account.badges.group": "Group",
"account.badges.muted": "Muted",
"account.badges.mutuals": "You follow each other",
"account.badges.requested_to_follow": "Requested to follow you",
"account.block": "Block @{name}",
"account.block_domain": "Block domain {domain}",
"account.block_short": "Block",
@ -80,6 +77,23 @@
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Mention @{name}",
"account.menu.add_to_list": "Add to list…",
"account.menu.block": "Block account",
"account.menu.block_domain": "Block {domain}",
"account.menu.copied": "Copied account link to clipboard",
"account.menu.copy": "Copy link",
"account.menu.direct": "Privately mention",
"account.menu.hide_reblogs": "Hide boosts in timeline",
"account.menu.mention": "Mention",
"account.menu.mute": "Mute account",
"account.menu.open_original_page": "View on {domain}",
"account.menu.remove_follower": "Remove follower",
"account.menu.report": "Report account",
"account.menu.share": "Share…",
"account.menu.show_reblogs": "Show boosts in timeline",
"account.menu.unblock": "Unblock account",
"account.menu.unblock_domain": "Unblock {domain}",
"account.menu.unmute": "Unmute account",
"account.moved_to": "{name} has indicated that their new account is now:",
"account.mute": "Mute @{name}",
"account.mute_notifications_short": "Mute notifications",

View File

@ -6,6 +6,7 @@ interface BaseMenuItem {
text: string;
description?: string;
icon?: IconProp;
iconId?: string;
highlighted?: boolean;
disabled?: boolean;
dangerous?: boolean;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M318-120q-82 0-140-58t-58-140q0-40 15-76t43-64l134-133 56 56-134 134q-17 17-25.5 38.5T200-318q0 49 34.5 83.5T318-200q23 0 45-8.5t39-25.5l133-134 57 57-134 133q-28 28-64 43t-76 15Zm79-220-57-57 223-223 57 57-223 223Zm251-28-56-57 134-133q17-17 25-38t8-44q0-50-34-85t-84-35q-23 0-44.5 8.5T558-726L425-592l-57-56 134-134q28-28 64-43t76-15q82 0 139.5 58T839-641q0 39-14.5 75T782-502L648-368Z"/></svg>

After

Width:  |  Height:  |  Size: 493 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M318-120q-82 0-140-58t-58-140q0-40 15-76t43-64l134-133 56 56-134 134q-17 17-25.5 38.5T200-318q0 49 34.5 83.5T318-200q23 0 45-8.5t39-25.5l133-134 57 57-134 133q-28 28-64 43t-76 15Zm79-220-57-57 223-223 57 57-223 223Zm251-28-56-57 134-133q17-17 25-38t8-44q0-50-34-85t-84-35q-23 0-44.5 8.5T558-726L425-592l-57-56 134-134q28-28 64-43t76-15q82 0 139.5 58T839-641q0 39-14.5 75T782-502L648-368Z"/></svg>

After

Width:  |  Height:  |  Size: 493 B

View File

@ -8248,7 +8248,6 @@ noscript {
}
.account__header {
overflow: hidden;
container: account-header / inline-size;
&.inactive {