mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-12 18:16:10 -05:00
Status redesign: Fix padding and adds variants (#40400)
This commit is contained in:
103
app/javascript/mastodon/components/status/meta.tsx
Normal file
103
app/javascript/mastodon/components/status/meta.tsx
Normal 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' />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user