diff --git a/app/soapbox/actions/interactions.ts b/app/soapbox/actions/interactions.ts index 77bccb41f..13689b503 100644 --- a/app/soapbox/actions/interactions.ts +++ b/app/soapbox/actions/interactions.ts @@ -3,7 +3,7 @@ import { defineMessages } from 'react-intl'; import toast from 'soapbox/toast'; import { isLoggedIn } from 'soapbox/utils/auth'; -import api from '../api'; +import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts, importFetchedStatus } from './importer'; @@ -73,6 +73,12 @@ const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST'; const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS'; const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL'; +const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS'; +const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL'; + +const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS'; +const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL'; + const messages = defineMessages({ bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' }, bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' }, @@ -380,9 +386,10 @@ const fetchReblogs = (id: string) => dispatch(fetchReblogsRequest(id)); api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - dispatch(fetchReblogsSuccess(id, response.data)); + dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchReblogsFail(id, error)); }); @@ -393,10 +400,11 @@ const fetchReblogsRequest = (id: string) => ({ id, }); -const fetchReblogsSuccess = (id: string, accounts: APIEntity[]) => ({ +const fetchReblogsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ type: REBLOGS_FETCH_SUCCESS, id, accounts, + next, }); const fetchReblogsFail = (id: string, error: AxiosError) => ({ @@ -405,6 +413,31 @@ const fetchReblogsFail = (id: string, error: AxiosError) => ({ error, }); +const expandReblogs = (id: string, path: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + api(getState).get(path).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandReblogsFail(id, error)); + }); + }; + +const expandReblogsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: REBLOGS_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandReblogsFail = (id: string, error: AxiosError) => ({ + type: REBLOGS_EXPAND_FAIL, + id, + error, +}); + const fetchFavourites = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; @@ -412,9 +445,10 @@ const fetchFavourites = (id: string) => dispatch(fetchFavouritesRequest(id)); api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - dispatch(fetchFavouritesSuccess(id, response.data)); + dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchFavouritesFail(id, error)); }); @@ -425,10 +459,11 @@ const fetchFavouritesRequest = (id: string) => ({ id, }); -const fetchFavouritesSuccess = (id: string, accounts: APIEntity[]) => ({ +const fetchFavouritesSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ type: FAVOURITES_FETCH_SUCCESS, id, accounts, + next, }); const fetchFavouritesFail = (id: string, error: AxiosError) => ({ @@ -437,6 +472,31 @@ const fetchFavouritesFail = (id: string, error: AxiosError) => ({ error, }); +const expandFavourites = (id: string, path: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + api(getState).get(path).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandFavouritesFail(id, error)); + }); + }; + +const expandFavouritesSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: FAVOURITES_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandFavouritesFail = (id: string, error: AxiosError) => ({ + type: FAVOURITES_EXPAND_FAIL, + id, + error, +}); + const fetchDislikes = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; @@ -669,6 +729,10 @@ export { REMOTE_INTERACTION_REQUEST, REMOTE_INTERACTION_SUCCESS, REMOTE_INTERACTION_FAIL, + FAVOURITES_EXPAND_SUCCESS, + FAVOURITES_EXPAND_FAIL, + REBLOGS_EXPAND_SUCCESS, + REBLOGS_EXPAND_FAIL, reblog, unreblog, toggleReblog, @@ -709,10 +773,12 @@ export { fetchReblogsRequest, fetchReblogsSuccess, fetchReblogsFail, + expandReblogs, fetchFavourites, fetchFavouritesRequest, fetchFavouritesSuccess, fetchFavouritesFail, + expandFavourites, fetchDislikes, fetchDislikesRequest, fetchDislikesSuccess, diff --git a/app/soapbox/features/ui/components/modals/favourites-modal.tsx b/app/soapbox/features/ui/components/modals/favourites-modal.tsx index 5dcd8edc8..d5ecbc837 100644 --- a/app/soapbox/features/ui/components/modals/favourites-modal.tsx +++ b/app/soapbox/features/ui/components/modals/favourites-modal.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchFavourites } from 'soapbox/actions/interactions'; +import { fetchFavourites, expandFavourites } from 'soapbox/actions/interactions'; import ScrollableList from 'soapbox/components/scrollable-list'; import { Modal, Spinner } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account-container'; @@ -16,6 +16,7 @@ const FavouritesModal: React.FC = ({ onClose, statusId }) => { const dispatch = useAppDispatch(); const accountIds = useAppSelector((state) => state.user_lists.favourited_by.get(statusId)?.items); + const next = useAppSelector((state) => state.user_lists.favourited_by.get(statusId)?.next); const fetchData = () => { dispatch(fetchFavourites(statusId)); @@ -29,6 +30,12 @@ const FavouritesModal: React.FC = ({ onClose, statusId }) => { onClose('FAVOURITES'); }; + const handleLoadMore = () => { + if (next) { + dispatch(expandFavourites(statusId, next!)); + } + }; + let body; if (!accountIds) { @@ -42,6 +49,10 @@ const FavouritesModal: React.FC = ({ onClose, statusId }) => { emptyMessage={emptyMessage} className='max-w-full' itemClassName='pb-3' + style={{ height: '80vh' }} + useWindowScroll={false} + onLoadMore={handleLoadMore} + hasMore={!!next} > {accountIds.map(id => , diff --git a/app/soapbox/features/ui/components/modals/reblogs-modal.tsx b/app/soapbox/features/ui/components/modals/reblogs-modal.tsx index b54c9258e..bb328d8f0 100644 --- a/app/soapbox/features/ui/components/modals/reblogs-modal.tsx +++ b/app/soapbox/features/ui/components/modals/reblogs-modal.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchReblogs } from 'soapbox/actions/interactions'; +import { fetchReblogs, expandReblogs } from 'soapbox/actions/interactions'; import { fetchStatus } from 'soapbox/actions/statuses'; import ScrollableList from 'soapbox/components/scrollable-list'; import { Modal, Spinner } from 'soapbox/components/ui'; @@ -16,6 +16,7 @@ interface IReblogsModal { const ReblogsModal: React.FC = ({ onClose, statusId }) => { const dispatch = useAppDispatch(); const accountIds = useAppSelector((state) => state.user_lists.reblogged_by.get(statusId)?.items); + const next = useAppSelector((state) => state.user_lists.reblogged_by.get(statusId)?.next); const fetchData = () => { dispatch(fetchReblogs(statusId)); @@ -30,6 +31,12 @@ const ReblogsModal: React.FC = ({ onClose, statusId }) => { onClose('REBLOGS'); }; + const handleLoadMore = () => { + if (next) { + dispatch(expandReblogs(statusId, next!)); + } + }; + let body; if (!accountIds) { @@ -43,6 +50,10 @@ const ReblogsModal: React.FC = ({ onClose, statusId }) => { emptyMessage={emptyMessage} className='max-w-full' itemClassName='pb-3' + style={{ height: '80vh' }} + useWindowScroll={false} + onLoadMore={handleLoadMore} + hasMore={!!next} > {accountIds.map((id) => , diff --git a/app/soapbox/reducers/user-lists.ts b/app/soapbox/reducers/user-lists.ts index 3cb1f9205..80a5fbafc 100644 --- a/app/soapbox/reducers/user-lists.ts +++ b/app/soapbox/reducers/user-lists.ts @@ -59,7 +59,9 @@ import { } from 'soapbox/actions/groups'; import { REBLOGS_FETCH_SUCCESS, + REBLOGS_EXPAND_SUCCESS, FAVOURITES_FETCH_SUCCESS, + FAVOURITES_EXPAND_SUCCESS, DISLIKES_FETCH_SUCCESS, REACTIONS_FETCH_SUCCESS, } from 'soapbox/actions/interactions'; @@ -172,9 +174,13 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) { case FOLLOWING_EXPAND_SUCCESS: return appendToList(state, ['following', action.id], action.accounts, action.next); case REBLOGS_FETCH_SUCCESS: - return normalizeList(state, ['reblogged_by', action.id], action.accounts); + return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next); + case REBLOGS_EXPAND_SUCCESS: + return appendToList(state, ['reblogged_by', action.id], action.accounts, action.next); case FAVOURITES_FETCH_SUCCESS: - return normalizeList(state, ['favourited_by', action.id], action.accounts); + return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next); + case FAVOURITES_EXPAND_SUCCESS: + return appendToList(state, ['favourited_by', action.id], action.accounts, action.next); case DISLIKES_FETCH_SUCCESS: return normalizeList(state, ['disliked_by', action.id], action.accounts); case REACTIONS_FETCH_SUCCESS: