Status redesign: Fix padding and adds variants (#40400)

This commit is contained in:
Echo
2026-09-07 15:46:09 +00:00
committed by GitHub
parent 5c87ea4829
commit 834fcf0a84
9 changed files with 210 additions and 22 deletions

View File

@@ -0,0 +1,103 @@
import type React from 'react';
import { useMemo } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import type {
AnyStatusShape,
StatusVisibility,
} from '@/mastodon/models/status';
import { statusLink } from './utils';
export const StatusMeta: React.FC<
{
status: Pick<
AnyStatusShape,
'account' | 'application' | 'created_at' | 'id' | 'visibility'
>;
} & React.ComponentPropsWithRef<'span'>
> = ({ status, ...props }) => {
const { created_at, application } = status;
const createdAt = useMemo(() => {
try {
return new Date(created_at);
} catch {
return null;
}
}, [created_at]);
const visibility = useMemo(
() => statusVisibilityText(status.visibility),
[status.visibility],
);
if (!createdAt) {
return null;
}
let applicationLink: React.ReactNode = application.name;
if (application.website) {
applicationLink = (
<a
href={status.application.website}
target='_blank'
rel='noopener noreferrer'
>
{status.application.name}
</a>
);
}
return (
<span {...props}>
<FormattedMessage
id='status.meta'
defaultMessage='{createdAt} on {source} • {visibility}'
values={{
createdAt: (
<Link to={statusLink(status)}>
<FormattedDate
value={createdAt}
year='numeric'
month='short'
day='2-digit'
hour='2-digit'
minute='2-digit'
/>
</Link>
),
source: applicationLink,
visibility,
}}
/>
</span>
);
};
function statusVisibilityText(visibility: StatusVisibility) {
switch (visibility) {
case 'private':
return (
<FormattedMessage
id='privacy.private.short'
defaultMessage='Followers'
/>
);
case 'direct':
return (
<FormattedMessage
id='privacy.message.short'
defaultMessage='Message'
description='Message refers to a direct message. For languages where this is confusing, "chat" or "direct message" can be used.'
/>
);
default:
return (
<FormattedMessage id='privacy.public.short' defaultMessage='Public' />
);
}
}

View File

@@ -13,6 +13,7 @@ import {
statusFactoryImmutable,
} from '@/testing/factories';
import type { StatusVariant } from './status';
import { StatusRedesign } from './status';
import type { AttachmentArgs } from './testing';
import { attachmentArgTypes, attachmentFactory } from './testing';
@@ -42,6 +43,7 @@ interface StatusStoryProps extends AttachmentArgs {
// Display
showThread?: boolean;
contextType?: StatusContextType;
variant?: StatusVariant;
showCounters?: boolean;
favouriteCount?: number;
reblogCount?: number;
@@ -67,6 +69,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
disableActions = false,
contextType,
variant,
showThread,
showCounters,
hidden,
@@ -81,6 +84,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
isQuotedPost={isQuote}
showActions={!disableActions}
contextType={contextType}
variant={variant}
withCounters={showCounters}
// Either we are showing a thread (in a timeline) or it's a full reply chain view.
showThread={isReply && showThread}
@@ -201,6 +205,11 @@ const meta = {
showTranslate: categoryInteraction,
// Display
variant: {
...categoryDisplay,
control: 'inline-radio',
options: ['feed', 'thread', 'page'] satisfies StatusVariant[],
},
showCounters: categoryDisplay,
favouriteCount: categoryDisplay,
reblogCount: categoryDisplay,
@@ -253,6 +262,7 @@ const meta = {
disableActions: false,
showTranslate: false,
variant: 'feed',
favouriteCount: 0,
reblogCount: 0,
replyCount: 0,

View File

@@ -19,6 +19,7 @@ import { StatusAttachments } from './attachments';
import { StatusContent } from './content';
import type { StatusHandlers } from './hooks';
import { useStatusHandlers, useTextForScreenReader } from './hooks';
import { StatusMeta } from './meta';
import { StatusPrepend } from './prepend';
import { StatusRedesignHeader } from './redesign/header';
import classes from './styles.module.scss';
@@ -30,9 +31,12 @@ type StatusRedesignProps = Merge<
accountId?: string;
contextType?: StatusContextType;
headerContents?: React.ReactNode;
variant?: StatusVariant;
}
>;
export type StatusVariant = 'feed' | 'thread' | 'page';
const selectStatusReblog = createAppSelector(
[(state, id?: string | null) => selectExpandedStatus(state, id ?? undefined)],
(status) => {
@@ -68,6 +72,7 @@ export const StatusRedesign: React.FC<StatusRedesignProps> = ({
onOpen,
showThread,
headerContents,
variant = contextToVariant(contextType),
}) => {
// Select data from store
const { status, parent } = useAppSelector((state) =>
@@ -129,10 +134,20 @@ export const StatusRedesign: React.FC<StatusRedesignProps> = ({
);
}
const showFooter =
(expanded && hashtagsInBar.length > 0) ||
variant === 'page' ||
(showActions && !isQuotedPost);
return (
<StatusHotkeys
{...hotkeysProps}
className={classNames(classes.root)}
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}
@@ -188,21 +203,30 @@ export const StatusRedesign: React.FC<StatusRedesignProps> = ({
</StatusContent>
)}
<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}
/>
)}
{showActions && !isQuotedPost && (
<StatusActionBar
scrollKey={scrollKey}
statusId={status.id}
contextType={contextType}
withDismiss={withDismiss}
withCounters={withCounters}
/>
)}
</footer>
{variant === 'page' && (
<StatusMeta status={status} className={classes.meta} />
)}
{showActions && !isQuotedPost && (
<StatusActionBar
scrollKey={scrollKey}
statusId={status.id}
contextType={contextType}
withDismiss={withDismiss}
withCounters={withCounters}
/>
)}
</footer>
)}
</StatusHotkeys>
);
};
@@ -256,3 +280,17 @@ const StatusHotkeys = ({
</Hotkeys>
);
};
function contextToVariant(contextType?: StatusContextType): StatusVariant {
switch (contextType) {
case 'composer':
case 'detailed':
case 'notifications':
case undefined:
return 'page';
case 'thread':
return 'thread';
default:
return 'feed';
}
}

