Change design of quote posts in web UI (#35584)

This commit is contained in:
Eugen Rochko 2025-07-30 17:53:42 +02:00 committed by GitHub
parent 39250ab961
commit 92bf55afd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 210 additions and 109 deletions

View File

@ -0,0 +1,63 @@
import { useState, useRef, useCallback, useId } from 'react';
import { FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/Overlay';
export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const accessibilityId = useId();
const [open, setOpen] = useState(false);
const triggerRef = useRef(null);
const handleClick = useCallback(() => {
setOpen(!open);
}, [open, setOpen]);
return (
<>
<button
className='link-button'
ref={triggerRef}
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
>
<FormattedMessage
id='learn_more_link.learn_more'
defaultMessage='Learn more'
/>
</button>
<Overlay
show={open}
rootClose
onHide={handleClick}
offset={[5, 5]}
placement='bottom-end'
target={triggerRef}
>
{({ props }) => (
<div
{...props}
role='region'
id={accessibilityId}
className='account__domain-pill__popout learn-more__popout dropdown-animation'
>
<div className='learn-more__popout__content'>{children}</div>
<div>
<button className='link-button' onClick={handleClick}>
<FormattedMessage
id='learn_more_link.got_it'
defaultMessage='Got it'
/>
</button>
</div>
</div>
)}
</Overlay>
</>
);
};

View File

@ -138,6 +138,16 @@ class StatusContent extends PureComponent {
onCollapsedToggle(collapsed); onCollapsedToggle(collapsed);
} }
// Remove quote fallback link from the DOM so it doesn't
// mess with paragraph margins
if (!!status.get('quote')) {
const inlineQuote = node.querySelector('.quote-inline');
if (inlineQuote) {
inlineQuote.remove();
}
}
} }
handleMouseEnter = ({ currentTarget }) => { handleMouseEnter = ({ currentTarget }) => {

View File

@ -3,19 +3,15 @@ import { useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { Map as ImmutableMap } from 'immutable'; import type { Map as ImmutableMap } from 'immutable';
import ArticleIcon from '@/material-icons/400-24px/article.svg?react'; import { LearnMoreLink } from 'mastodon/components/learn_more_link';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { Icon } from 'mastodon/components/icon';
import StatusContainer from 'mastodon/containers/status_container'; import StatusContainer from 'mastodon/containers/status_container';
import type { Status } from 'mastodon/models/status'; import type { Status } from 'mastodon/models/status';
import type { RootState } from 'mastodon/store'; import type { RootState } from 'mastodon/store';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import QuoteIcon from '../../images/quote.svg?react';
import { fetchStatus } from '../actions/statuses'; import { fetchStatus } from '../actions/statuses';
import { makeGetStatus } from '../selectors'; import { makeGetStatus } from '../selectors';
@ -31,7 +27,6 @@ const QuoteWrapper: React.FC<{
'status__quote--error': isError, 'status__quote--error': isError,
})} })}
> >
<Icon id='quote' icon={QuoteIcon} className='status__quote-icon' />
{children} {children}
</div> </div>
); );
@ -45,27 +40,20 @@ const NestedQuoteLink: React.FC<{
accountId ? state.accounts.get(accountId) : undefined, accountId ? state.accounts.get(accountId) : undefined,
); );
const quoteAuthorName = account?.display_name_html; const quoteAuthorName = account?.acct;
if (!quoteAuthorName) { if (!quoteAuthorName) {
return null; return null;
} }
const quoteAuthorElement = (
<span dangerouslySetInnerHTML={{ __html: quoteAuthorName }} />
);
const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`;
return ( return (
<Link to={quoteUrl} className='status__quote-author-button'> <div className='status__quote-author-button'>
<FormattedMessage <FormattedMessage
id='status.quote_post_author' id='status.quote_post_author'
defaultMessage='Post by {name}' defaultMessage='Quoted a post by @{name}'
values={{ name: quoteAuthorElement }} values={{ name: quoteAuthorName }}
/> />
<Icon id='chevron_right' icon={ChevronRightIcon} /> </div>
<Icon id='article' icon={ArticleIcon} />
</Link>
); );
}; };
@ -112,39 +100,42 @@ export const QuotedStatus: React.FC<{
defaultMessage='Hidden due to one of your filters' defaultMessage='Hidden due to one of your filters'
/> />
); );
} else if (quoteState === 'deleted') {
quoteError = (
<FormattedMessage
id='status.quote_error.removed'
defaultMessage='This post was removed by its author.'
/>
);
} else if (quoteState === 'unauthorized') {
quoteError = (
<FormattedMessage
id='status.quote_error.unauthorized'
defaultMessage='This post cannot be displayed as you are not authorized to view it.'
/>
);
} else if (quoteState === 'pending') { } else if (quoteState === 'pending') {
quoteError = ( quoteError = (
<FormattedMessage <>
id='status.quote_error.pending_approval' <FormattedMessage
defaultMessage='This post is pending approval from the original author.' id='status.quote_error.pending_approval'
/> defaultMessage='Post pending'
/>
<LearnMoreLink>
<h6>
<FormattedMessage
id='status.quote_error.pending_approval_popout.title'
defaultMessage='Pending quote? Remain calm'
/>
</h6>
<p>
<FormattedMessage
id='status.quote_error.pending_approval_popout.body'
defaultMessage='Quotes shared across the Fediverse may take time to display, as different servers have different protocols.'
/>
</p>
</LearnMoreLink>
</>
); );
} else if (quoteState === 'rejected' || quoteState === 'revoked') { } else if (
!status ||
!quotedStatusId ||
quoteState === 'deleted' ||
quoteState === 'rejected' ||
quoteState === 'revoked' ||
quoteState === 'unauthorized'
) {
quoteError = ( quoteError = (
<FormattedMessage <FormattedMessage
id='status.quote_error.rejected' id='status.quote_error.not_available'
defaultMessage='This post cannot be displayed as the original author does not allow it to be quoted.' defaultMessage='Post unavailable'
/>
);
} else if (!status || !quotedStatusId) {
quoteError = (
<FormattedMessage
id='status.quote_error.not_found'
defaultMessage='This post cannot be displayed.'
/> />
); );
} }
@ -168,7 +159,7 @@ export const QuotedStatus: React.FC<{
isQuotedPost isQuotedPost
id={quotedStatusId} id={quotedStatusId}
contextType={contextType} contextType={contextType}
avatarSize={40} avatarSize={32}
> >
{canRenderChildQuote && ( {canRenderChildQuote && (
<QuotedStatus <QuotedStatus

View File

@ -498,6 +498,8 @@
"keyboard_shortcuts.translate": "to translate a post", "keyboard_shortcuts.translate": "to translate a post",
"keyboard_shortcuts.unfocus": "Unfocus compose textarea/search", "keyboard_shortcuts.unfocus": "Unfocus compose textarea/search",
"keyboard_shortcuts.up": "Move up in the list", "keyboard_shortcuts.up": "Move up in the list",
"learn_more_link.got_it": "Got it",
"learn_more_link.learn_more": "Learn more",
"lightbox.close": "Close", "lightbox.close": "Close",
"lightbox.next": "Next", "lightbox.next": "Next",
"lightbox.previous": "Previous", "lightbox.previous": "Previous",
@ -873,12 +875,11 @@
"status.open": "Expand this post", "status.open": "Expand this post",
"status.pin": "Pin on profile", "status.pin": "Pin on profile",
"status.quote_error.filtered": "Hidden due to one of your filters", "status.quote_error.filtered": "Hidden due to one of your filters",
"status.quote_error.not_found": "This post cannot be displayed.", "status.quote_error.not_available": "Post unavailable",
"status.quote_error.pending_approval": "This post is pending approval from the original author.", "status.quote_error.pending_approval": "Post pending",
"status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.", "status.quote_error.pending_approval_popout.body": "Quotes shared across the Fediverse may take time to display, as different servers have different protocols.",
"status.quote_error.removed": "This post was removed by its author.", "status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm",
"status.quote_error.unauthorized": "This post cannot be displayed as you are not authorized to view it.", "status.quote_post_author": "Quoted a post by @{name}",
"status.quote_post_author": "Post by {name}",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility", "status.reblog_private": "Boost with original visibility",

View File

@ -12,6 +12,8 @@ body {
--background-color: #fff; --background-color: #fff;
--background-color-tint: rgba(255, 255, 255, 80%); --background-color-tint: rgba(255, 255, 255, 80%);
--background-filter: blur(10px); --background-filter: blur(10px);
--surface-variant-background-color: #f1ebfb;
--surface-border-color: #cac4d0;
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)}; --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)};
--rich-text-container-color: rgba(255, 216, 231, 100%); --rich-text-container-color: rgba(255, 216, 231, 100%);
--rich-text-text-color: rgba(114, 47, 83, 100%); --rich-text-text-color: rgba(114, 47, 83, 100%);

View File

@ -1433,10 +1433,6 @@ body > [data-popper-placement] {
} }
} }
.status--has-quote .quote-inline {
display: none;
}
.status { .status {
padding: 16px; padding: 16px;
min-height: 54px; min-height: 54px;
@ -1470,10 +1466,6 @@ body > [data-popper-placement] {
margin-top: 16px; margin-top: 16px;
} }
&--is-quote {
border: none;
}
&--in-thread { &--in-thread {
--thread-margin: calc(46px + 8px); --thread-margin: calc(46px + 8px);
@ -1860,79 +1852,99 @@ body > [data-popper-placement] {
// --status-gutter-width is currently only set inside of // --status-gutter-width is currently only set inside of
// .notification-ungrouped, so everywhere else this will fall back // .notification-ungrouped, so everywhere else this will fall back
// to the pixel values // to the pixel values
--quote-margin: var(--status-gutter-width, 36px); --quote-margin: var(--status-gutter-width);
position: relative; position: relative;
margin-block-start: 16px; margin-block-start: 16px;
margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px)); margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px));
border-radius: 8px; border-radius: 12px;
color: var(--nested-card-text); color: var(--nested-card-text);
background: var(--nested-card-background); border: 1px solid var(--surface-border-color);
border: var(--nested-card-border);
@container (width > 460px) {
--quote-margin: var(--status-gutter-width, 56px);
}
} }
.status__quote--error { .status__quote--error {
box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 8px; gap: 8px;
padding: 12px; padding: 12px;
font-size: 15px; font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
min-height: 56px;
.link-button {
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
}
} }
.status__quote-author-button { .status__quote-author-button {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: inline-flex; display: flex;
width: auto; margin-top: 8px;
margin-block-start: 10px; padding: 8px 12px;
padding: 5px 12px;
align-items: center; align-items: center;
gap: 6px;
font-family: inherit; font-family: inherit;
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 400;
line-height: normal; line-height: 20px;
letter-spacing: 0; letter-spacing: 0.25px;
text-decoration: none; color: $darker-text-color;
color: $highlight-text-color; background: var(--surface-variant-background-color);
background: var(--nested-card-background); border-radius: 8px;
border: var(--nested-card-border); cursor: default;
border-radius: 4px;
&:active,
&:focus,
&:hover {
border-color: lighten($highlight-text-color, 4%);
color: lighten($highlight-text-color, 4%);
}
&:focus-visible {
outline: $ui-button-icon-focus-outline;
}
} }
.status__quote-icon { .status--is-quote {
position: absolute; border: none;
inset-block-start: 18px; padding: 12px;
inset-inline-start: -40px;
display: block;
width: 26px;
height: 26px;
padding: 5px;
color: #6a49ba;
z-index: 10;
.status__quote--error & { .status__info {
inset-block-start: 50%; padding-bottom: 8px;
transform: translateY(-50%);
} }
@container (width > 460px) { .display-name,
inset-inline-start: -50px; .status__relative-time {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.1px;
}
.display-name__account {
font-size: 12px;
line-height: 16px;
letter-spacing: 0.5px;
}
.status__content {
display: -webkit-box;
font-size: 14px;
letter-spacing: 0.25px;
line-height: 20px;
-webkit-line-clamp: 4;
line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
p {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
}
.media-gallery,
.video-player,
.audio-player,
.attachment-list,
.poll {
margin-top: 8px;
} }
} }
@ -2152,6 +2164,27 @@ body > [data-popper-placement] {
} }
} }
.learn-more__popout {
gap: 8px;
&__content {
display: flex;
flex-direction: column;
gap: 4px;
}
h6 {
font-size: inherit;
font-weight: 500;
line-height: inherit;
letter-spacing: 0.1px;
}
.link-button {
font-weight: 500;
}
}
.account__wrapper { .account__wrapper {
display: flex; display: flex;
gap: 10px; gap: 10px;

View File

@ -16,6 +16,7 @@
--surface-background-color: #{darken($ui-base-color, 4%)}; --surface-background-color: #{darken($ui-base-color, 4%)};
--surface-variant-background-color: #{$ui-base-color}; --surface-variant-background-color: #{$ui-base-color};
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)}; --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
--surface-border-color: #{lighten($ui-base-color, 8%)};
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)}; --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)};
--avatar-border-radius: 8px; --avatar-border-radius: 8px;
--media-outline-color: #{rgba(#fcf8ff, 0.15)}; --media-outline-color: #{rgba(#fcf8ff, 0.15)};