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);
}
// 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 }) => {

View File

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

View File

@ -498,6 +498,8 @@
"keyboard_shortcuts.translate": "to translate a post",
"keyboard_shortcuts.unfocus": "Unfocus compose textarea/search",
"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.next": "Next",
"lightbox.previous": "Previous",
@ -873,12 +875,11 @@
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.quote_error.filtered": "Hidden due to one of your filters",
"status.quote_error.not_found": "This post cannot be displayed.",
"status.quote_error.pending_approval": "This post is pending approval from the original author.",
"status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.",
"status.quote_error.removed": "This post was removed by its author.",
"status.quote_error.unauthorized": "This post cannot be displayed as you are not authorized to view it.",
"status.quote_post_author": "Post by {name}",
"status.quote_error.not_available": "Post unavailable",
"status.quote_error.pending_approval": "Post pending",
"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.pending_approval_popout.title": "Pending quote? Remain calm",
"status.quote_post_author": "Quoted a post by @{name}",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility",

View File

@ -12,6 +12,8 @@ body {
--background-color: #fff;
--background-color-tint: rgba(255, 255, 255, 80%);
--background-filter: blur(10px);
--surface-variant-background-color: #f1ebfb;
--surface-border-color: #cac4d0;
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)};
--rich-text-container-color: rgba(255, 216, 231, 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 {
padding: 16px;
min-height: 54px;
@ -1470,10 +1466,6 @@ body > [data-popper-placement] {
margin-top: 16px;
}
&--is-quote {
border: none;
}
&--in-thread {
--thread-margin: calc(46px + 8px);
@ -1860,79 +1852,99 @@ body > [data-popper-placement] {
// --status-gutter-width is currently only set inside of
// .notification-ungrouped, so everywhere else this will fall back
// to the pixel values
--quote-margin: var(--status-gutter-width, 36px);
--quote-margin: var(--status-gutter-width);
position: relative;
margin-block-start: 16px;
margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px));
border-radius: 8px;
border-radius: 12px;
color: var(--nested-card-text);
background: var(--nested-card-background);
border: var(--nested-card-border);
@container (width > 460px) {
--quote-margin: var(--status-gutter-width, 56px);
}
border: 1px solid var(--surface-border-color);
}
.status__quote--error {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
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 {
position: relative;
overflow: hidden;
display: inline-flex;
width: auto;
margin-block-start: 10px;
padding: 5px 12px;
display: flex;
margin-top: 8px;
padding: 8px 12px;
align-items: center;
gap: 6px;
font-family: inherit;
font-size: 14px;
font-weight: 700;
line-height: normal;
letter-spacing: 0;
text-decoration: none;
color: $highlight-text-color;
background: var(--nested-card-background);
border: var(--nested-card-border);
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;
}
font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px;
color: $darker-text-color;
background: var(--surface-variant-background-color);
border-radius: 8px;
cursor: default;
}
.status__quote-icon {
position: absolute;
inset-block-start: 18px;
inset-inline-start: -40px;
display: block;
width: 26px;
height: 26px;
padding: 5px;
color: #6a49ba;
z-index: 10;
.status--is-quote {
border: none;
padding: 12px;
.status__quote--error & {
inset-block-start: 50%;
transform: translateY(-50%);
.status__info {
padding-bottom: 8px;
}
@container (width > 460px) {
inset-inline-start: -50px;
.display-name,
.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 {
display: flex;
gap: 10px;

View File

@ -16,6 +16,7 @@
--surface-background-color: #{darken($ui-base-color, 4%)};
--surface-variant-background-color: #{$ui-base-color};
--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)};
--avatar-border-radius: 8px;
--media-outline-color: #{rgba(#fcf8ff, 0.15)};