View File

@@ -3,13 +3,33 @@
.root {
--col-gap: var(--space-xs);
--avatar-size: 40px;
--content-col: 2;
display: grid;
grid-template-columns: calc(var(--avatar-size) + var(--col-gap)) auto;
grid-template-rows:
minmax(calc(var(--avatar-size) + var(--col-gap)), auto)
1fr auto;
grid-template-rows: minmax(calc(var(--avatar-size) + var(--col-gap)), auto);
grid-auto-rows: auto;
row-gap: var(--space-xs);
padding: var(--space-md);
}
.variantThread {
--avatar-size: 32px;
padding: var(--space-xs) 0;
}
.variantPage {
padding: 0;
}
.isQuote {
padding: var(--space-sm);
}
.variantPage,
.isQuote {
--content-col: span 2;
}
.header {
@@ -17,8 +37,10 @@
}
.content,
.footer {
grid-column: 2;
.footer,
.root :global(.status__prepend),
.root :global(.content-warning) {
grid-column: var(--content-col);
}
.content {
@@ -60,6 +82,12 @@
z-index: 1;
}
.meta {
@include mixins.type-micro;
color: var(--color-text-secondary);
}
.actions {
display: flex;

View File

@@ -1,8 +1,8 @@
import type { AccountStatusShape, StatusShape } from '@/mastodon/models/status';
import type { AnyStatusShape } from '@/mastodon/models/status';
export function statusLink({
account,
id,
}: Pick<StatusShape | AccountStatusShape, 'account' | 'id'>) {
}: Pick<AnyStatusShape, 'account' | 'id'>) {
return `/@${typeof account === 'string' ? account : account.acct}/${id}`;
}

View File

@@ -99,6 +99,7 @@ export const RedesignComposeForm: React.FC<
<FormattedMessage
id='compose.message.notice'
defaultMessage='Messages are not end-to-end encrypted'
description='Message refers to a direct message. For languages where this is confusing, "chat" or "direct message" can be used.'
/>
</p>
)}

View File

@@ -45,6 +45,7 @@ const ComposerModalSwitch: React.FC = () => {
<FormattedMessage
id='compose.switch_modal.body'
defaultMessage='Your message has limited visibility. If you convert to a post, it will switch to your default post visibility.'
description='Message refers to a direct message. For languages where this is confusing, "chat" or "direct message" can be used.'
/>
<ModalActions>

View File

@@ -1203,6 +1203,7 @@
"privacy.change": "Change post privacy",
"privacy.direct.long": "Everyone mentioned in the post",
"privacy.direct.short": "Private mention",
"privacy.message.short": "Message",
"privacy.private.long": "Only your followers",
"privacy.private.short": "Followers",
"privacy.public.long": "Anyone on and off Mastodon",
@@ -1375,6 +1376,7 @@
"status.media.show": "Click to show",
"status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}",
"status.meta": "{createdAt} on {source} • {visibility}",
"status.more": "More",
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",

View File

@@ -93,6 +93,11 @@ export type ExpandedStatusShape = Omit<AccountStatusShape, 'reblog'> & {
reblog?: Omit<ExpandedStatusShape, 'reblog'>;
};
export type AnyStatusShape =
| StatusShape
| AccountStatusShape
| ExpandedStatusShape;
export type CardShape = Omit<ApiPreviewCardJSON, 'authors'> & {
authors: (Omit<ApiPreviewCardAuthorJSON, 'author'> & {
accountId?: string;