[Glitch] Profile redesign: Follow button and menu reorg

Port 346ca87ee8 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo
2026-02-03 12:33:20 +01:00
committed by Claire
parent 3c2a06dc24
commit 82f7cdcb48
9 changed files with 714 additions and 271 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 '@/flavours/glitch/identity_context';
import { isClientFeatureEnabled } from '@/flavours/glitch/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.edit_profile);
} 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';
@@ -81,6 +82,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;
}
@@ -118,7 +153,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'
@@ -148,7 +188,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} />
@@ -157,11 +203,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'>
@@ -183,12 +231,24 @@ export const AccountHeader: React.FC<{
</div>
</div>
)}
{isRedesignEnabled() && (
<AccountButtons
className={classNames(
redesignClasses.buttonsMobile,
!isFooterIntersecting && redesignClasses.buttonsMobileIsStuck,
)}
accountId={accountId}
noShare
/>
)}
</div>
</AnimateEmojiProvider>
<ActionBar account={account} />
{!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 '@/flavours/glitch/actions/accounts';
import { removeAccountFromFollowers } from '@/flavours/glitch/actions/accounts_typed';
import { showAlert } from '@/flavours/glitch/actions/alerts';
import {
directCompose,
mentionCompose,
@@ -26,16 +27,78 @@ 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 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}' },
@@ -112,80 +175,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(
@@ -197,13 +258,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,
),
@@ -217,27 +560,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({
@@ -248,12 +598,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',
@@ -276,134 +690,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

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

View File

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