diff --git a/packages/nicolium/src/actions/importer.ts b/packages/nicolium/src/actions/importer.ts index b457bf670..29074a11c 100644 --- a/packages/nicolium/src/actions/importer.ts +++ b/packages/nicolium/src/actions/importer.ts @@ -11,6 +11,7 @@ import type { Relationship as BaseRelationship, Status as BaseStatus, StatusWithoutAccount, + Translation, } from 'pl-api'; type Status = BaseStatus | (StatusWithoutAccount & { expectsCard?: boolean }); @@ -24,6 +25,7 @@ interface ImportStatusAction { type: typeof STATUS_IMPORT; status: Status; idempotencyKey?: string; + skipQueryDataUpdate?: boolean; } interface ImportStatusesAction { @@ -57,6 +59,7 @@ const importEntities = const polls: Record = {}; const relationships: Record = {}; const statuses: Record = {}; + const translations: Record> = {}; const processAccount = (account: BaseAccount, withSelf = true) => { if (!override && selectAccount(account.id)) return; @@ -82,6 +85,10 @@ const importEntities = if (status.reblog) processStatus(status.reblog); if (status.poll) polls[status.poll.id] = status.poll; if (status.group) groups[status.group.id] = status.group; + if (status.translation) { + if (!translations[status.id]) translations[status.id] = {}; + translations[status.id][status.translation.language] = status.translation; + } }; if (options.withParents) { @@ -138,6 +145,16 @@ const importEntities = if (!isEmpty(statuses)) dispatch({ type: STATUSES_IMPORT, statuses: Object.values(statuses) }); + if (!isEmpty(translations)) { + for (const [statusId, translationsByLanguage] of Object.entries(translations)) { + for (const [language, translation] of Object.entries(translationsByLanguage)) { + queryClient.setQueryData( + queryKeys.statuses.translations(statusId, language), + translation, + ); + } + } + } }; type ImporterAction = ImportStatusAction | ImportStatusesAction; diff --git a/packages/nicolium/src/actions/statuses.ts b/packages/nicolium/src/actions/statuses.ts index 70dd8d494..29638bfdd 100644 --- a/packages/nicolium/src/actions/statuses.ts +++ b/packages/nicolium/src/actions/statuses.ts @@ -30,8 +30,6 @@ const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST' as const; const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS' as const; const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL' as const; -const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS' as const; - const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS' as const; const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS' as const; @@ -232,35 +230,6 @@ const updateStatus = (status: BaseStatus) => (dispatch: AppDispatch) => { dispatch(importEntities({ statuses: [status] })); }; -const fetchContext = - (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch, getState: () => RootState) => { - const params = - intl && useSettingsStore.getState().settings.autoTranslate - ? { - language: intl.locale, - } - : undefined; - - return getClient(getState()) - .statuses.getContext(statusId, params) - .then((context) => { - const { ancestors, descendants } = context; - const statuses = ancestors.concat(descendants); - dispatch(importEntities({ statuses })); - useContextStore.getState().actions.importContext(statusId, context); - dispatch({ type: CONTEXT_FETCH_SUCCESS, statusId, ancestors, descendants }); - return context; - }) - .catch((error) => { - if (error.response?.status === 404) { - dispatch(deleteFromTimelines(statusId)); - } - }); - }; - -const fetchStatusWithContext = (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch) => - Promise.all([dispatch(fetchContext(statusId, intl)), dispatch(fetchStatus(statusId, intl))]); - const muteStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; @@ -370,12 +339,6 @@ type StatusesAction = params: Pick; error: unknown; } - | { - type: typeof CONTEXT_FETCH_SUCCESS; - statusId: string; - ancestors: Array; - descendants: Array; - } | { type: typeof STATUS_MUTE_SUCCESS; statusId: string } | { type: typeof STATUS_UNMUTE_SUCCESS; statusId: string } | ReturnType; @@ -390,7 +353,6 @@ export { STATUS_DELETE_REQUEST, STATUS_DELETE_SUCCESS, STATUS_DELETE_FAIL, - CONTEXT_FETCH_SUCCESS, STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, STATUS_UNFILTER, @@ -400,8 +362,6 @@ export { deleteStatus, deleteStatusFromGroup, updateStatus, - fetchContext, - fetchStatusWithContext, muteStatus, unmuteStatus, toggleMuteStatus, diff --git a/packages/nicolium/src/components/statuses/status-action-bar.tsx b/packages/nicolium/src/components/statuses/status-action-bar.tsx index eb9b9f05b..c4ff7d5c4 100644 --- a/packages/nicolium/src/components/statuses/status-action-bar.tsx +++ b/packages/nicolium/src/components/statuses/status-action-bar.tsx @@ -57,8 +57,8 @@ import Popover from '../ui/popover'; import type { Menu } from '@/components/dropdown-menu'; import type { Emoji as EmojiType } from '@/features/emoji'; import type { UnauthorizedModalAction } from '@/modals/unauthorized-modal'; +import type { SelectedStatus } from '@/queries/statuses/use-status'; import type { Me } from '@/reducers/me'; -import type { SelectedStatus } from '@/selectors'; const messages = defineMessages({ adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' }, diff --git a/packages/nicolium/src/components/statuses/status-hover-card.tsx b/packages/nicolium/src/components/statuses/status-hover-card.tsx index fbf98ec47..b24e98a9e 100644 --- a/packages/nicolium/src/components/statuses/status-hover-card.tsx +++ b/packages/nicolium/src/components/statuses/status-hover-card.tsx @@ -2,13 +2,10 @@ import { autoUpdate, flip, shift, useFloating, useTransitionStyles } from '@floa import { useRouter } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useEffect } from 'react'; -import { useIntl } from 'react-intl'; -import { fetchStatus } from '@/actions/statuses'; import { showStatusHoverCard } from '@/components/statuses/hover-status-wrapper'; import StatusContainer from '@/containers/status-container'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; +import { useStatus } from '@/queries/statuses/use-status'; import { useStatusHoverCardActions, useStatusHoverCardStore } from '@/stores/status-hover-card'; interface IStatusHoverCard { @@ -17,20 +14,12 @@ interface IStatusHoverCard { /** Popup status preview that appears when hovering reply to */ const StatusHoverCard: React.FC = ({ visible = true }) => { - const dispatch = useAppDispatch(); const router = useRouter(); - const intl = useIntl(); const { statusId, ref } = useStatusHoverCardStore(); const { closeStatusHoverCard, updateStatusHoverCard } = useStatusHoverCardActions(); - const status = useAppSelector((state) => state.statuses[statusId!]); - - useEffect(() => { - if (statusId && !status) { - dispatch(fetchStatus(statusId, intl)); - } - }, [statusId, status]); + useStatus(statusId ?? undefined); useEffect(() => { const unlisten = router.subscribe('onLoad', ({ pathChanged }) => { diff --git a/packages/nicolium/src/components/statuses/status.tsx b/packages/nicolium/src/components/statuses/status.tsx index ab4746b9a..3e410df29 100644 --- a/packages/nicolium/src/components/statuses/status.tsx +++ b/packages/nicolium/src/components/statuses/status.tsx @@ -12,16 +12,15 @@ import Emojify from '@/features/emoji/emojify'; import StatusTypeIcon from '@/features/status/components/status-type-icon'; import { Hotkeys } from '@/features/ui/components/hotkeys'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useGroupQuery } from '@/queries/groups/use-group'; import { useFollowedTags } from '@/queries/hashtags/use-followed-tags'; +import { useStatus, type SelectedStatus } from '@/queries/statuses/use-status'; import { useFavouriteStatus, useReblogStatus, useUnfavouriteStatus, useUnreblogStatus, } from '@/queries/statuses/use-status-interactions'; -import { makeGetStatus, type SelectedStatus } from '@/selectors'; import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; @@ -203,10 +202,7 @@ const Status: React.FC = (props) => { const didShowCard = useRef(false); const node = useRef(null); - const getStatus = useMemo(makeGetStatus, []); - const actualStatus = useAppSelector( - (state) => (status.reblog_id && getStatus(state, { id: status.reblog_id })!) || status, - ); + const actualStatus = useStatus(status.reblog_id || undefined).data || status; const { data: group } = useGroupQuery(actualStatus.group_id ?? undefined); diff --git a/packages/nicolium/src/containers/status-container.tsx b/packages/nicolium/src/containers/status-container.tsx index 8e063926d..8db8e3a43 100644 --- a/packages/nicolium/src/containers/status-container.tsx +++ b/packages/nicolium/src/containers/status-container.tsx @@ -1,8 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import Status, { type IStatus } from '@/components/statuses/status'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { makeGetStatus } from '@/selectors'; +import { useStatus } from '@/queries/statuses/use-status'; interface IStatusContainer extends Omit { id: string; @@ -16,10 +15,9 @@ interface IStatusContainer extends Omit { * @deprecated Use the Status component directly. */ const StatusContainer: React.FC = (props) => { - const { id, contextType } = props; + const { id, contextType: _contextType } = props; - const getStatus = useMemo(makeGetStatus, []); - const status = useAppSelector((state) => getStatus(state, { id, contextType })); + const { data: status } = useStatus(id); if (status) { return ; diff --git a/packages/nicolium/src/features/status/components/detailed-status.tsx b/packages/nicolium/src/features/status/components/detailed-status.tsx index db90b8146..1fb5b7529 100644 --- a/packages/nicolium/src/features/status/components/detailed-status.tsx +++ b/packages/nicolium/src/features/status/components/detailed-status.tsx @@ -20,7 +20,7 @@ import { useGroupQuery } from '@/queries/groups/use-group'; import StatusInteractionBar from './status-interaction-bar'; import StatusTypeIcon from './status-type-icon'; -import type { SelectedStatus } from '@/selectors'; +import type { SelectedStatus } from '@/queries/statuses/use-status'; const messages = defineMessages({ applicationName: { id: 'status.application_name', defaultMessage: 'Sent from {name}' }, diff --git a/packages/nicolium/src/features/status/components/thread.tsx b/packages/nicolium/src/features/status/components/thread.tsx index cd24eeb3f..f9a3a0a9d 100644 --- a/packages/nicolium/src/features/status/components/thread.tsx +++ b/packages/nicolium/src/features/status/components/thread.tsx @@ -28,8 +28,8 @@ import { textForScreenReader } from '@/utils/status'; import DetailedStatus from './detailed-status'; import ThreadStatus from './thread-status'; +import type { SelectedStatus } from '@/queries/statuses/use-status'; import type { NormalizedStatus as Status } from '@/reducers/statuses'; -import type { SelectedStatus } from '@/selectors'; import type { Account } from 'pl-api'; import type { VirtuosoHandle } from 'react-virtuoso'; diff --git a/packages/nicolium/src/modals/media-modal.tsx b/packages/nicolium/src/modals/media-modal.tsx index b14c49e35..26d300791 100644 --- a/packages/nicolium/src/modals/media-modal.tsx +++ b/packages/nicolium/src/modals/media-modal.tsx @@ -5,7 +5,6 @@ import clsx from 'clsx'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { fetchStatusWithContext } from '@/actions/statuses'; import ExtendedVideoPlayer from '@/components/media/extended-video-player'; import MissingIndicator from '@/components/missing-indicator'; import StatusActionBar from '@/components/statuses/status-action-bar'; @@ -17,9 +16,7 @@ import PlaceholderStatus from '@/features/placeholder/components/placeholder-sta import Thread from '@/features/status/components/thread'; import ZoomableImage from '@/features/ui/components/zoomable-image'; import Video from '@/features/video'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { makeGetStatus } from '@/selectors'; +import { useStatus } from '@/queries/statuses/use-status'; import { userTouching } from '@/utils/is-mobile'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; @@ -49,16 +46,11 @@ interface MediaModalProps { const MediaModal: React.FC = (props) => { const { statusId, onClose, time = 0 } = props; - const dispatch = useAppDispatch(); const intl = useIntl(); - const getStatus = useCallback(makeGetStatus(), []); - const status = useAppSelector((state) => - statusId ? getStatus(state, { id: statusId }) : undefined, - ); + const { data: status, isPending } = useStatus(statusId, { withContext: true }); const media = status?.media_attachments ?? props.media ?? []; - const [isLoaded, setIsLoaded] = useState(!!status); const [index, setIndex] = useState(props.index || 0); const [zoomedIn, setZoomedIn] = useState(false); const [navigationHidden, setNavigationHidden] = useState(false); @@ -267,19 +259,6 @@ const MediaModal: React.FC = (props) => { [media.length, index, zoomedIn, handleZoomClick], ); - // Load data. - useEffect(() => { - if (status?.id) { - dispatch(fetchStatusWithContext(status.id, intl)) - .then(() => { - setIsLoaded(true); - }) - .catch(() => { - setIsLoaded(true); - }); - } - }, [status?.id]); - useEffect(() => { window.addEventListener('keydown', handleKeyDown, false); @@ -289,7 +268,7 @@ const MediaModal: React.FC = (props) => { }, [index]); if (statusId) { - if (!isLoaded) { + if (isPending) { return ; } else if (!status) { return ; diff --git a/packages/nicolium/src/modals/mentions-modal.tsx b/packages/nicolium/src/modals/mentions-modal.tsx index a61ff6dc1..bb87f3787 100644 --- a/packages/nicolium/src/modals/mentions-modal.tsx +++ b/packages/nicolium/src/modals/mentions-modal.tsx @@ -1,13 +1,10 @@ -import React, { useCallback, useEffect } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; -import { fetchStatusWithContext } from '@/actions/statuses'; import ScrollableList from '@/components/scrollable-list'; import Modal from '@/components/ui/modal'; import AccountContainer from '@/containers/account-container'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { makeGetStatus } from '@/selectors'; +import { useStatus } from '@/queries/statuses/use-status'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; @@ -16,25 +13,13 @@ interface MentionsModalProps { } const MentionsModal: React.FC = ({ onClose, statusId }) => { - const dispatch = useAppDispatch(); - const intl = useIntl(); - const getStatus = useCallback(makeGetStatus(), []); - - const status = useAppSelector((state) => getStatus(state, { id: statusId })); + const { data: status } = useStatus(statusId); const accountIds = status ? status.mentions.map((m) => m.id) : null; - const fetchData = () => { - dispatch(fetchStatusWithContext(statusId, intl)); - }; - const onClickClose = () => { onClose('MENTIONS'); }; - useEffect(() => { - fetchData(); - }, []); - return ( } diff --git a/packages/nicolium/src/pages/statuses/event-discussion.tsx b/packages/nicolium/src/pages/statuses/event-discussion.tsx index 1394f83c4..ea869da2e 100644 --- a/packages/nicolium/src/pages/statuses/event-discussion.tsx +++ b/packages/nicolium/src/pages/statuses/event-discussion.tsx @@ -1,7 +1,6 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import React, { useEffect, useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; -import { fetchStatusWithContext } from '@/actions/statuses'; import MissingIndicator from '@/components/missing-indicator'; import ScrollableList from '@/components/scrollable-list'; import Tombstone from '@/components/statuses/tombstone'; @@ -11,9 +10,8 @@ import ThreadStatus from '@/features/status/components/thread-status'; import PendingStatus from '@/features/ui/components/pending-status'; import { eventDiscussionRoute } from '@/features/ui/router'; import { ComposeForm } from '@/features/ui/util/async-components'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; -import { makeGetStatus } from '@/selectors'; +import { useStatus } from '@/queries/statuses/use-status'; import { useComposeActions } from '@/stores/compose'; import { useDescendantsIds } from '@/stores/contexts'; import { selectChild } from '@/utils/scroll-utils'; @@ -23,37 +21,20 @@ import type { VirtuosoHandle } from 'react-virtuoso'; const EventDiscussionPage: React.FC = () => { const { statusId } = eventDiscussionRoute.useParams(); - const intl = useIntl(); - const dispatch = useAppDispatch(); const { eventDiscussionCompose } = useComposeActions(); - const getStatus = useCallback(makeGetStatus(), []); - const status = useAppSelector((state) => getStatus(state, { id: statusId })); + const { data: status, isPending } = useStatus(statusId); const me = useAppSelector((state) => state.me); const descendantsIds = useDescendantsIds(statusId); - const [isLoaded, setIsLoaded] = useState(!!status); - const node = useRef(null); const scroller = useRef(null); - const fetchData = () => dispatch(fetchStatusWithContext(statusId, intl)); - useEffect(() => { - fetchData() - .then(() => { - setIsLoaded(true); - }) - .catch(() => { - setIsLoaded(true); - }); - }, [statusId]); - - useEffect(() => { - if (isLoaded && me) eventDiscussionCompose(`reply:${statusId}`, status!); - }, [isLoaded, me]); + if (status && me) eventDiscussionCompose(`reply:${statusId}`, status); + }, [status, me]); const handleMoveUp = (id: string) => { const index = descendantsIds.indexOf(id); @@ -100,7 +81,7 @@ const EventDiscussionPage: React.FC = () => { const hasDescendants = descendantsIds.length > 0; - if (!status && isLoaded) { + if (!status && isPending) { return ; } else if (!status) { return ; diff --git a/packages/nicolium/src/pages/statuses/event-information.tsx b/packages/nicolium/src/pages/statuses/event-information.tsx index 6571c49f0..0e6519e67 100644 --- a/packages/nicolium/src/pages/statuses/event-information.tsx +++ b/packages/nicolium/src/pages/statuses/event-information.tsx @@ -1,7 +1,6 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { FormattedDate, FormattedMessage, useIntl } from 'react-intl'; +import React, { useCallback } from 'react'; +import { FormattedDate, FormattedMessage } from 'react-intl'; -import { fetchStatus } from '@/actions/statuses'; import MissingIndicator from '@/components/missing-indicator'; import StatusContent from '@/components/statuses/status-content'; import HStack from '@/components/ui/hstack'; @@ -9,47 +8,29 @@ import Icon from '@/components/ui/icon'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import { eventInformationRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFrontendConfig } from '@/hooks/use-frontend-config'; -import { makeGetStatus } from '@/selectors'; +import { useStatus } from '@/queries/statuses/use-status'; import { useModalsActions } from '@/stores/modals'; const EventInformationPage: React.FC = () => { const { statusId } = eventInformationRoute.useParams(); - const dispatch = useAppDispatch(); - const getStatus = useCallback(makeGetStatus(), []); - const intl = useIntl(); - - const status = useAppSelector((state) => getStatus(state, { id: statusId }))!; + const { data: status, isPending } = useStatus(statusId); const { openModal } = useModalsActions(); const { tileServer } = useFrontendConfig(); - const [isLoaded, setIsLoaded] = useState(!!status); - - useEffect(() => { - dispatch(fetchStatus(statusId, intl)) - .then(() => { - setIsLoaded(true); - }) - .catch(() => { - setIsLoaded(true); - }); - }, [statusId]); - const handleShowMap: React.MouseEventHandler = (e) => { e.preventDefault(); openModal('EVENT_MAP', { - statusId: status.id, + statusId: statusId, }); }; const renderEventLocation = useCallback(() => { - const event = status.event!; - + if (!status?.event) return null; + const event = status.event; if (!event.location) return null; const text = [{event.location.name}]; @@ -107,8 +88,8 @@ const EventInformationPage: React.FC = () => { }, [status]); const renderEventDate = useCallback(() => { - const event = status.event!; - + if (!status?.event) return null; + const event = status.event; if (!event.start_time) return null; const startDate = new Date(event.start_time); @@ -158,7 +139,7 @@ const EventInformationPage: React.FC = () => { }, [status]); const renderLinks = useCallback(() => { - if (!status.event?.links?.length) return null; + if (!status?.event?.links?.length) return null; return ( @@ -182,7 +163,7 @@ const EventInformationPage: React.FC = () => { ); }, [status]); - if (!status && isLoaded) { + if (!status && isPending) { return ; } else if (!status) return null; diff --git a/packages/nicolium/src/pages/statuses/status.tsx b/packages/nicolium/src/pages/statuses/status.tsx index 077355e78..e2d38a331 100644 --- a/packages/nicolium/src/pages/statuses/status.tsx +++ b/packages/nicolium/src/pages/statuses/status.tsx @@ -1,9 +1,8 @@ import { Navigate } from '@tanstack/react-router'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { changeSetting } from '@/actions/settings'; -import { fetchStatusWithContext } from '@/actions/statuses'; import DropdownMenu, { type Menu } from '@/components/dropdown-menu'; import MissingIndicator from '@/components/missing-indicator'; import PullToRefresh from '@/components/pull-to-refresh'; @@ -13,8 +12,7 @@ import PlaceholderStatus from '@/features/placeholder/components/placeholder-sta import Thread from '@/features/status/components/thread'; import { statusRoute } from '@/features/ui/router'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { makeGetStatus } from '@/selectors'; +import { useStatus } from '@/queries/statuses/use-status'; import { useSettings } from '@/stores/settings'; const messages = defineMessages({ @@ -53,34 +51,25 @@ const StatusPage: React.FC = () => { const dispatch = useAppDispatch(); const intl = useIntl(); - const getStatus = useCallback(makeGetStatus(), []); - const status = useAppSelector((state) => getStatus(state, { id: statusId })); + const { + data: status, + isPending, + refetch, + refetchContext, + } = useStatus(statusId, { withContext: true }); + const [expandAllStatuses, setExpandAllStatuses] = useState<() => void>(); - const [isLoaded, setIsLoaded] = useState(!!status); const { displaySpoilers, threads: { displayMode }, } = useSettings(); - /** Fetch the status (and context) from the API. */ - const fetchData = () => { - return dispatch(fetchStatusWithContext(statusId, intl)); + const handleRefresh = () => { + refetch(); + refetchContext(); }; - // Load data. - useEffect(() => { - fetchData() - .then(() => { - setIsLoaded(true); - }) - .catch(() => { - setIsLoaded(true); - }); - }, [statusId]); - - const handleRefresh = () => fetchData(); - const items = useMemo(() => { const menu: Menu = [ { @@ -133,7 +122,7 @@ const StatusPage: React.FC = () => { ); } - if (!status && isLoaded) { + if (!status && !isPending) { return ; } else if (!status) { return ( diff --git a/packages/nicolium/src/queries/keys.ts b/packages/nicolium/src/queries/keys.ts index b3775823f..d89b10c35 100644 --- a/packages/nicolium/src/queries/keys.ts +++ b/packages/nicolium/src/queries/keys.ts @@ -4,6 +4,7 @@ import type { MinifiedGroupMember } from './groups/use-group-members'; import type { FilterType } from './notifications/use-notifications'; import type { DraftStatus } from './statuses/use-draft-statuses'; import type { MinifiedInteractionRequest } from './statuses/use-interaction-requests'; +import type { MinifiedContext } from './statuses/use-status'; import type { MinifiedStatusEdit } from './statuses/use-status-history'; import type { MinifiedEmojiReaction } from './statuses/use-status-interactions'; import type { TimelineEntry } from './timelines/use-home-timeline'; @@ -13,6 +14,7 @@ import type { MinifiedAdminReport, MinifiedConversation, } from './utils/minify-list'; +import type { NormalizedStatus } from '@/reducers/statuses'; import type { DataTag, InfiniteData } from '@tanstack/react-query'; import type { Account, @@ -235,6 +237,14 @@ const statusLists = { const statuses = { root: ['statuses'] as const, + show: (statusId: string) => { + const key = ['statuses', statusId] as const; + return key as TaggedKey; + }, + contexts: (statusId: string) => { + const key = ['statuses', 'contexts', statusId] as const; + return key as TaggedKey; + }, polls: { root: ['statuses', 'polls'] as const, show: (pollId: string) => { diff --git a/packages/nicolium/src/queries/statuses/use-status.ts b/packages/nicolium/src/queries/statuses/use-status.ts new file mode 100644 index 000000000..2593bebf4 --- /dev/null +++ b/packages/nicolium/src/queries/statuses/use-status.ts @@ -0,0 +1,118 @@ +import { useQuery, type UseQueryResult } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +import { importEntities } from '@/actions/importer'; +import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useClient } from '@/hooks/use-client'; +import { type NormalizedStatus, normalizeStatus } from '@/reducers/statuses'; +import { useContextsActions } from '@/stores/contexts'; + +import { useAccount } from '../accounts/use-account'; +import { queryKeys } from '../keys'; + +import type { Context, AsyncRefreshHeader, Account } from 'pl-api'; + +const minifyContext = ({ + ancestors, + descendants, + references, + ...context +}: Context & { asyncRefreshHeader: AsyncRefreshHeader | null }) => ({ + ancestor_ids: ancestors.map(({ id }) => id), + descendant_ids: descendants.map(({ id }) => id), + reference_ids: references.map(({ id }) => id), + ...context, +}); + +type MinifiedContext = ReturnType; + +type SelectedStatus = NormalizedStatus & { + account: Account; + reblog?: SelectedStatus; + quote?: SelectedStatus; +}; + +const useStatusQuery = (statusId?: string) => { + const client = useClient(); + const dispatch = useAppDispatch(); + + const statusQuery = useQuery({ + queryKey: queryKeys.statuses.show(statusId!), + queryFn: () => + client.statuses.getStatus(statusId!).then((status) => { + const normalizedStatus = normalizeStatus(status); + + dispatch({ + type: 'STATUS_IMPORT', + status, + skipQueryDataUpdate: true, + }); + dispatch(importEntities({ statuses: [status] }, { withParents: false })); + + return normalizedStatus; + }), + enabled: !!statusId, + }); + + const account = useAccount(statusQuery.data?.account_id ?? undefined); + + return useMemo(() => { + if (!statusQuery.data) return statusQuery; + return { + ...statusQuery, + data: { + ...statusQuery.data, + account: account.data!, + }, + }; + }, [statusQuery.data, account.data]) as unknown as UseQueryResult; +}; + +const useStatus = ( + statusId?: string, + { withContext }: { withContext?: boolean; contextType?: string } = {}, +) => { + const { refetch: refetchContext } = useStatusContext(withContext ? statusId : undefined); + + const statusQuery = useStatusQuery(statusId); + + const reblogQuery = useStatusQuery(statusQuery.data?.reblog_id ?? undefined); + const quoteQuery = useStatusQuery(statusQuery.data?.quote_id ?? undefined); + + const account = useAccount(statusQuery.data?.account_id ?? undefined); + + return useMemo(() => { + if (!statusQuery.data) return { ...statusQuery, refetchContext }; + return { + ...statusQuery, + data: { + ...statusQuery.data, + account: account.data!, + reblog: reblogQuery.data, + quote: quoteQuery.data, + }, + refetchContext, + }; + }, [statusQuery.data, reblogQuery.data, quoteQuery.data, account.data]); +}; + +const useStatusContext = (statusId?: string) => { + const client = useClient(); + const dispatch = useAppDispatch(); + const { importContext } = useContextsActions(); + + return useQuery({ + queryKey: queryKeys.statuses.contexts(statusId!), + queryFn: () => + client.statuses.getContext(statusId!).then((context) => { + const { ancestors, descendants, references } = context; + const statuses = [...ancestors, ...descendants, ...references]; + importContext(statusId!, context); + dispatch(importEntities({ statuses })); + return minifyContext(context); + }), + enabled: !!statusId, + }); +}; + +export { useStatus, useStatusContext, type MinifiedContext, type SelectedStatus }; diff --git a/packages/nicolium/src/reducers/statuses.ts b/packages/nicolium/src/reducers/statuses.ts index ae336a4f0..d6344d50a 100644 --- a/packages/nicolium/src/reducers/statuses.ts +++ b/packages/nicolium/src/reducers/statuses.ts @@ -48,6 +48,8 @@ import { STATUS_DELETE_SUCCESS, } from '@/actions/statuses'; import { TIMELINE_DELETE, type TimelineAction } from '@/actions/timelines'; +import { queryClient } from '@/queries/client'; +import { queryKeys } from '@/queries/keys'; import { simulateEmojiReact, simulateUnEmojiReact } from '@/utils/emoji-reacts'; import { unescapeHTML } from '@/utils/html'; @@ -188,10 +190,18 @@ type NormalizedStatus = ReturnType; type State = Record; -const importStatus = (state: State, status: BaseStatus | StatusWithoutAccount) => { +const importStatus = ( + state: State, + status: BaseStatus | StatusWithoutAccount, + skipQueryDataUpdate?: boolean, +) => { const oldStatus = state[status.id]; + const normalizedStatus = normalizeStatus(status, oldStatus); - state[status.id] = normalizeStatus(status, oldStatus); + state[status.id] = normalizedStatus; + + if (skipQueryDataUpdate) return; + queryClient.setQueryData(queryKeys.statuses.show(status.id), normalizedStatus); }; const importStatuses = (state: State, statuses: Array) => { @@ -296,7 +306,7 @@ const statuses = ( switch (action.type) { case STATUS_IMPORT: return create(state, (draft) => { - importStatus(draft, action.status); + importStatus(draft, action.status, action.skipQueryDataUpdate); }); case STATUSES_IMPORT: return create(state, (draft) => { diff --git a/packages/pl-api/lib/main.ts b/packages/pl-api/lib/main.ts index 3b7c16c63..b195ef2ff 100644 --- a/packages/pl-api/lib/main.ts +++ b/packages/pl-api/lib/main.ts @@ -1,7 +1,7 @@ export { default as PlApiClient } from './client'; export { PlApiBaseClient } from './client-base'; export { PlApiDirectoryClient } from './directory-client'; -export { type Response as PlApiResponse } from './request'; +export { type Response as PlApiResponse, type AsyncRefreshHeader } from './request'; export * from './entities'; export * from './features'; export * from './params';