Merge commit '02de05dc272856c77fac5c9e1d84cf585ae20c1e' into glitch-soc/merge-upstream

This commit is contained in:
Claire committed 2025-08-27 18:13:03 +02:00
commit 884d7996b1
50 files changed
+653 -96

No files matched your search

@@ -0,0 +1,113 @@
import { useCallback, useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { List as ImmutableList } from 'immutable';
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
import { fetchQuotes } from 'mastodon/actions/interactions_typed';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import StatusList from 'mastodon/components/status_list';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import Column from '../ui/components/column';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
});
const emptyList = ImmutableList();
export const Quotes: React.FC<{
multiColumn?: boolean;
params?: { statusId: string };
}> = ({ multiColumn, params }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const statusId = params?.statusId;
const isCorrectStatusId: boolean = useAppSelector(
(state) => state.status_lists.getIn(['quotes', 'statusId']) === statusId,
);
const statusIds = useAppSelector((state) =>
state.status_lists.getIn(['quotes', 'items'], emptyList),
);
const nextUrl = useAppSelector(
(state) =>
state.status_lists.getIn(['quotes', 'next']) as string | undefined,
);
const isLoading = useAppSelector((state) =>
state.status_lists.getIn(['quotes', 'isLoading'], true),
);
const hasMore = !!nextUrl;
useEffect(() => {
if (statusId) void dispatch(fetchQuotes({ statusId }));
}, [dispatch, statusId]);
const handleLoadMore = useCallback(() => {
if (statusId && isCorrectStatusId && nextUrl)
void dispatch(fetchQuotes({ statusId, next: nextUrl }));
}, [dispatch, statusId, isCorrectStatusId, nextUrl]);
const handleRefresh = useCallback(() => {
if (statusId) void dispatch(fetchQuotes({ statusId }));
}, [dispatch, statusId]);
if (!statusIds || !isCorrectStatusId) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
const emptyMessage = (
<FormattedMessage
id='status.quotes.empty'
defaultMessage='No one has quoted this post yet. When someone does, it will show up here.'
/>
);
return (
<Column bindToDocument={!multiColumn}>
<ColumnHeader
showBackButton
multiColumn={multiColumn}
extraButton={
<button
type='button'
className='column-header__button'
title={intl.formatMessage(messages.refresh)}
aria-label={intl.formatMessage(messages.refresh)}
onClick={handleRefresh}
>
<Icon id='refresh' icon={RefreshIcon} />
</button>
}
/>
<StatusList
scrollKey='quotes_timeline'
statusIds={statusIds}
onLoadMore={handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default Quotes;
@@ -32,7 +32,7 @@ const Embed: React.FC<{ id: string }> = ({ id }) => {
const dispatchRenderSignal = useRenderSignal();
useEffect(() => {
dispatch(fetchStatus(id, false, false));
dispatch(fetchStatus(id, { alsoFetchContext: false }));
}, [dispatch, id]);
const handleToggleHidden = useCallback(() => {
@@ -31,6 +31,7 @@ import { VisibilityIcon } from 'mastodon/components/visibility_icon';
import { Audio } from 'mastodon/features/audio';
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
import { Video } from 'mastodon/features/video';
import { me } from 'mastodon/initial_state';
import Card from './card';
@@ -282,6 +283,22 @@ export const DetailedStatus: React.FC<{
if (['private', 'direct'].includes(status.get('visibility') as string)) {
quotesLink = '';
} else if (status.getIn(['account', 'id']) === me) {
quotesLink = (
<Link
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/quotes`}
className='detailed-status__link'
>
<span className='detailed-status__quotes'>
<AnimatedNumber value={status.get('quotes_count')} />
</span>
<FormattedMessage
id='status.quotes'
defaultMessage='{count, plural, one {quote} other {quotes}}'
values={{ count: status.get('quotes_count') }}
/>
</Link>
);
} else {
quotesLink = (
<span className='detailed-status__link'>
@@ -399,7 +416,10 @@ export const DetailedStatus: React.FC<{
{hashtagBar}
{status.get('quote') && (
<QuotedStatus quote={status.get('quote')} />
<QuotedStatus
quote={status.get('quote')}
parentQuotePostId={status.get('id')}
/>
)}
</>
)}
@@ -38,7 +38,7 @@ class FilterModal extends ImmutablePureComponent {
handleSuccess = () => {
const { dispatch, statusId } = this.props;
dispatch(fetchStatus(statusId, true));
dispatch(fetchStatus(statusId, {forceFetch: true}));
this.setState({ isSubmitting: false, isSubmitted: true, step: 'submitted' });
};
@@ -75,6 +75,7 @@ import {
PrivacyPolicy,
TermsOfService,
AccountFeatured,
Quotes,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { focusColumn, getFocusedItemIndex, focusItemSibling } from './util/focusUtils';
@@ -209,6 +210,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
<WrappedRoute path='/@:acct/:statusId/quotes' component={Quotes} content={children} />
{/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
@@ -86,6 +86,10 @@ export function Favourites () {
return import('../../favourites');
}
export function Quotes () {
return import('../../quotes');
}
export function FollowRequests () {
return import('../../follow_requests');
}