Add visual indicator & link to nested quote posts (#34766)
This commit is contained in:
parent
72356bd5ec
commit
79ccba1758
6 changed files with 122 additions and 12 deletions
|
@ -1,18 +1,24 @@
|
||||||
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 ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
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 { useAppSelector } from 'mastodon/store';
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import QuoteIcon from '../../images/quote.svg?react';
|
import QuoteIcon from '../../images/quote.svg?react';
|
||||||
|
|
||||||
|
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
|
||||||
|
|
||||||
const QuoteWrapper: React.FC<{
|
const QuoteWrapper: React.FC<{
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactElement;
|
||||||
}> = ({ isError, children }) => {
|
}> = ({ isError, children }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -26,16 +32,52 @@ const QuoteWrapper: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const QuoteLink: React.FC<{
|
||||||
|
status: Status;
|
||||||
|
}> = ({ 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 = (
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: quoteAuthorName }} />
|
||||||
|
);
|
||||||
|
const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={quoteUrl} className='status__quote-author-button'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.quote_post_author'
|
||||||
|
defaultMessage='Post by {name}'
|
||||||
|
values={{ name: quoteAuthorElement }}
|
||||||
|
/>
|
||||||
|
<Icon id='chevron_right' icon={ChevronRightIcon} />
|
||||||
|
<Icon id='article' icon={ArticleIcon} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>;
|
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 quotedStatusId = quote.get('quoted_status');
|
||||||
const state = quote.get('state');
|
const state = quote.get('state');
|
||||||
const status = useAppSelector((state) =>
|
const status = useAppSelector((state) =>
|
||||||
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
|
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
let quoteError: React.ReactNode | null = null;
|
let quoteError: React.ReactNode = null;
|
||||||
|
|
||||||
if (state === 'deleted') {
|
if (state === 'deleted') {
|
||||||
quoteError = (
|
quoteError = (
|
||||||
|
@ -78,14 +120,28 @@ export const QuotedStatus: React.FC<{ quote: QuoteMap }> = ({ quote }) => {
|
||||||
return <QuoteWrapper isError>{quoteError}</QuoteWrapper>;
|
return <QuoteWrapper isError>{quoteError}</QuoteWrapper>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (variant === 'link' && status) {
|
||||||
|
return <QuoteLink status={status} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childQuote = status?.get('quote') as QuoteMap | undefined;
|
||||||
|
const canRenderChildQuote =
|
||||||
|
childQuote && nestingLevel <= MAX_QUOTE_POSTS_NESTING_LEVEL;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QuoteWrapper>
|
<QuoteWrapper>
|
||||||
<StatusContainer
|
{/* @ts-expect-error Status is not yet typed */}
|
||||||
// @ts-expect-error Status isn't typed yet
|
<StatusContainer isQuotedPost id={quotedStatusId} avatarSize={40}>
|
||||||
isQuotedPost
|
{canRenderChildQuote && (
|
||||||
id={quotedStatusId}
|
<QuotedStatus
|
||||||
avatarSize={40}
|
quote={childQuote}
|
||||||
/>
|
variant={
|
||||||
|
nestingLevel === MAX_QUOTE_POSTS_NESTING_LEVEL ? 'link' : 'full'
|
||||||
|
}
|
||||||
|
nestingLevel={nestingLevel + 1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StatusContainer>
|
||||||
</QuoteWrapper>
|
</QuoteWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.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.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_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.read_more": "Read more",
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
"status.reblog_private": "Boost with original visibility",
|
"status.reblog_private": "Boost with original visibility",
|
||||||
|
|
1
app/javascript/material-icons/400-24px/article-fill.svg
Normal file
1
app/javascript/material-icons/400-24px/article-fill.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm80-160h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Z"/></svg>
|
After Width: | Height: | Size: 292 B |
1
app/javascript/material-icons/400-24px/article.svg
Normal file
1
app/javascript/material-icons/400-24px/article.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-280h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Zm-80 480q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z"/></svg>
|
After Width: | Height: | Size: 331 B |
|
@ -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 `<Icon icon={IconName} /> component
|
||||||
|
|
|
@ -1880,11 +1880,15 @@ body > [data-popper-placement] {
|
||||||
.status__quote {
|
.status__quote {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-block-start: 16px;
|
margin-block-start: 16px;
|
||||||
margin-inline-start: 56px;
|
margin-inline-start: 36px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--nested-card-text);
|
color: var(--nested-card-text);
|
||||||
background: var(--nested-card-background);
|
background: var(--nested-card-background);
|
||||||
border: var(--nested-card-border);
|
border: var(--nested-card-border);
|
||||||
|
|
||||||
|
@media screen and (min-width: $mobile-breakpoint) {
|
||||||
|
margin-inline-start: 56px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__quote--error {
|
.status__quote--error {
|
||||||
|
@ -1895,10 +1899,42 @@ body > [data-popper-placement] {
|
||||||
font-size: 15px;
|
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 {
|
.status__quote-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-block-start: 18px;
|
inset-block-start: 18px;
|
||||||
inset-inline-start: -50px;
|
inset-inline-start: -40px;
|
||||||
display: block;
|
display: block;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
|
@ -1910,6 +1946,10 @@ body > [data-popper-placement] {
|
||||||
inset-block-start: 50%;
|
inset-block-start: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $mobile-breakpoint) {
|
||||||
|
inset-inline-start: -50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status__link {
|
.detailed-status__link {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue