Status redesign: Actions menu and interactions (#40442)

This commit is contained in:
Echo
2026-09-09 18:18:26 +00:00
committed by GitHub
parent 1b043ce64f
commit 8b6f5b22be
14 changed files with 399 additions and 170 deletions

View File

@@ -18,7 +18,7 @@ import { deleteModal } from '../initial_state';
import { selectStatusInteractions } from '../selectors/statuses';
import { showAlert, showGenericAlert } from './alerts';
import { replyCompose } from './compose';
import { replyComposeById } from './compose';
import { quoteComposeById } from './compose_typed';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import {
@@ -42,6 +42,7 @@ import {
export type StatusInteractionIntent =
| 'bookmark'
| 'copy'
| 'delete'
| 'editQuotePolicy'
| 'edit'
@@ -59,6 +60,10 @@ export type StatusInteractionIntent =
| 'translate';
const messages = defineMessages({
copied: {
id: 'status.copied',
defaultMessage: 'Copied post link to clipboard',
},
noEdits: {
id: 'status.cannot_edit',
defaultMessage: 'You are not allowed to edit this post',
@@ -136,6 +141,13 @@ export const statusInteraction = createAppThunk(
dispatch(bookmark(statusImmutable));
}
return;
case 'copy':
void navigator.clipboard
.writeText(status.url ?? status.uri)
.then(() => {
dispatch(showAlert({ message: messages.copied }));
});
return;
case 'delete':
if (!deleteModal) {
void dispatch(deleteStatus(statusId));
@@ -238,7 +250,7 @@ export const statusInteraction = createAppThunk(
}
return;
case 'reply':
dispatch(replyCompose(statusImmutable));
dispatch(replyComposeById(statusId));
return;
case 'report':
dispatch(

View File

@@ -176,6 +176,34 @@ a.base {
}
}
// Toggle buttons
.toggle {
&:hover {
background-color: var(--bg-hover);
}
&.solid {
@include active {
--bg: var(--color-bg-brand-base);
--fg: var(--color-text-on-brand-base);
}
}
&.tonal {
@include active {
--bg: rgb(from var(--color-bg-brand-base) r g b / 10%);
--fg: var(--color-text-brand);
}
}
&.ghost {
@include active {
--bg: transparent;
--fg: var(--color-text-brand);
}
}
}
// Helper classes
.icon,
@@ -197,21 +225,3 @@ a.base {
.loading {
color: inherit;
}
// Toggle tonal buttons by default show the neutral color, but when pressed show the brand color.
.toggle {
&.tonal {
@include active {
--bg: rgb(from var(--color-bg-brand-base) r g b / 10%);
--fg: var(--color-text-brand);
}
}
&.solid,
&.ghost {
@include active {
--bg: var(--color-bg-brand-base);
--fg: var(--color-text-on-brand-base);
}
}
}

View File

@@ -6,7 +6,7 @@ import ChatIcon from '@/material-icons/400-24px/chat.svg?react';
import DownloadIcon from '@/material-icons/400-24px/download.svg?react';
import HeadphonesIcon from '@/material-icons/400-24px/headphones.svg?react';
import { Button, IconButton, ToggleButton } from './redesign';
import { Button, IconButton, ToggleButton, ToggleIconButton } from './redesign';
const iconArgType = {
control: 'select',
@@ -109,3 +109,17 @@ export const Toggle: Story = {
return <ToggleButton {...args} active={active} onClick={onToggle} />;
},
};
export const ToggleIcon: Story = {
render(args) {
const [active, { onToggle }] = useToggle();
return (
<ToggleIconButton
{...args}
active={active}
onClick={onToggle}
icon={args.leadingIcon ?? ChatIcon}
/>
);
},
};

View File

@@ -176,3 +176,15 @@ export const ToggleButton: React.FC<ButtonProps & { active?: boolean }> = ({
className={classNames(className, classes.toggle)}
/>
);
export const ToggleIconButton: React.FC<
IconButtonProps & { active?: boolean }
> = ({ active, className, ...props }) => (
<IconButton
aria-pressed={active}
{...props}
// Toggle buttons always have neutral until pressed.
color='neutral'
className={classNames(className, classes.toggle)}
/>
);

View File

@@ -266,7 +266,7 @@ export type MenuListProps<As extends React.ElementType> = Omit<
'isOpen' | 'onClose' | 'reference' | 'popoverElement'
>;
export const MenuList = <As extends React.ElementType>({
export const MenuList = <As extends React.ElementType = 'div'>({
children,
...props
}: MenuListProps<As>) => {

View File

@@ -1,5 +1,5 @@
import type React from 'react';
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@@ -11,6 +11,7 @@ import {
ChatCircleTextIcon,
DotsThreeIcon,
HeartIcon,
QuotesIcon,
ShareFatIcon,
} from '@phosphor-icons/react';
@@ -34,7 +35,7 @@ import { useStatus } from '@/mastodon/hooks/useStatus';
import { useIdentity } from '@/mastodon/identity_context';
import { quickBoosting } from '@/mastodon/initial_state';
import type { Account } from '@/mastodon/models/account';
import type { MenuItem } from '@/mastodon/models/dropdown_menu';
import type { MenuItem as DropdownItem } from '@/mastodon/models/dropdown_menu';
import type { Relationship } from '@/mastodon/models/relationship';
import type { StatusShape } from '@/mastodon/models/status';
import {
@@ -52,20 +53,30 @@ import {
import type { AppDispatch } from '@/mastodon/store';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { Button, IconButton } from '../button/redesign';
import { Dropdown } from '../dropdown_menu';
import {
Button,
IconButton,
ToggleButton,
ToggleIconButton,
} from '../button/redesign';
import {
Menu,
MenuItem,
MenuItemDivider,
MenuItemLink,
MenuList,
MenuTrigger,
} from '../menu';
import { RemoveQuoteHint } from '../status_action_bar/remove_quote_hint';
import { quoteItemState } from './boost_button_utils';
import { boostItemState, quoteItemState } from './boost_button_utils';
import { useStatusContext } from './hooks';
import classes from './styles.module.scss';
import type { StatusContextType } from './types';
interface StatusActionBarProps {
statusId: string;
contextType?: StatusContextType;
withDismiss?: boolean;
withCounters?: boolean;
scrollKey?: string;
}
const messages = defineMessages({
@@ -137,10 +148,8 @@ const messages = defineMessages({
export const StatusActionBar: React.FC<StatusActionBarProps> = ({
statusId,
contextType,
withDismiss,
withCounters,
scrollKey,
}) => {
const status = useStatus(statusId);
const quotedAccountId = useAppSelector(
@@ -148,16 +157,17 @@ export const StatusActionBar: React.FC<StatusActionBarProps> = ({
state.statuses.getIn([status?.quote?.quoted_status, 'account']) ?? null,
);
const currentAccountId = useCurrentAccountId();
const { contextType } = useStatusContext();
const statusUrl = status?.url ?? status?.uri;
// Actions
const dispatch = useAppDispatch();
const handleReplyClick = useCallback(() => {
dispatch(statusInteraction({ statusId, intent: 'reply' }));
}, [dispatch, statusId]);
const handleBoostClick = useCallback(() => {
dispatch(statusInteraction({ statusId, intent: 'reblog' }));
}, [dispatch, statusId]);
dispatch(statusInteraction({ statusId, intent: 'reply', contextType }));
}, [contextType, dispatch, statusId]);
const handleFavouriteClick = useCallback(() => {
dispatch(statusInteraction({ statusId, intent: 'favourite', contextType }));
}, [contextType, dispatch, statusId]);
const handleShareClick = useCallback(() => {
if (!statusUrl) {
return;
@@ -171,29 +181,40 @@ export const StatusActionBar: React.FC<StatusActionBarProps> = ({
url: statusUrl,
});
} else {
void nav.clipboard.writeText(statusUrl);
dispatch(statusInteraction({ statusId, intent: 'copy', contextType }));
}
}, [statusUrl]);
const handleFavouriteClick = useCallback(() => {
dispatch(statusInteraction({ statusId, intent: 'favourite' }));
}, [dispatch, statusId]);
}, [contextType, dispatch, statusId, statusUrl]);
const handleBookmarkClick = useCallback(() => {
dispatch(statusInteraction({ statusId, intent: 'bookmark' }));
}, [dispatch, statusId]);
dispatch(statusInteraction({ statusId, intent: 'bookmark', contextType }));
}, [contextType, dispatch, statusId]);
const intl = useIntl();
const favouriteIcon = useCallback(
(props: React.SVGProps<SVGSVGElement>) =>
status?.favourited ? (
<HeartIcon {...props} weight='fill' />
) : (
<HeartIcon {...props} />
),
[status?.favourited],
);
const bookmarkIcon = useCallback(
(props: React.SVGProps<SVGSVGElement>) =>
status?.bookmarked ? (
<BookmarkSimpleIcon {...props} weight='fill' />
) : (
<BookmarkSimpleIcon {...props} />
),
[status?.bookmarked],
);
if (!status) {
return null;
}
const isPublic =
status.visibility === 'public' || status.visibility === 'unlisted';
const isReply =
!status.in_reply_to_id || status.in_reply_to_account_id === status.account;
const replyTitle = isReply
? intl.formatMessage(messages.reply)
: intl.formatMessage(messages.replyAll);
const favouriteTitle = intl.formatMessage(
status.favourited ? messages.removeFavourite : messages.favourite,
@@ -208,32 +229,28 @@ export const StatusActionBar: React.FC<StatusActionBarProps> = ({
<Button
size='sm'
variant='ghost'
title={replyTitle}
title={intl.formatMessage(messages.replyAll)}
leadingIcon={ChatCircleTextIcon}
onClick={handleReplyClick}
>
{withCounters && status.replies_count}
</Button>
<Button
size='sm'
variant='ghost'
leadingIcon={ArrowsClockwiseIcon}
onClick={handleBoostClick}
>
<StatusReblogButton statusId={statusId}>
{withCounters && status.reblogs_count}
</Button>
</StatusReblogButton>
<Button
<ToggleButton
size='sm'
variant='ghost'
active={status.favourited}
title={favouriteTitle}
leadingIcon={HeartIcon}
leadingIcon={favouriteIcon}
onClick={handleFavouriteClick}
className={classes.actionsButtonGap}
>
{withCounters && status.favourites_count}
</Button>
</ToggleButton>
{isPublic && (
<IconButton
@@ -246,11 +263,13 @@ export const StatusActionBar: React.FC<StatusActionBarProps> = ({
</IconButton>
)}
<IconButton
<ToggleIconButton
size='sm'
variant='ghost'
icon={BookmarkSimpleIcon}
active={status.bookmarked}
icon={bookmarkIcon}
onClick={handleBookmarkClick}
aria-pressed={status.bookmarked}
>
{!status.bookmarked ? (
<FormattedMessage id='status.bookmark' defaultMessage='Bookmark' />
@@ -260,7 +279,7 @@ export const StatusActionBar: React.FC<StatusActionBarProps> = ({
defaultMessage='Remove bookmark'
/>
)}
</IconButton>
</ToggleIconButton>
<RemoveQuoteHint
className='status__action-bar__button-wrapper'
@@ -270,9 +289,7 @@ export const StatusActionBar: React.FC<StatusActionBarProps> = ({
<StatusActionMenu
dismissQuoteHint={dismissQuoteHint}
status={status}
contextType={contextType}
withDismiss={withDismiss}
scrollKey={scrollKey}
/>
)}
</RemoveQuoteHint>
@@ -280,14 +297,99 @@ export const StatusActionBar: React.FC<StatusActionBarProps> = ({
);
};
const StatusReblogButton: React.FC<{
statusId: string;
children: React.ReactNode;
}> = ({ statusId, children }) => {
const conditions = useAppSelector((state) =>
selectStatusConditions(state, statusId),
);
const { isBoosted } = conditions;
const boostState = boostItemState(conditions);
const quoteState = quoteItemState(conditions);
const intl = useIntl();
const dispatch = useAppDispatch();
const onReblog = useCallback(() => {
dispatch(statusInteraction({ statusId, intent: 'reblog' }));
}, [dispatch, statusId]);
const onQuote = useCallback(() => {
dispatch(statusInteraction({ statusId, intent: 'quote' }));
}, [dispatch, statusId]);
if (quickBoosting) {
return (
<ToggleButton
size='sm'
variant='ghost'
active={isBoosted}
leadingIcon={ArrowsClockwiseIcon}
onClick={onReblog}
disabled={boostState.disabled}
>
{children}
</ToggleButton>
);
}
return (
<Menu>
<MenuTrigger
as={ToggleButton}
size='sm'
variant='ghost'
active={isBoosted}
leadingIcon={ArrowsClockwiseIcon}
>
{children}
</MenuTrigger>
<MenuList placement='bottom' maxWidth={180} container={document.body}>
<MenuItem
onClick={onReblog}
icon={ArrowsClockwiseIcon}
disabled={boostState.disabled}
>
<p>
{intl.formatMessage(boostState.title)}
{boostState.meta && (
<span className={classes.actionDescription}>
{intl.formatMessage(boostState.meta)}
</span>
)}
</p>
</MenuItem>
<MenuItem
onClick={onQuote}
icon={QuotesFilledIcon}
disabled={quoteState.disabled}
>
<p>
{intl.formatMessage(quoteState.title)}
{quoteState.meta && (
<span className={classes.actionDescription}>
{intl.formatMessage(quoteState.meta)}
</span>
)}
</p>
</MenuItem>
</MenuList>
</Menu>
);
};
const QuotesFilledIcon = (props: React.SVGProps<SVGSVGElement>) => (
<QuotesIcon {...props} weight='fill' />
);
const StatusActionMenu: React.FC<{
dismissQuoteHint: () => void;
status: StatusShape;
contextType?: StatusContextType;
scrollKey?: string;
withDismiss?: boolean;
}> = ({ status, dismissQuoteHint, contextType, scrollKey, withDismiss }) => {
}> = ({ status, dismissQuoteHint, withDismiss }) => {
const account = useAppSelector((state) => state.accounts.get(status.account));
const { contextType } = useStatusContext();
const { permissions } = useIdentity();
const relationship = useRelationship(account?.id);
const intl = useIntl();
@@ -337,7 +439,7 @@ const StatusActionMenu: React.FC<{
dispatch,
],
);
const handleOpen = useCallback(() => {
const onOpen = useCallback(() => {
// Replicates needsStatusRefresh of the Dropdown component.
if (quickBoosting && !status.quote_approval) {
dispatch(
@@ -350,14 +452,61 @@ const StatusActionMenu: React.FC<{
}, [dismissQuoteHint, dispatch, status.id, status.quote_approval]);
return (
<Dropdown scrollKey={scrollKey} items={menu} onOpen={handleOpen}>
<IconButton size='sm' variant='ghost' icon={DotsThreeIcon}>
<Menu>
<MenuTrigger
as={IconButton}
size='sm'
variant='ghost'
icon={DotsThreeIcon}
>
<FormattedMessage id='status.more' defaultMessage='More' />
</IconButton>
</Dropdown>
</MenuTrigger>
<MenuList placement='top-end' container={document.body}>
{menu.map((item, index) => (
<StatusActionItem key={index} item={item} />
))}
<StatusActionLoader onMount={onOpen} />
</MenuList>
</Menu>
);
};
const StatusActionItem: React.FC<{ item: DropdownItem }> = ({ item }) => {
if (!item) {
return <MenuItemDivider />;
}
const commonProps = {
icon: item.icon,
disabled: item.disabled,
className: classNames(item.dangerous && classes.actionDangerous),
children: item.description ? (
<p>
{item.text}
<span className={classes.actionDescription}>{item.description}</span>
</p>
) : (
item.text
),
} as const;
if ('to' in item) {
return <MenuItemLink {...commonProps} to={item.to} as='link' />;
} else if ('href' in item) {
return <MenuItemLink {...commonProps} href={item.href} as='a' />;
}
return <MenuItem {...commonProps} onClick={item.action} />;
};
const StatusActionLoader = ({ onMount }: { onMount: () => void }) => {
useEffect(() => {
onMount();
}, [onMount]);
return null;
};
interface MenuItemsParams {
status: StatusShape;
account?: Account;
@@ -383,7 +532,7 @@ function getMenuItems({
relationship,
dispatch,
}: MenuItemsParams) {
const menu: MenuItem[] = [];
const menu: DropdownItem[] = [];
const statusId = status.id;
const statusUrl = status.url ?? status.uri;
@@ -403,9 +552,7 @@ function getMenuItems({
menu.push({
text: intl.formatMessage(messages.copy),
action: () => {
void navigator.clipboard.writeText(statusUrl);
},
action: onStatusInteraction('copy'),
});
if (isPublic && 'share' in navigator) {

View File

@@ -12,6 +12,7 @@ import {
} from '@/testing/factories';
import { StatusAttachments } from './attachments';
import { StatusContext } from './hooks';
import type { AttachmentArgs } from './testing';
import { attachmentArgTypes, attachmentFactory } from './testing';
@@ -26,9 +27,9 @@ const meta = {
title: 'Components/Status/StatusAttachments',
render() {
return (
<div style={{ width: 'min(600px, 80vw)' }}>
<StatusAttachments statusId='1' contextType='home' />
</div>
<StatusContext.Provider value={{}}>
<StatusAttachments statusId='1' />
</StatusContext.Provider>
);
},
args: {
@@ -138,6 +139,13 @@ const meta = {
};
},
},
decorators: [
(Story) => (
<div style={{ width: 'min(600px, 80vw)' }}>
<Story />
</div>
),
],
} as Meta<StatusAttachmentsStoryProps>;
export default meta;

View File

@@ -24,12 +24,12 @@ import { Card, CardBody, CardTitle } from '../card';
import { PictureInPicturePlaceholder } from '../picture_in_picture_placeholder';
import { RelativeTimestamp } from '../relative_timestamp';
import { useStatusContext } from './hooks';
import { StatusQuote } from './quote';
export const StatusAttachments: React.FC<{
statusId: string;
contextType?: string;
}> = ({ statusId, contextType }) => {
}> = ({ statusId }) => {
const status = useExpandedStatus(statusId);
if (!status) {
@@ -42,7 +42,6 @@ export const StatusAttachments: React.FC<{
<MediaAttachments
statusId={statusId}
accountId={status.account.id}
contextType={contextType}
sensitive={status.sensitive}
language={status.translation?.language ?? status.language}
attachment={attachment}
@@ -98,7 +97,6 @@ const Video = lazy(() => import('@/mastodon/features/video'));
const MediaAttachments: React.FC<{
statusId: string;
accountId: string;
contextType?: string;
sensitive: boolean;
language: string;
attachment: MediaAttachmentShape;
@@ -107,7 +105,6 @@ const MediaAttachments: React.FC<{
}> = ({
statusId,
accountId,
contextType,
sensitive,
language,
attachment,
@@ -122,6 +119,7 @@ const MediaAttachments: React.FC<{
'media_attachments',
]) as Immutable.List<MediaAttachment>;
});
const { contextType } = useStatusContext();
const mediaFilters = useAppSelector((state) =>
selectMediaFilters(state, { statusId, contextType }),
);

View File

@@ -90,7 +90,6 @@ export const StatusContent: React.FC<
ref={onRef}
>
<EmojiHTML
{...props}
className={classes.contentText}
ref={onRef}
lang={language}

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react';
import { createContext, use, useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@@ -23,6 +23,15 @@ import { FOCUS_TARGET } from '../navigation_focus_target';
import { useElementHandledLink } from './handled_link';
import type { StatusContextType } from './types';
export const StatusContext = createContext<{
id?: string | null;
contextType?: StatusContextType;
}>({});
export function useStatusContext() {
return use(StatusContext);
}
const messages = defineMessages({
quote_noun: {
id: 'status.quote_noun',

View File

@@ -18,7 +18,11 @@ import { StatusActionBar } from './action_bar';
import { StatusAttachments } from './attachments';
import { StatusContent } from './content';
import type { StatusHandlers } from './hooks';
import { useStatusHandlers, useTextForScreenReader } from './hooks';
import {
StatusContext,
useStatusHandlers,
useTextForScreenReader,
} from './hooks';
import { StatusMeta } from './meta';
import { StatusPrepend } from './prepend';
import { StatusRedesignHeader } from './redesign/header';
@@ -65,9 +69,8 @@ export const StatusRedesign: React.FC<StatusRedesignProps> = ({
isQuotedPost,
hidden,
showActions = true,
scrollKey,
children,
withCounters,
withCounters = true,
withDismiss,
onOpen,
showThread,
@@ -140,94 +143,94 @@ export const StatusRedesign: React.FC<StatusRedesignProps> = ({
(showActions && !isQuotedPost);
return (
<StatusHotkeys
{...hotkeysProps}
className={classNames(
classes.root,
variant === 'thread' && classes.variantThread,
variant === 'page' && classes.variantPage,
isQuotedPost && classes.isQuote,
)}
data-featured={featured ? 'true' : null}
aria-label={screenReaderText}
data-nosnippet={status.account.noindex || undefined}
>
{!skipPrepend && (
<StatusPrepend
status={actualStatus}
isReblog={!!parent}
showThread={showThread}
/>
)}
<StatusContext.Provider value={{ id, contextType }}>
<StatusHotkeys
{...hotkeysProps}
className={classNames(
classes.root,
variant === 'thread' && classes.variantThread,
variant === 'page' && classes.variantPage,
isQuotedPost && classes.isQuote,
)}
data-featured={featured ? 'true' : null}
aria-label={screenReaderText}
data-nosnippet={status.account.noindex || undefined}
>
{!skipPrepend && (
<StatusPrepend
status={actualStatus}
isReblog={!!parent}
showThread={showThread}
/>
)}
<StatusRedesignHeader status={status} className={classes.header}>
{headerContents}
</StatusRedesignHeader>
<StatusRedesignHeader status={status} className={classes.header}>
{headerContents}
</StatusRedesignHeader>
{matchedFilters.length > 0 && (
<FilterWarning
title={matchedFilters.map((filter) => filter.title).join(', ')}
expanded={showDespiteFilter}
onClick={onFilterToggle}
/>
)}
{matchedFilters.length > 0 && (
<FilterWarning
title={matchedFilters.map((filter) => filter.title).join(', ')}
expanded={showDespiteFilter}
onClick={onFilterToggle}
/>
)}
{(matchedFilters.length === 0 || showDespiteFilter) && (
<ContentWarning
statusId={status.id}
expanded={expanded}
onClick={onExpandedToggle}
/>
)}
{(matchedFilters.length === 0 || showDespiteFilter) && (
<ContentWarning
statusId={status.id}
expanded={expanded}
onClick={onExpandedToggle}
/>
)}
{expanded && (
<StatusContent
status={status}
statusContent={statusContent}
onReadMore={handlers.onOpen}
onTranslate={onTranslate}
collapsible
>
{!!status.poll && (
<Poll
pollId={status.poll}
statusUrl={status.uri}
accountId={status.account.id}
lang={status.translation?.language ?? status.language}
/>
)}
{expanded && (
<StatusContent
status={status}
statusContent={statusContent}
onReadMore={handlers.onOpen}
onTranslate={onTranslate}
collapsible
>
{!!status.poll && (
<Poll
pollId={status.poll}
statusUrl={status.uri}
accountId={status.account.id}
lang={status.translation?.language ?? status.language}
/>
)}
<StatusAttachments statusId={status.id} contextType={contextType} />
<StatusAttachments statusId={status.id} />
{children}
</StatusContent>
)}
{children}
</StatusContent>
)}
{showFooter && (
<footer className={classes.footer}>
{expanded && hashtagsInBar.length > 0 && (
<HashtagBar
hashtags={hashtagsInBar}
accountId={status.account.id}
/>
)}
{showFooter && (
<footer className={classes.footer}>
{expanded && hashtagsInBar.length > 0 && (
<HashtagBar
hashtags={hashtagsInBar}
accountId={status.account.id}
/>
)}
{variant === 'page' && (
<StatusMeta status={status} className={classes.meta} />
)}
{variant === 'page' && (
<StatusMeta status={status} className={classes.meta} />
)}
{showActions && !isQuotedPost && (
<StatusActionBar
scrollKey={scrollKey}
statusId={status.id}
contextType={contextType}
withDismiss={withDismiss}
withCounters={withCounters}
/>
)}
</footer>
)}
</StatusHotkeys>
{showActions && !isQuotedPost && (
<StatusActionBar
statusId={status.id}
withDismiss={withDismiss}
withCounters={withCounters}
/>
)}
</footer>
)}
</StatusHotkeys>
</StatusContext.Provider>
);
};

View File

@@ -111,7 +111,7 @@
.actions {
display: flex;
button:not(:hover, :active) {
> button:not(:hover, :active, [aria-pressed='true'], [aria-expanded='true']) {
opacity: 0.7;
}
}
@@ -120,6 +120,21 @@
margin-inline-end: auto;
}
.actionDescription {
@include mixins.type-label-sm;
display: block;
color: var(--color-text-tertiary);
[data-menu-item='true'][aria-disabled='true'] & {
color: inherit;
}
}
.actionDangerous {
color: var(--color-text-error);
}
.buttonAlign {
margin-inline-start: calc(-1 * var(--space-sm));
}

View File

@@ -1367,6 +1367,7 @@
"status.context.retry": "Retry",
"status.context.show": "Show",
"status.continued_thread": "Continued thread",
"status.copied": "Copied post link to clipboard",
"status.copy": "Copy link to post",
"status.delete": "Delete",
"status.delete.success": "Post deleted",

View File

@@ -172,6 +172,7 @@ export const selectStatusInteractions = createAppSelector(
Partial<typeof conditionals> & { allowed: boolean }
> = {
bookmark: addAllowed({ isLoggedIn }),
copy: addAllowed({}),
delete: addAllowed({ isMine }),
edit: addAllowed({ isMine }),
editQuotePolicy: addAllowed({ isMine, isPublic }),