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 {