mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-05 17:31:12 +00:00
Add ability to list quotes of one's own posts (#35914)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Crowdin / Upload translations / upload-translations (push) Waiting to run
Check formatting / lint (push) Waiting to run
CSS Linting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Haml Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Some checks failed
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Crowdin / Upload translations / upload-translations (push) Waiting to run
Check formatting / lint (push) Waiting to run
CSS Linting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Haml Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
This commit is contained in:
parent
9c55b2fbe4
commit
02de05dc27
|
@ -2,11 +2,12 @@ import {
|
||||||
apiReblog,
|
apiReblog,
|
||||||
apiUnreblog,
|
apiUnreblog,
|
||||||
apiRevokeQuote,
|
apiRevokeQuote,
|
||||||
|
apiGetQuotes,
|
||||||
} from 'mastodon/api/interactions';
|
} from 'mastodon/api/interactions';
|
||||||
import type { StatusVisibility } from 'mastodon/models/status';
|
import type { StatusVisibility } from 'mastodon/models/status';
|
||||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
import { importFetchedStatus } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const reblog = createDataLoadingThunk(
|
export const reblog = createDataLoadingThunk(
|
||||||
'status/reblog',
|
'status/reblog',
|
||||||
|
@ -53,3 +54,19 @@ export const revokeQuote = createDataLoadingThunk(
|
||||||
return discardLoadData;
|
return discardLoadData;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fetchQuotes = createDataLoadingThunk(
|
||||||
|
'status/fetch_quotes',
|
||||||
|
async ({ statusId, next }: { statusId: string; next?: string }) => {
|
||||||
|
const { links, statuses } = await apiGetQuotes(statusId, next);
|
||||||
|
|
||||||
|
return {
|
||||||
|
links,
|
||||||
|
statuses,
|
||||||
|
replace: !next,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(payload, { dispatch }) => {
|
||||||
|
dispatch(importFetchedStatuses(payload.statuses));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRequestPost } from 'mastodon/api';
|
import api, { apiRequestPost, getLinks } from 'mastodon/api';
|
||||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||||
import type { StatusVisibility } from 'mastodon/models/status';
|
import type { StatusVisibility } from 'mastodon/models/status';
|
||||||
|
|
||||||
|
@ -14,3 +14,15 @@ export const apiRevokeQuote = (quotedStatusId: string, statusId: string) =>
|
||||||
apiRequestPost<ApiStatusJSON>(
|
apiRequestPost<ApiStatusJSON>(
|
||||||
`v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`,
|
`v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const apiGetQuotes = async (statusId: string, url?: string) => {
|
||||||
|
const response = await api().request<ApiStatusJSON[]>({
|
||||||
|
method: 'GET',
|
||||||
|
url: url ?? `/api/v1/statuses/${statusId}/quotes`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statuses: response.data,
|
||||||
|
links: getLinks(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
113
app/javascript/mastodon/features/quotes/index.tsx
Normal file
113
app/javascript/mastodon/features/quotes/index.tsx
Normal file
|
@ -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;
|
|
@ -31,6 +31,7 @@ import { VisibilityIcon } from 'mastodon/components/visibility_icon';
|
||||||
import { Audio } from 'mastodon/features/audio';
|
import { Audio } from 'mastodon/features/audio';
|
||||||
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
|
||||||
import { Video } from 'mastodon/features/video';
|
import { Video } from 'mastodon/features/video';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import Card from './card';
|
import Card from './card';
|
||||||
|
|
||||||
|
@ -282,6 +283,22 @@ export const DetailedStatus: React.FC<{
|
||||||
|
|
||||||
if (['private', 'direct'].includes(status.get('visibility') as string)) {
|
if (['private', 'direct'].includes(status.get('visibility') as string)) {
|
||||||
quotesLink = '';
|
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 {
|
} else {
|
||||||
quotesLink = (
|
quotesLink = (
|
||||||
<span className='detailed-status__link'>
|
<span className='detailed-status__link'>
|
||||||
|
|
|
@ -75,6 +75,7 @@ import {
|
||||||
PrivacyPolicy,
|
PrivacyPolicy,
|
||||||
TermsOfService,
|
TermsOfService,
|
||||||
AccountFeatured,
|
AccountFeatured,
|
||||||
|
Quotes,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { ColumnsContextProvider } from './util/columns_context';
|
import { ColumnsContextProvider } from './util/columns_context';
|
||||||
import { focusColumn, getFocusedItemIndex, focusItemSibling } from './util/focusUtils';
|
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' exact component={Status} content={children} />
|
||||||
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
|
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
|
||||||
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} 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 */}
|
{/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
|
||||||
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
||||||
|
|
|
@ -86,6 +86,10 @@ export function Favourites () {
|
||||||
return import('../../favourites');
|
return import('../../favourites');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Quotes () {
|
||||||
|
return import('../../quotes');
|
||||||
|
}
|
||||||
|
|
||||||
export function FollowRequests () {
|
export function FollowRequests () {
|
||||||
return import('../../follow_requests');
|
return import('../../follow_requests');
|
||||||
}
|
}
|
||||||
|
|
|
@ -904,6 +904,7 @@
|
||||||
"status.quote_post_author": "Quoted a post by @{name}",
|
"status.quote_post_author": "Quoted a post by @{name}",
|
||||||
"status.quote_private": "Private posts cannot be quoted",
|
"status.quote_private": "Private posts cannot be quoted",
|
||||||
"status.quotes": "{count, plural, one {quote} other {quotes}}",
|
"status.quotes": "{count, plural, one {quote} other {quotes}}",
|
||||||
|
"status.quotes.empty": "No one has quoted this post yet. When someone does, it will show up here.",
|
||||||
"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",
|
||||||
|
|
|
@ -28,6 +28,9 @@ import {
|
||||||
PIN_SUCCESS,
|
PIN_SUCCESS,
|
||||||
UNPIN_SUCCESS,
|
UNPIN_SUCCESS,
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
|
import {
|
||||||
|
fetchQuotes
|
||||||
|
} from '../actions/interactions_typed';
|
||||||
import {
|
import {
|
||||||
PINNED_STATUSES_FETCH_SUCCESS,
|
PINNED_STATUSES_FETCH_SUCCESS,
|
||||||
} from '../actions/pin_statuses';
|
} from '../actions/pin_statuses';
|
||||||
|
@ -40,8 +43,6 @@ import {
|
||||||
TRENDS_STATUSES_EXPAND_FAIL,
|
TRENDS_STATUSES_EXPAND_FAIL,
|
||||||
} from '../actions/trends';
|
} from '../actions/trends';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
favourites: ImmutableMap({
|
favourites: ImmutableMap({
|
||||||
next: null,
|
next: null,
|
||||||
|
@ -63,6 +64,12 @@ const initialState = ImmutableMap({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
items: ImmutableOrderedSet(),
|
items: ImmutableOrderedSet(),
|
||||||
}),
|
}),
|
||||||
|
quotes: ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
loaded: false,
|
||||||
|
items: ImmutableOrderedSet(),
|
||||||
|
statusId: null,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeList = (state, listType, statuses, next) => {
|
const normalizeList = (state, listType, statuses, next) => {
|
||||||
|
@ -147,6 +154,13 @@ export default function statusLists(state = initialState, action) {
|
||||||
case muteAccountSuccess.type:
|
case muteAccountSuccess.type:
|
||||||
return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.payload.statuses.getIn([statusId, 'account']) === action.payload.relationship.id));
|
return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.payload.statuses.getIn([statusId, 'account']) === action.payload.relationship.id));
|
||||||
default:
|
default:
|
||||||
|
if (fetchQuotes.fulfilled.match(action))
|
||||||
|
return normalizeList(state, 'quotes', action.payload.statuses, action.payload.next).set('statusId', action.meta.arg.statusId);
|
||||||
|
else if (fetchQuotes.pending.match(action))
|
||||||
|
return state.setIn(['quotes', 'isLoading'], true).setIn(['quotes', 'statusId'], action.meta.arg.statusId);
|
||||||
|
else if (fetchQuotes.rejected.match(action))
|
||||||
|
return state.setIn(['quotes', 'isLoading', false]).setIn(['quotes', 'statusId'], action.meta.arg.statusId);
|
||||||
|
else
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user