diff --git a/app/javascript/mastodon/api_types/media_attachments.ts b/app/javascript/mastodon/api_types/media_attachments.ts index 8cb798f7604..7385125487e 100644 --- a/app/javascript/mastodon/api_types/media_attachments.ts +++ b/app/javascript/mastodon/api_types/media_attachments.ts @@ -31,7 +31,7 @@ export interface ApiImageAttachmentJSON extends BaseApiMediaAttachmentJSON { export interface ApiAudioAttachmentJSON extends BaseApiMediaAttachmentJSON { type: 'audio'; meta: { - colors: ApiColorsAttachmentMetaJSON; + colors?: ApiColorsAttachmentMetaJSON; original: ApiVideoAttachmentMetaJSON; small: ApiImageAttachmentMetaJSON; }; @@ -40,7 +40,7 @@ export interface ApiAudioAttachmentJSON extends BaseApiMediaAttachmentJSON { export interface ApiVideoAttachmentJSON extends BaseApiMediaAttachmentJSON { type: 'video'; meta: { - colors: ApiColorsAttachmentMetaJSON; + colors?: ApiColorsAttachmentMetaJSON; original: ApiVideoAttachmentMetaJSON; small: ApiImageAttachmentMetaJSON; focus?: ApiFocusAttachmentMetaJSON; diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index eeea446d0c6..f33b091bcd9 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -15,7 +15,7 @@ interface Props { 'id' | 'acct' | 'avatar' | 'avatar_static' >; alt?: string; - size?: number; + size?: number | null; style?: React.CSSProperties; inline?: boolean; animate?: boolean; @@ -41,11 +41,14 @@ export const Avatar: React.FC = ({ const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const style = { - ...styleFromParent, - width: `${size}px`, - height: `${size}px`, - }; + const style = + size !== null + ? { + ...styleFromParent, + width: `${size}px`, + height: `${size}px`, + } + : styleFromParent; const src = hovering || animate ? account?.avatar : account?.avatar_static; diff --git a/app/javascript/mastodon/components/emoji/html.tsx b/app/javascript/mastodon/components/emoji/html.tsx index 1f392548de0..4d2de6dd15c 100644 --- a/app/javascript/mastodon/components/emoji/html.tsx +++ b/app/javascript/mastodon/components/emoji/html.tsx @@ -31,7 +31,7 @@ export const EmojiHTML = < onElement, onAttribute, extraArgs, - ref, + children, ...props }: PolymorphicProps, As>) => { const contents = useMemo( @@ -47,8 +47,10 @@ export const EmojiHTML = < return ( - + {contents} + + {children} ); diff --git a/app/javascript/mastodon/components/status/action_bar.tsx b/app/javascript/mastodon/components/status/action_bar.tsx index d92a3775f67..42654e03a7b 100644 --- a/app/javascript/mastodon/components/status/action_bar.tsx +++ b/app/javascript/mastodon/components/status/action_bar.tsx @@ -3,6 +3,8 @@ import { useCallback, useMemo } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import classNames from 'classnames'; + import { ArrowsClockwiseIcon, BookmarkSimpleIcon, @@ -202,7 +204,7 @@ export const StatusActionBar: React.FC = ({ isQuotingMe && contextType === 'notifications'; return ( -
+
- ); - - const renderTranslate = - !!onTranslate && + const isCollapsed = !!onReadMore && collapsible && collapsed; + const renderTranslate = !!( + onTranslate && + !isCollapsed && signedIn && ['public', 'unlisted'].includes(status.visibility) && - status.search_index && - status.search_index.trim().length > 0 && - targetLanguages?.includes(intl.locale.replace(/[_-].*/, '')); - const translateButton = renderTranslate && ( - + status.search_index?.trim().length && + targetLanguages?.includes(intl.locale.replace(/[_-].*/, '')) ); - const poll = !!status.poll && ( - - ); - - const content = ( + return ( + > + {children} + + {renderTranslate && ( + + )} + + {isCollapsed && ( + + )} + ); - - const classNames = classnames('status__content', { - 'status__content--with-action': onClick, - 'status__content--collapsed': renderReadMore, - }); - - if (!onClick) { - return ( -
- {content} - {poll} - {translateButton} -
- ); - } - - /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ - return ( - <> -
- {content} - {poll} - {translateButton} -
- - {readMoreButton} - - ); - /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ }; const TranslateButton: React.FC<{ - onClick: React.MouseEventHandler; + onTranslate: React.MouseEventHandler; translation?: StatusTranslation; -}> = ({ translation, onClick }) => { +}> = ({ translation, onTranslate }) => { if (!translation) { return ( - + ); } @@ -181,12 +146,12 @@ const TranslateButton: React.FC<{ return (
- +
{ - // Only handle clicks on the empty space above the content - if (event.target !== event.currentTarget && event.detail >= 1) { - return; - } - - onOpenClick(event); - }, - [onOpenClick], - ); - const acct = status?.account.acct; const onOpenProfile = useCallback(() => { if (acct) { @@ -190,7 +178,6 @@ export function useStatusHandlers({ onOpenClick, onExpandedToggle, onFilterToggle, - onHeaderClick, onMention, onOpen: () => { onOpenCallback(); @@ -208,7 +195,6 @@ export function useStatusHandlers({ handlerFactory, onExpandedToggle, onFilterToggle, - onHeaderClick, onMention, onOpenCallback, onOpenClick, diff --git a/app/javascript/mastodon/components/status/redesign/header.tsx b/app/javascript/mastodon/components/status/redesign/header.tsx index da7f0021766..02b451eaa87 100644 --- a/app/javascript/mastodon/components/status/redesign/header.tsx +++ b/app/javascript/mastodon/components/status/redesign/header.tsx @@ -18,14 +18,12 @@ interface StatusRedesignHeaderProps { status: Pick; children?: React.ReactNode; className?: string; - avatarSize?: number; } export const StatusRedesignHeader: React.FC = ({ status, children, className, - avatarSize = 40, }) => { const account = status.account; const handle = useAccountHandle(account); @@ -44,11 +42,16 @@ export const StatusRedesignHeader: React.FC = ({ return (
- - + + -
+

span { + display: block; + width: var(--avatar-size); + height: var(--avatar-size); + } } .headerName { diff --git a/app/javascript/mastodon/components/status/status.stories.tsx b/app/javascript/mastodon/components/status/status.stories.tsx index ce30f9e42b8..77083b34ca8 100644 --- a/app/javascript/mastodon/components/status/status.stories.tsx +++ b/app/javascript/mastodon/components/status/status.stories.tsx @@ -21,6 +21,7 @@ import type { StatusContextType } from './types'; interface StatusStoryProps extends AttachmentArgs { // Contents text: string; + tags: string; visibility: StatusVisibility; isReblog?: boolean; isReply?: boolean; @@ -169,6 +170,7 @@ const meta = { isPoll: categoryContents, isQuote: categoryContents, text: categoryContents, + tags: categoryContents, attachment1: { ...categoryContents, ...attachmentArgTypes.attachment1, @@ -232,6 +234,7 @@ const meta = { }, args: { text: 'This is a status', + tags: '', visibility: 'public', isReblog: false, isReply: false, @@ -283,7 +286,8 @@ const meta = { }, }, stateFn({ - text, + text: textBase, + tags: tagsStr, contentWarning, visibility, attachment1, @@ -301,6 +305,28 @@ const meta = { }: StatusStoryProps) { const account = accountFactoryImmutable(); + const tags = tagsStr + .split(',') + .map((tagStr) => { + const tag = tagStr.trim().replace(/^#/, ''); + if (!tag) { + return null; + } + return { + name: tag, + url: `https://example.com/tags/${tag}`, + }; + }) + .filter((tag) => !!tag); + + let text = textBase.trim(); + if (tags.length > 0) { + const tagText = tags + .map((tag) => ``) + .join(' '); + text += `\n${tagText}`; + } + const status = statusFactoryImmutable({ text, spoiler_text: contentWarning, @@ -310,6 +336,7 @@ const meta = { attachment2, attachment3, ), + tags, reblogged: hasReblogged, favourited: hasFavourited, bookmarked: hasBookmarked, diff --git a/app/javascript/mastodon/components/status/status.tsx b/app/javascript/mastodon/components/status/status.tsx index 9a8ee6f116d..014375f8369 100644 --- a/app/javascript/mastodon/components/status/status.tsx +++ b/app/javascript/mastodon/components/status/status.tsx @@ -12,6 +12,7 @@ import { ContentWarning } from '../content_warning'; import { FilterWarning } from '../filter_warning'; import { computeHashtagBarForStatus, HashtagBar } from '../hashtag_bar'; import { Hotkeys } from '../hotkeys'; +import { Poll } from '../poll'; import { StatusActionBar } from './action_bar'; import { StatusAttachments } from './attachments'; @@ -53,8 +54,6 @@ const selectStatusReblog = createAppSelector( export const StatusRedesign: React.FC = ({ id, muted, - rootId, - unread, skipPrepend, unfocusable, contextType, @@ -64,7 +63,6 @@ export const StatusRedesign: React.FC = ({ showActions = true, scrollKey, children, - avatarSize = 40, withCounters, withDismiss, onOpen, @@ -86,7 +84,7 @@ export const StatusRedesign: React.FC = ({ reblogAcct: parent?.account.acct, isQuote: isQuotedPost, }); - const { statusContent, hashtagsInBar } = useMemo( + const { statusContent, hashtagsInBar = [] } = useMemo( (): Partial> => status ? computeHashtagBarForStatus(status) : {}, [status], @@ -95,10 +93,8 @@ export const StatusRedesign: React.FC = ({ // Handlers const { showDespiteFilter, - onHeaderClick, onExpandedToggle, onFilterToggle, - onOpenClick, onTranslate, ...handlers } = useStatusHandlers({ status, contextType, onOpen }); @@ -125,10 +121,7 @@ export const StatusRedesign: React.FC = ({ if (hidden) { return ( - + {status.account.display_name || status.account.username} {status.spoiler_text && {status.spoiler_text}} {expanded && {status.content}} @@ -139,17 +132,7 @@ export const StatusRedesign: React.FC = ({ return ( = ({ /> )} - + {headerContents} @@ -183,37 +166,43 @@ export const StatusRedesign: React.FC = ({ )} {expanded && ( - <> - - - - - {hashtagsInBar && ( - + {!!status.poll && ( + )} + + {children} - + )} - {showActions && !isQuotedPost && ( - - )} +

); }; diff --git a/app/javascript/mastodon/components/status/styles.module.scss b/app/javascript/mastodon/components/status/styles.module.scss index 4403dab98ee..7bb405182e9 100644 --- a/app/javascript/mastodon/components/status/styles.module.scss +++ b/app/javascript/mastodon/components/status/styles.module.scss @@ -1,12 +1,67 @@ +@use '@/styles/mastodon/mixins'; + .root { - display: flex; - flex-direction: column; - gap: var(--space-xs); + --col-gap: var(--space-xs); + --avatar-size: 40px; + + 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; + row-gap: var(--space-xs); +} + +.header { + grid-column: span 2; +} + +.content, +.footer { + grid-column: 2; +} + +.content { + @include mixins.type-body; + + p:not(:last-child) { + margin-block-end: 1em; + } + + a { + color: var(--color-text-brand); + } + + &.collapsed { + position: relative; + max-height: 15lh; + overflow-y: clip; + + &::after { + content: ''; + position: absolute; + inset-inline: 0; + inset-block-end: 0; + height: calc(var(--space-4xl) * 2); + background: linear-gradient( + to bottom, + transparent, + var(--color-bg-blend) + ); + pointer-events: none; + } + } +} + +.contentReadMore { + position: absolute; + inset-block-end: 0; + inset-inline-start: 0; + z-index: 1; } .actions { display: flex; - margin-inline-start: calc(-1 * var(--space-sm)); button:not(:hover, :active) { opacity: 0.7; @@ -16,3 +71,7 @@ .actionsButtonGap { margin-inline-end: auto; } + +.buttonAlign { + margin-inline-start: calc(-1 * var(--space-sm)); +}