From fa8ec1d62f8ecff37815d92d600229527d021a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicole=20Miko=C5=82ajczyk?= Date: Mon, 5 May 2025 18:08:49 +0200 Subject: [PATCH] pl-fe: migrate account gallery to react query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicole Mikołajczyk --- packages/pl-fe/src/actions/statuses.ts | 4 +- .../account-gallery/components/media-item.tsx | 11 ++-- .../src/features/account-gallery/index.tsx | 39 ++++--------- .../components/panels/group-media-panel.tsx | 43 +++----------- .../components/panels/profile-media-panel.tsx | 49 +++++----------- .../pl-fe/src/hooks/use-account-gallery.ts | 58 +++++++++++++++++++ packages/pl-fe/src/layouts/group-layout.tsx | 4 +- packages/pl-fe/src/modals/media-modal.tsx | 4 +- .../pl-fe/src/pages/groups/group-gallery.tsx | 28 ++------- .../timelines/use-account-media-timeline.ts | 14 +++++ packages/pl-fe/src/selectors/index.ts | 38 +----------- 11 files changed, 125 insertions(+), 167 deletions(-) create mode 100644 packages/pl-fe/src/hooks/use-account-gallery.ts create mode 100644 packages/pl-fe/src/queries/timelines/use-account-media-timeline.ts diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index 57d9aa5f3..c97f88823 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -11,7 +11,7 @@ import { setComposeToStatus } from './compose'; import { importEntities } from './importer'; import { deleteFromTimelines } from './timelines'; -import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus } from 'pl-api'; +import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus, StatusSource } from 'pl-api'; import type { Status } from 'pl-fe/normalizers/status'; import type { AppDispatch, RootState } from 'pl-fe/store'; import type { IntlShape } from 'react-intl'; @@ -128,7 +128,7 @@ const deleteStatus = (statusId: string, groupId?: string, withRedraft = false) = dispatch(deleteFromTimelines(statusId)); if (withRedraft) { - dispatch(setComposeToStatus(status, poll, response.text || '', response.spoiler_text, response.content_type, withRedraft)); + dispatch(setComposeToStatus(status, poll, response.text || '', response.spoiler_text, (response as StatusSource).content_type, withRedraft)); useModalsStore.getState().openModal('COMPOSE'); } }) diff --git a/packages/pl-fe/src/features/account-gallery/components/media-item.tsx b/packages/pl-fe/src/features/account-gallery/components/media-item.tsx index 7dc02911d..159d70e2d 100644 --- a/packages/pl-fe/src/features/account-gallery/components/media-item.tsx +++ b/packages/pl-fe/src/features/account-gallery/components/media-item.tsx @@ -2,13 +2,14 @@ import clsx from 'clsx'; import React, { useState } from 'react'; import { Link } from 'react-router-dom'; +import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; import Blurhash from 'pl-fe/components/blurhash'; import Icon from 'pl-fe/components/icon'; import StillImage from 'pl-fe/components/still-image'; import { useSettings } from 'pl-fe/hooks/use-settings'; import { isIOS } from 'pl-fe/is-mobile'; -import type { AccountGalleryAttachment } from 'pl-fe/selectors'; +import type { AccountGalleryAttachment } from 'pl-fe/hooks/use-account-gallery'; interface IMediaItem { attachment: AccountGalleryAttachment; @@ -18,7 +19,8 @@ interface IMediaItem { const MediaItem: React.FC = ({ attachment, onOpenMedia, isLast }) => { const { autoPlayGif, displayMedia } = useSettings(); - const [visible, setVisible] = useState(displayMedia !== 'hide_all' && !attachment.status?.sensitive || displayMedia === 'show_all'); + const { account } = useAccount(attachment.account_id); + const [visible, setVisible] = useState(displayMedia !== 'hide_all' && !attachment.sensitive || displayMedia === 'show_all'); const handleMouseEnter: React.MouseEventHandler = e => { const video = e.target as HTMLVideoElement; @@ -49,8 +51,7 @@ const MediaItem: React.FC = ({ attachment, onOpenMedia, isLast }) => } }; - const status = attachment.status; - const title = status.spoiler_text || attachment.description; + const title = attachment.description; let thumbnail: React.ReactNode = ''; let icon; @@ -119,7 +120,7 @@ const MediaItem: React.FC = ({ attachment, onOpenMedia, isLast }) => return (
- + { - const dispatch = useAppDispatch(); const { username } = useParams<{ username: string }>(); const { openModal } = useModalsStore(); @@ -26,9 +22,7 @@ const AccountGallery = () => { isUnavailable, } = useAccountLookup(username, { withRelationship: true }); - const attachments: Array = useAppSelector((state) => account ? getAccountGallery(state, account.id) : []); - const isLoading = useAppSelector((state) => state.timelines[`account:${account?.id}:with_replies:media`]?.isLoading); - const hasMore = useAppSelector((state) => state.timelines[`account:${account?.id}:with_replies:media`]?.hasMore); + const { data: attachments, isFetching, isLoading, hasNextPage: hasMore, fetchNextPage } = useAccountGallery(account?.id!); const handleScrollToBottom = () => { if (hasMore) { @@ -37,9 +31,7 @@ const AccountGallery = () => { }; const handleLoadMore = () => { - if (account) { - dispatch(fetchAccountTimeline(account.id, { only_media: true }, true)); - } + fetchNextPage({ cancelRefetch: false }); }; const handleLoadOlder: React.MouseEventHandler = e => { @@ -49,22 +41,13 @@ const AccountGallery = () => { const handleOpenMedia = (attachment: AccountGalleryAttachment) => { if (attachment.type === 'video') { - openModal('VIDEO', { media: attachment, statusId: attachment.status.id }); + openModal('VIDEO', { media: attachment, statusId: attachment.status_id }); } else { - const media = attachment.status.media_attachments; - const index = media.findIndex((x) => x.id === attachment.id); - - openModal('MEDIA', { media, index, statusId: attachment.status.id }); + openModal('MEDIA', { index: attachment.index, statusId: attachment.status_id }); } }; - useEffect(() => { - if (account) { - dispatch(fetchAccountTimeline(account.id, { only_media: true, limit: 40 })); - } - }, [account?.id]); - - if (accountLoading || (!attachments && isLoading)) { + if (accountLoading || isLoading) { return ( @@ -80,8 +63,8 @@ const AccountGallery = () => { let loadOlder = null; - if (hasMore && !(isLoading && attachments.length === 0)) { - loadOlder = ; + if (hasMore && !(isFetching && attachments.length === 0)) { + loadOlder = ; } if (isUnavailable) { @@ -99,7 +82,7 @@ const AccountGallery = () => {
{attachments.map((attachment, index) => ( { {loadOlder} - {isLoading && attachments.length === 0 && ( + {isFetching && attachments.length === 0 && (
diff --git a/packages/pl-fe/src/features/ui/components/panels/group-media-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/group-media-panel.tsx index b9e1e7066..7e2aebdbb 100644 --- a/packages/pl-fe/src/features/ui/components/panels/group-media-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/group-media-panel.tsx @@ -1,13 +1,10 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchGroupTimeline } from 'pl-fe/actions/timelines'; import Spinner from 'pl-fe/components/ui/spinner'; import Text from 'pl-fe/components/ui/text'; import Widget from 'pl-fe/components/ui/widget'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; -import { type AccountGalleryAttachment, getGroupGallery } from 'pl-fe/selectors'; +import { type AccountGalleryAttachment, useGroupGallery } from 'pl-fe/hooks/use-account-gallery'; import { useModalsStore } from 'pl-fe/stores/modals'; import MediaItem from '../../../account-gallery/components/media-item'; @@ -15,42 +12,22 @@ import MediaItem from '../../../account-gallery/components/media-item'; import type { Group } from 'pl-fe/normalizers/group'; interface IGroupMediaPanel { - group?: Group; + group: Group; } const GroupMediaPanel: React.FC = ({ group }) => { - const dispatch = useAppDispatch(); const { openModal } = useModalsStore(); - const [loading, setLoading] = useState(true); - - const isMember = !!group?.relationship?.member; - const isPrivate = group?.locked; - - const attachments: Array = useAppSelector((state) => group ? getGroupGallery(state, group?.id) : []); + const { data: attachments, isLoading } = useGroupGallery(group.id); const handleOpenMedia = (attachment: AccountGalleryAttachment): void => { if (attachment.type === 'video') { - openModal('VIDEO', { media: attachment, statusId: attachment.status.id }); + openModal('VIDEO', { media: attachment, statusId: attachment.status_id }); } else { - const media = attachment.status.media_attachments; - const index = media.findIndex(x => x.id === attachment.id); - - openModal('MEDIA', { media, index, statusId: attachment.status.id }); + openModal('MEDIA', { index: attachment.index, statusId: attachment.status_id }); } }; - useEffect(() => { - setLoading(true); - - if (group && (isMember || !isPrivate)) { - dispatch(fetchGroupTimeline(group.id, { only_media: true, limit: 40 })) - // @ts-ignore - .then(() => setLoading(false)) - .catch(() => {}); - } - }, [group?.id, isMember, isPrivate]); - const renderAttachments = () => { const nineAttachments = attachments.slice(0, 9); @@ -59,7 +36,7 @@ const GroupMediaPanel: React.FC = ({ group }) => {
{nineAttachments.map((attachment, index) => ( = ({ group }) => { } }; - if (isPrivate && !isMember) { - return null; - } - return ( }> {group && (
- {loading ? ( + {isLoading ? ( ) : ( renderAttachments() diff --git a/packages/pl-fe/src/features/ui/components/panels/profile-media-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/profile-media-panel.tsx index abe17921c..350472722 100644 --- a/packages/pl-fe/src/features/ui/components/panels/profile-media-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/profile-media-panel.tsx @@ -1,13 +1,10 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchAccountTimeline } from 'pl-fe/actions/timelines'; import Spinner from 'pl-fe/components/ui/spinner'; import Text from 'pl-fe/components/ui/text'; import Widget from 'pl-fe/components/ui/widget'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; -import { type AccountGalleryAttachment, getAccountGallery } from 'pl-fe/selectors'; +import { type AccountGalleryAttachment, useAccountGallery } from 'pl-fe/hooks/use-account-gallery'; import { useModalsStore } from 'pl-fe/stores/modals'; import MediaItem from '../../../account-gallery/components/media-item'; @@ -19,37 +16,21 @@ interface IProfileMediaPanel { } const ProfileMediaPanel: React.FC = ({ account }) => { - const dispatch = useAppDispatch(); const { openModal } = useModalsStore(); - const [loading, setLoading] = useState(true); - - const attachments: Array = useAppSelector((state) => account ? getAccountGallery(state, account?.id) : []); + const { data: attachments, isLoading } = useAccountGallery(account?.id!); const handleOpenMedia = (attachment: AccountGalleryAttachment): void => { if (attachment.type === 'video') { - openModal('VIDEO', { media: attachment, statusId: attachment.status.id }); + openModal('VIDEO', { media: attachment, statusId: attachment.status_id }); } else { - const media = attachment.status.media_attachments; - const index = media.findIndex(x => x.id === attachment.id); - openModal('MEDIA', { media, index, statusId: attachment.status.id }); + openModal('MEDIA', { index: attachment.index, statusId: attachment.status_id }); } }; - useEffect(() => { - setLoading(true); - - if (account) { - dispatch(fetchAccountTimeline(account.id, { only_media: true, limit: 40 })) - // @ts-ignore yes it does - .then(() => setLoading(false)) - .catch(() => {}); - } - }, [account?.id]); - const renderAttachments = () => { - const publicAttachments = attachments.filter(attachment => attachment.status.visibility === 'public'); + const publicAttachments = attachments.filter(attachment => attachment.visibility === 'public'); const nineAttachments = publicAttachments.slice(0, 9); if (nineAttachments.length) { @@ -57,7 +38,7 @@ const ProfileMediaPanel: React.FC = ({ account }) => {
{nineAttachments.map((attachment, index) => ( = ({ account }) => { return ( }> - {account && ( -
- {loading ? ( - - ) : ( - renderAttachments() - )} -
- )} +
+ {isLoading || !account ? ( + + ) : ( + renderAttachments() + )} +
); }; diff --git a/packages/pl-fe/src/hooks/use-account-gallery.ts b/packages/pl-fe/src/hooks/use-account-gallery.ts new file mode 100644 index 000000000..a5188106e --- /dev/null +++ b/packages/pl-fe/src/hooks/use-account-gallery.ts @@ -0,0 +1,58 @@ +import { createSelector } from 'reselect'; + +import { useAccountMediaTimeline, useGroupMediaTimeline } from 'pl-fe/queries/timelines/use-account-media-timeline'; + +import { useAppSelector } from './use-app-selector'; + +import type { MediaAttachment } from 'pl-api'; +import type { RootState } from 'pl-fe/store'; + + +type AccountGalleryAttachment = MediaAttachment & { + index: number; + sensitive: boolean; + visibility: string; + status_id: string; + account_id: string; +} + +const getGallery = createSelector([ + (state: RootState, statusIds: string[]) => statusIds, + (state: RootState) => state.statuses, +], (statusIds, statuses) => + statusIds.reduce((medias: Array, statusId: string) => { + const status = statuses[statusId]; + if (!status) return medias; + if (status.reblog_id) return medias; + + return medias.concat( + status.media_attachments.map((media, index) => ({ + ...media, + index, + sensitive: status.sensitive, + visibility: status.visibility, + status_id: statusId, + account_id: status.account.id, + }))); + }, []), +); + +const useAccountGallery = (accountId: string) => { + const result = useAccountMediaTimeline(accountId); + + return { + ...result, + data: useAppSelector((state) => getGallery(state, result.data || [])), + }; +}; + +const useGroupGallery = (groupId: string) => { + const result = useGroupMediaTimeline(groupId); + + return { + ...result, + data: useAppSelector((state) => getGallery(state, result.data || [])), + }; +}; + +export { useAccountGallery, useGroupGallery, type AccountGalleryAttachment }; diff --git a/packages/pl-fe/src/layouts/group-layout.tsx b/packages/pl-fe/src/layouts/group-layout.tsx index 7ca274b87..0dbef27cf 100644 --- a/packages/pl-fe/src/layouts/group-layout.tsx +++ b/packages/pl-fe/src/layouts/group-layout.tsx @@ -116,7 +116,9 @@ const GroupLayout: React.FC = ({ params, children }) => { {!me && ( )} - + {group && (group.relationship?.member || !group.locked) && ( + + )} diff --git a/packages/pl-fe/src/modals/media-modal.tsx b/packages/pl-fe/src/modals/media-modal.tsx index 3d36f990f..f392f1b16 100644 --- a/packages/pl-fe/src/modals/media-modal.tsx +++ b/packages/pl-fe/src/modals/media-modal.tsx @@ -47,7 +47,7 @@ const containerStyle: React.CSSProperties = { }; interface MediaModalProps { - media: Array; + media?: Array; statusId?: string; index: number; time?: number; @@ -55,7 +55,6 @@ interface MediaModalProps { const MediaModal: React.FC = (props) => { const { - media, statusId, onClose, time = 0, @@ -66,6 +65,7 @@ const MediaModal: React.FC = (props) => { const getStatus = useCallback(makeGetStatus(), []); const status = useAppSelector((state) => statusId ? getStatus(state, { id: statusId }) : undefined); + const media = status?.media_attachments || props.media || []; const [isLoaded, setIsLoaded] = useState(!!status); const [index, setIndex] = useState(null); diff --git a/packages/pl-fe/src/pages/groups/group-gallery.tsx b/packages/pl-fe/src/pages/groups/group-gallery.tsx index 5cf3c0ba9..ef25cb699 100644 --- a/packages/pl-fe/src/pages/groups/group-gallery.tsx +++ b/packages/pl-fe/src/pages/groups/group-gallery.tsx @@ -2,16 +2,14 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { useGroup } from 'pl-fe/api/hooks/groups/use-group'; -import { useGroupMedia } from 'pl-fe/api/hooks/groups/use-group-media'; import LoadMore from 'pl-fe/components/load-more'; import MissingIndicator from 'pl-fe/components/missing-indicator'; import Column from 'pl-fe/components/ui/column'; import Spinner from 'pl-fe/components/ui/spinner'; import MediaItem from 'pl-fe/features/account-gallery/components/media-item'; +import { type AccountGalleryAttachment, useGroupGallery } from 'pl-fe/hooks/use-account-gallery'; import { useModalsStore } from 'pl-fe/stores/modals'; -import type { Status } from 'pl-fe/normalizers/status'; -import type { AccountGalleryAttachment } from 'pl-fe/selectors'; interface IGroupGallery { params: { groupId: string }; @@ -24,27 +22,13 @@ const GroupGallery: React.FC = (props) => { const { group, isLoading: groupIsLoading } = useGroup(groupId); - const { - entities: statuses, - fetchNextPage, - isLoading, - isFetching, - hasNextPage, - } = useGroupMedia(groupId); - - const attachments = statuses.reduce((result, status) => { - result.push(...status.media_attachments.map((a) => ({ ...a, status, account: status.account }))); - return result; - }, []); + const { data: attachments, isFetching, isLoading, hasNextPage, fetchNextPage } = useGroupGallery(groupId); const handleOpenMedia = (attachment: AccountGalleryAttachment) => { if (attachment.type === 'video') { - openModal('VIDEO', { media: attachment, statusId: attachment.status.id }); + openModal('VIDEO', { media: attachment, statusId: attachment.status_id }); } else { - const media = (attachment.status as Status).media_attachments; - const index = media.findIndex((x) => x.id === attachment.id); - - openModal('MEDIA', { media, index, statusId: attachment.status.id }); + openModal('MEDIA', { index: attachment.index, statusId: attachment.status_id }); } }; @@ -71,7 +55,7 @@ const GroupGallery: React.FC = (props) => {
{attachments.map((attachment, index) => ( = (props) => {
{hasNextPage && ( - + fetchNextPage({ cancelRefetch: false })} /> )} ); diff --git a/packages/pl-fe/src/queries/timelines/use-account-media-timeline.ts b/packages/pl-fe/src/queries/timelines/use-account-media-timeline.ts new file mode 100644 index 000000000..24c839f31 --- /dev/null +++ b/packages/pl-fe/src/queries/timelines/use-account-media-timeline.ts @@ -0,0 +1,14 @@ +import { makePaginatedResponseQuery } from '../utils/make-paginated-response-query'; +import { minifyStatusList } from '../utils/minify-list'; + +const useAccountMediaTimeline = makePaginatedResponseQuery( + (accountId: string) => ['timelineIds', `account:${accountId}:with_replies:media`], + (client, [accountId]) => client.accounts.getAccountStatuses(accountId!, { only_media: true }).then(minifyStatusList), +); + +const useGroupMediaTimeline = makePaginatedResponseQuery( + (groupId: string) => ['timelineIds', `group:${groupId}:media`], + (client, [groupId]) => client.timelines.groupTimeline(groupId!, { only_media: true }).then(minifyStatusList), +); + +export { useAccountMediaTimeline, useGroupMediaTimeline }; diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 17c8a0ca6..bb9aeebfa 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -8,7 +8,7 @@ import { validId } from 'pl-fe/utils/auth'; import ConfigDB from 'pl-fe/utils/config-db'; import { shouldFilter } from 'pl-fe/utils/timelines'; -import type { Account as BaseAccount, Filter, MediaAttachment, NotificationGroup, Relationship } from 'pl-api'; +import type { Filter, NotificationGroup, Relationship } from 'pl-api'; import type { EntityStore } from 'pl-fe/entity-store/types'; import type { Account } from 'pl-fe/normalizers/account'; import type { Group } from 'pl-fe/normalizers/group'; @@ -199,39 +199,6 @@ type SelectedNotification = NotificationGroup & { target: Account; }) -type AccountGalleryAttachment = MediaAttachment & { - status: MinifiedStatus; - account: BaseAccount; -} - -const getAccountGallery = createSelector([ - (state: RootState, id: string) => state.timelines[`account:${id}:with_replies:media`]?.items || [], - (state: RootState) => state.statuses, -], (statusIds, statuses) => - statusIds.reduce((medias: Array, statusId: string) => { - const status = statuses[statusId]; - if (!status) return medias; - if (status.reblog_id) return medias; - - return medias.concat( - status.media_attachments.map(media => ({ ...media, status, account: status.account }))); - }, []), -); - -const getGroupGallery = createSelector([ - (state: RootState, id: string) => state.timelines[`group:${id}:media`]?.items || [], - (state: RootState) => state.statuses, -], (statusIds, statuses) => - statusIds.reduce((medias: Array, statusId: string) => { - const status = statuses[statusId]; - if (!status) return medias; - if (status.reblog_id) return medias; - - return medias.concat( - status.media_attachments.map(media => ({ ...media, status, account: status.account }))); - }, []), -); - const makeGetReport = () => { const getStatus = makeGetStatus(); @@ -354,9 +321,6 @@ export { type SelectedStatus, makeGetNotification, type SelectedNotification, - type AccountGalleryAttachment, - getAccountGallery, - getGroupGallery, makeGetReport, makeGetOtherAccounts, makeGetHosts,