[Glitch] Add visual indicator & link to nested quote posts

Port 79ccba1758 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion
2025-05-22 12:39:45 +02:00
committed by Claire
parent e3b424aa02
commit 5061a32f1d
2 changed files with 107 additions and 11 deletions

View File

@@ -1,17 +1,23 @@
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 QuoteIcon from '@/images/quote.svg?react'; import QuoteIcon from '@/images/quote.svg?react';
import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import StatusContainer from 'flavours/glitch/containers/status_container'; import StatusContainer from 'flavours/glitch/containers/status_container';
import type { Status } from 'flavours/glitch/models/status';
import { useAppSelector } from 'flavours/glitch/store'; import { useAppSelector } from 'flavours/glitch/store';
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
@@ -25,16 +31,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 = (
@@ -77,14 +119,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>
); );
}; };

View File

@@ -1946,11 +1946,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 {
@@ -1961,10 +1965,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;
@@ -1976,6 +2012,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 {