diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index eea60c3ce7..2f1354c27a 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -1,18 +1,24 @@ 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 StatusContainer from 'mastodon/containers/status_container'; +import type { Status } from 'mastodon/models/status'; import { useAppSelector } from 'mastodon/store'; import QuoteIcon from '../../images/quote.svg?react'; +const MAX_QUOTE_POSTS_NESTING_LEVEL = 1; + const QuoteWrapper: React.FC<{ isError?: boolean; - children: React.ReactNode; + children: React.ReactElement; }> = ({ isError, children }) => { return (
= ({ status }) => { + const accountId = status.get('account') as string; + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + + const quoteAuthorName = account?.display_name_html; + + if (!quoteAuthorName) { + return null; + } + + const quoteAuthorElement = ( + + ); + const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`; + + return ( + + + + + + ); +}; + type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>; -export const QuotedStatus: React.FC<{ quote: QuoteMap }> = ({ quote }) => { +export const QuotedStatus: React.FC<{ + quote: QuoteMap; + variant?: 'full' | 'link'; + nestingLevel?: number; +}> = ({ quote, nestingLevel = 1, variant = 'full' }) => { const quotedStatusId = quote.get('quoted_status'); const state = quote.get('state'); const status = useAppSelector((state) => quotedStatusId ? state.statuses.get(quotedStatusId) : undefined, ); - let quoteError: React.ReactNode | null = null; + let quoteError: React.ReactNode = null; if (state === 'deleted') { quoteError = ( @@ -78,14 +120,28 @@ export const QuotedStatus: React.FC<{ quote: QuoteMap }> = ({ quote }) => { return {quoteError}; } + if (variant === 'link' && status) { + return ; + } + + const childQuote = status?.get('quote') as QuoteMap | undefined; + const canRenderChildQuote = + childQuote && nestingLevel <= MAX_QUOTE_POSTS_NESTING_LEVEL; + return ( - + {/* @ts-expect-error Status is not yet typed */} + + {canRenderChildQuote && ( + + )} + ); }; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 9bcf79afbe..9882691a20 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -868,6 +868,7 @@ "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.read_more": "Read more", "status.reblog": "Boost", "status.reblog_private": "Boost with original visibility", diff --git a/app/javascript/material-icons/400-24px/article-fill.svg b/app/javascript/material-icons/400-24px/article-fill.svg new file mode 100644 index 0000000000..5ea367df92 --- /dev/null +++ b/app/javascript/material-icons/400-24px/article-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/article.svg b/app/javascript/material-icons/400-24px/article.svg new file mode 100644 index 0000000000..1265c26dad --- /dev/null +++ b/app/javascript/material-icons/400-24px/article.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/README.md b/app/javascript/material-icons/README.md index 1479cb2255..c583d5ee2c 100644 --- a/app/javascript/material-icons/README.md +++ b/app/javascript/material-icons/README.md @@ -1 +1,12 @@ -Files in this directory are Material Symbols icons fetched using the `icons:download` task. +Files in this directory are Material Symbols icons fetched using the `icons:download` rake task (see `/lib/tasks/icons.rake`). + +To add another icon, follow these steps: + +- Determine the name of the Material Symbols icon you want to download. + You can find a searchable overview of all icons on [https://fonts.google.com/icons]. + Click on the icon you want to use and find the icon name towards the bottom of the slide-out panel (it'll be something like `icon_name`) +- Import the icon in your React component using the following format: + `import IconName from '@/material-icons/400-24px/icon_name.svg?react';` +- Run `RAILS_ENV=development rails icons:download` to download any newly imported icons. + +The import should now work and the icon should appear when passed to the ` component diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index bc5625167e..9c38f5685b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1880,11 +1880,15 @@ body > [data-popper-placement] { .status__quote { position: relative; margin-block-start: 16px; - margin-inline-start: 56px; + margin-inline-start: 36px; border-radius: 8px; color: var(--nested-card-text); background: var(--nested-card-background); border: var(--nested-card-border); + + @media screen and (min-width: $mobile-breakpoint) { + margin-inline-start: 56px; + } } .status__quote--error { @@ -1895,10 +1899,42 @@ body > [data-popper-placement] { font-size: 15px; } +.status__quote-author-button { + position: relative; + overflow: hidden; + display: inline-flex; + width: auto; + margin-block-start: 10px; + padding: 5px 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; + } +} + .status__quote-icon { position: absolute; inset-block-start: 18px; - inset-inline-start: -50px; + inset-inline-start: -40px; display: block; width: 26px; height: 26px; @@ -1910,6 +1946,10 @@ body > [data-popper-placement] { inset-block-start: 50%; transform: translateY(-50%); } + + @media screen and (min-width: $mobile-breakpoint) { + inset-inline-start: -50px; + } } .detailed-status__link {