From 92bf55afd08adadae40469ff9f6b6cc76aaf36ca Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 30 Jul 2025 17:53:42 +0200 Subject: [PATCH] Change design of quote posts in web UI (#35584) --- .../mastodon/components/learn_more_link.tsx | 63 ++++++++ .../mastodon/components/status_content.jsx | 10 ++ .../mastodon/components/status_quoted.tsx | 85 +++++----- app/javascript/mastodon/locales/en.json | 13 +- .../styles/mastodon-light/css_variables.scss | 2 + .../styles/mastodon/components.scss | 145 +++++++++++------- .../styles/mastodon/css_variables.scss | 1 + 7 files changed, 210 insertions(+), 109 deletions(-) create mode 100644 app/javascript/mastodon/components/learn_more_link.tsx diff --git a/app/javascript/mastodon/components/learn_more_link.tsx b/app/javascript/mastodon/components/learn_more_link.tsx new file mode 100644 index 00000000000..b5337794c95 --- /dev/null +++ b/app/javascript/mastodon/components/learn_more_link.tsx @@ -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 ( + <> + + + + {({ props }) => ( +
+
{children}
+ +
+ +
+
+ )} +
+ + ); +}; diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 5f0f7079aec..38d24921c5e 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -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 }) => { diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index d3d8b58c33d..8d43ea1819a 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -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, })} > - {children} ); @@ -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 = ( - - ); - const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`; - return ( - +
- - - +
); }; @@ -112,39 +100,42 @@ export const QuotedStatus: React.FC<{ defaultMessage='Hidden due to one of your filters' /> ); - } else if (quoteState === 'deleted') { - quoteError = ( - - ); - } else if (quoteState === 'unauthorized') { - quoteError = ( - - ); } else if (quoteState === 'pending') { quoteError = ( - + <> + + + +
+ +
+

+ +

+
+ ); - } else if (quoteState === 'rejected' || quoteState === 'revoked') { + } else if ( + !status || + !quotedStatusId || + quoteState === 'deleted' || + quoteState === 'rejected' || + quoteState === 'revoked' || + quoteState === 'unauthorized' + ) { quoteError = ( - ); - } else if (!status || !quotedStatusId) { - quoteError = ( - ); } @@ -168,7 +159,7 @@ export const QuotedStatus: React.FC<{ isQuotedPost id={quotedStatusId} contextType={contextType} - avatarSize={40} + avatarSize={32} > {canRenderChildQuote && ( [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; diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss index 431cdd7a8e8..16ed033b968 100644 --- a/app/javascript/styles/mastodon/css_variables.scss +++ b/app/javascript/styles/mastodon/css_variables.scss @@ -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)};