diff --git a/src/@types/react-navigation.d.ts b/src/@types/react-navigation.d.ts index 8cccba4f..492bda1f 100644 --- a/src/@types/react-navigation.d.ts +++ b/src/@types/react-navigation.d.ts @@ -5,6 +5,7 @@ declare namespace Nav { | { type: 'status' queryKey: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline status: Mastodon.Status } | { @@ -86,6 +87,7 @@ declare namespace Nav { 'Tab-Shared-Search': undefined 'Tab-Shared-Toot': { toot: Mastodon.Status + rootQueryKey: any } } diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index 5060d797..c62fae7a 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -1,5 +1,5 @@ import ComponentSeparator from '@components/Separator' -import { useScrollToTop } from '@react-navigation/native' +import { useNavigation, useScrollToTop } from '@react-navigation/native' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { getLocalActiveIndex } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' @@ -27,6 +27,7 @@ export interface Props { hashtag?: Mastodon.Tag['name'] list?: Mastodon.List['id'] toot?: Mastodon.Status['id'] + rootQueryKey?: QueryKeyTimeline account?: Mastodon.Account['id'] disableRefresh?: boolean disableInfinity?: boolean @@ -38,6 +39,7 @@ const Timeline: React.FC = ({ hashtag, list, toot, + rootQueryKey, account, disableRefresh = false, disableInfinity = false, @@ -108,6 +110,13 @@ const Timeline: React.FC = ({ 350 ) }, []) + // Auto go back when toot page is empty + const navigation = useNavigation() + useEffect(() => { + if (toot && isSuccess && flattenData.length === 0) { + navigation.goBack() + } + }, [isSuccess, flattenData.length]) const keyExtractor = useCallback(({ id }) => id, []) const renderItem = useCallback( @@ -127,6 +136,7 @@ const Timeline: React.FC = ({ item={item} queryKey={queryKey} {...(toot === item.id && { highlighted: true })} + {...(toot && { rootQueryKey })} // @ts-ignore {...(data?.pages[0].pinned && { pinned: data?.pages[0].pinned })} /> diff --git a/src/components/Timeline/Conversation.tsx b/src/components/Timeline/Conversation.tsx index f3efd8a7..4b7fc193 100644 --- a/src/components/Timeline/Conversation.tsx +++ b/src/components/Timeline/Conversation.tsx @@ -86,7 +86,8 @@ const TimelineConversation: React.FC = ({ if (conversation.last_status) { conversation.unread && mutate() navigation.push('Tab-Shared-Toot', { - toot: conversation.last_status + toot: conversation.last_status, + rootQueryKey: queryKey }) } }, []) diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 91cb330b..472da2ec 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -20,17 +20,19 @@ import { useSelector } from 'react-redux' export interface Props { item: Mastodon.Status & { isPinned?: boolean } queryKey?: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline origin?: string highlighted?: boolean disableDetails?: boolean disableOnPress?: boolean - pinned: Mastodon.Status['id'][] + pinned?: Mastodon.Status['id'][] } // When the poll is long const TimelineDefault: React.FC = ({ item, queryKey, + rootQueryKey, origin, highlighted = false, disableDetails = false, @@ -55,7 +57,8 @@ const TimelineDefault: React.FC = ({ !disableOnPress && !highlighted && navigation.push('Tab-Shared-Toot', { - toot: actualStatus + toot: actualStatus, + rootQueryKey: queryKey }) }, []) @@ -72,6 +75,7 @@ const TimelineDefault: React.FC = ({ } ]} onPress={onPress} + disabled={queryKey && queryKey[1].toot !== undefined} > {item.reblog ? ( @@ -86,6 +90,7 @@ const TimelineDefault: React.FC = ({ /> @@ -109,6 +114,7 @@ const TimelineDefault: React.FC = ({ {queryKey && actualStatus.poll ? ( = ({ > = ({ analytics('timeline_notification_press') notification.status && navigation.push('Tab-Shared-Toot', { - toot: notification.status + toot: notification.status, + rootQueryKey: queryKey }) }, []) diff --git a/src/components/Timeline/Shared/Actions.tsx b/src/components/Timeline/Shared/Actions.tsx index b4ee1ab9..07f26f36 100644 --- a/src/components/Timeline/Shared/Actions.tsx +++ b/src/components/Timeline/Shared/Actions.tsx @@ -17,6 +17,7 @@ import { useQueryClient } from 'react-query' export interface Props { queryKey: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline status: Mastodon.Status accts: Mastodon.Account['acct'][] // When replying to conversations reblog: boolean @@ -24,6 +25,7 @@ export interface Props { const TimelineActions: React.FC = ({ queryKey, + rootQueryKey, status, accts, reblog @@ -120,6 +122,7 @@ const TimelineActions: React.FC = ({ mutation.mutate({ type: 'updateStatusProperty', queryKey, + rootQueryKey, id: status.id, reblog, payload: { @@ -139,6 +142,7 @@ const TimelineActions: React.FC = ({ mutation.mutate({ type: 'updateStatusProperty', queryKey, + rootQueryKey, id: status.id, reblog, payload: { @@ -157,6 +161,7 @@ const TimelineActions: React.FC = ({ mutation.mutate({ type: 'updateStatusProperty', queryKey, + rootQueryKey, id: status.id, reblog, payload: { diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx index d7d2917f..58fa423b 100644 --- a/src/components/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timeline/Shared/HeaderDefault.tsx @@ -1,22 +1,27 @@ +import Icon from '@components/Icon' +import { useNavigation } from '@react-navigation/native' +import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' +import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { Pressable, StyleSheet, View } from 'react-native' import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedCreated from './HeaderShared/Created' -import HeaderSharedVisibility from './HeaderShared/Visibility' -import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import HeaderSharedMuted from './HeaderShared/Muted' -import { useNavigation } from '@react-navigation/native' -import Icon from '@components/Icon' -import { useTheme } from '@utils/styles/ThemeManager' +import HeaderSharedVisibility from './HeaderShared/Visibility' export interface Props { queryKey?: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline status: Mastodon.Status } -const TimelineHeaderDefault: React.FC = ({ queryKey, status }) => { +const TimelineHeaderDefault: React.FC = ({ + queryKey, + rootQueryKey, + status +}) => { const navigation = useNavigation() const { theme } = useTheme() @@ -38,6 +43,7 @@ const TimelineHeaderDefault: React.FC = ({ queryKey, status }) => { onPress={() => navigation.navigate('Screen-Actions', { queryKey, + rootQueryKey, status, url: status.url || status.uri, type: 'status' diff --git a/src/components/Timeline/Shared/Poll.tsx b/src/components/Timeline/Shared/Poll.tsx index a5c2934e..1538b080 100644 --- a/src/components/Timeline/Shared/Poll.tsx +++ b/src/components/Timeline/Shared/Poll.tsx @@ -10,6 +10,7 @@ import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline' +import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusProperty' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import { maxBy } from 'lodash' @@ -20,6 +21,7 @@ import { useQueryClient } from 'react-query' export interface Props { queryKey: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline statusId: Mastodon.Status['id'] poll: NonNullable reblog: boolean @@ -28,6 +30,7 @@ export interface Props { const TimelinePoll: React.FC = ({ queryKey, + rootQueryKey, statusId, poll, reblog, @@ -43,7 +46,19 @@ const TimelinePoll: React.FC = ({ const queryClient = useQueryClient() const mutation = useTimelineMutation({ queryClient, - onSuccess: true, + onSuccess: ({ body }, params) => { + const theParams = params as MutationVarsTimelineUpdateStatusProperty + queryClient.cancelQueries(queryKey) + rootQueryKey && queryClient.cancelQueries(rootQueryKey) + + haptics('Success') + switch (theParams.payload.property) { + case 'poll': + theParams.payload.data = (body as unknown) as Mastodon.Poll + updateStatusProperty({ queryClient, ...theParams }) + break + } + }, onError: (err: any, params) => { const theParams = params as MutationVarsTimelineUpdateStatusProperty haptics('Error') @@ -76,6 +91,7 @@ const TimelinePoll: React.FC = ({ mutation.mutate({ type: 'updateStatusProperty', queryKey, + rootQueryKey, id: statusId, reblog, payload: { @@ -102,6 +118,7 @@ const TimelinePoll: React.FC = ({ mutation.mutate({ type: 'updateStatusProperty', queryKey, + rootQueryKey, id: statusId, reblog, payload: { diff --git a/src/screens/Actions/Account.tsx b/src/screens/Actions/Account.tsx index f87adc76..ef6379bb 100644 --- a/src/screens/Actions/Account.tsx +++ b/src/screens/Actions/Account.tsx @@ -13,11 +13,17 @@ import { useQueryClient } from 'react-query' export interface Props { queryKey?: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline account: Mastodon.Account dismiss: () => void } -const ActionsAccount: React.FC = ({ queryKey, account, dismiss }) => { +const ActionsAccount: React.FC = ({ + queryKey, + rootQueryKey, + account, + dismiss +}) => { const { t } = useTranslation('componentTimeline') const queryClient = useQueryClient() @@ -59,6 +65,7 @@ const ActionsAccount: React.FC = ({ queryKey, account, dismiss }) => { }, onSettled: () => { queryKey && queryClient.invalidateQueries(queryKey) + rootQueryKey && queryClient.invalidateQueries(rootQueryKey) } }) diff --git a/src/screens/Actions/Domain.tsx b/src/screens/Actions/Domain.tsx index ffae7de6..6c0abe38 100644 --- a/src/screens/Actions/Domain.tsx +++ b/src/screens/Actions/Domain.tsx @@ -14,11 +14,17 @@ import { useQueryClient } from 'react-query' export interface Props { queryKey: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline domain: string dismiss: () => void } -const ActionsDomain: React.FC = ({ queryKey, domain, dismiss }) => { +const ActionsDomain: React.FC = ({ + queryKey, + rootQueryKey, + domain, + dismiss +}) => { const { t } = useTranslation('componentTimeline') const queryClient = useQueryClient() const mutation = useTimelineMutation({ @@ -31,6 +37,7 @@ const ActionsDomain: React.FC = ({ queryKey, domain, dismiss }) => { }) }) queryClient.invalidateQueries(queryKey) + rootQueryKey && queryClient.invalidateQueries(rootQueryKey) } }) diff --git a/src/screens/Actions/Root.tsx b/src/screens/Actions/Root.tsx index 29258de9..b47348d2 100644 --- a/src/screens/Actions/Root.tsx +++ b/src/screens/Actions/Root.tsx @@ -107,6 +107,7 @@ const ScreenActionsRoot = React.memo( {!sameAccount && ( @@ -115,6 +116,7 @@ const ScreenActionsRoot = React.memo( @@ -122,6 +124,7 @@ const ScreenActionsRoot = React.memo( {!sameDomain && statusDomain && ( diff --git a/src/screens/Actions/Status.tsx b/src/screens/Actions/Status.tsx index 68954ca7..48be0715 100644 --- a/src/screens/Actions/Status.tsx +++ b/src/screens/Actions/Status.tsx @@ -11,10 +11,12 @@ import { } from '@utils/queryHooks/timeline' import analytics from '@components/analytics' import { StackNavigationProp } from '@react-navigation/stack' +import deleteItem from '@utils/queryHooks/timeline/deleteItem' export interface Props { navigation: StackNavigationProp queryKey: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline status: Mastodon.Status dismiss: () => void } @@ -22,6 +24,7 @@ export interface Props { const ActionsStatus: React.FC = ({ navigation, queryKey, + rootQueryKey, status, dismiss }) => { @@ -66,6 +69,7 @@ const ActionsStatus: React.FC = ({ type: 'deleteItem', source: 'statuses', queryKey, + rootQueryKey, id: status.id }) }} @@ -106,6 +110,11 @@ const ActionsStatus: React.FC = ({ queryKey, id: status.id }) + deleteItem({ + queryClient, + rootQueryKey, + id: status.id + }) if (res.body.id) { // @ts-ignore navigation.navigate('Screen-Compose', { @@ -131,6 +140,7 @@ const ActionsStatus: React.FC = ({ mutation.mutate({ type: 'updateStatusProperty', queryKey, + rootQueryKey, id: status.id, payload: { property: 'muted', @@ -158,6 +168,7 @@ const ActionsStatus: React.FC = ({ mutation.mutate({ type: 'updateStatusProperty', queryKey, + rootQueryKey, id: status.id, payload: { property: 'pinned', diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index c92ff11c..9494063b 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -4,10 +4,18 @@ import { SharedTootProp } from './sharedScreens' const TabSharedToot: React.FC = ({ route: { - params: { toot } + params: { toot, rootQueryKey } } }) => { - return + return ( + + ) } export default TabSharedToot diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts index 438b40ad..385ad182 100644 --- a/src/utils/queryHooks/timeline.ts +++ b/src/utils/queryHooks/timeline.ts @@ -226,6 +226,7 @@ export type MutationVarsTimelineUpdateStatusProperty = { // This is status in general, including "status" inside conversation and notification type: 'updateStatusProperty' queryKey: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline id: Mastodon.Status['id'] | Mastodon.Poll['id'] reblog?: boolean payload: @@ -264,7 +265,8 @@ export type MutationVarsTimelineDeleteItem = { // This is for deleting status and conversation type: 'deleteItem' source: 'statuses' | 'conversations' - queryKey: QueryKeyTimeline + queryKey?: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline id: Mastodon.Conversation['id'] } @@ -366,7 +368,7 @@ const useTimelineMutation = ({ onError?: MutationOptionsTimeline['onError'] onMutate?: boolean onSettled?: MutationOptionsTimeline['onSettled'] - onSuccess?: MutationOptionsTimeline['onSuccess'] | boolean + onSuccess?: MutationOptionsTimeline['onSuccess'] }) => { return useMutation< { body: Mastodon.Conversation | Mastodon.Notification | Mastodon.Status }, @@ -375,27 +377,7 @@ const useTimelineMutation = ({ >(mutationFunction, { onError, onSettled, - ...(typeof onSuccess === 'function' - ? { onSuccess } - : onSuccess - ? { - onSuccess: ({ body }, params) => { - queryClient.cancelQueries(params.queryKey) - - haptics('Success') - switch (params.type) { - case 'updateStatusProperty': - switch (params.payload.property) { - case 'poll': - params.payload.data = (body as unknown) as Mastodon.Poll - updateStatusProperty({ queryClient, ...params }) - break - } - break - } - } - } - : undefined), + onSuccess, ...(onMutate && { onMutate: params => { queryClient.cancelQueries(params.queryKey) diff --git a/src/utils/queryHooks/timeline/deleteItem.ts b/src/utils/queryHooks/timeline/deleteItem.ts index b333196a..5acb85b3 100644 --- a/src/utils/queryHooks/timeline/deleteItem.ts +++ b/src/utils/queryHooks/timeline/deleteItem.ts @@ -1,24 +1,45 @@ import { InfiniteData, QueryClient } from 'react-query' -import { QueryKeyTimeline, TimelineData } from '../timeline' +import { MutationVarsTimelineDeleteItem } from '../timeline' const deleteItem = ({ queryClient, queryKey, + rootQueryKey, id }: { queryClient: QueryClient - queryKey: QueryKeyTimeline - id: Mastodon.Status['id'] + queryKey?: MutationVarsTimelineDeleteItem['queryKey'] + rootQueryKey?: MutationVarsTimelineDeleteItem['rootQueryKey'] + id: MutationVarsTimelineDeleteItem['id'] }) => { - queryClient.setQueryData | undefined>(queryKey, old => { - if (old) { - old.pages = old.pages.map(page => { - page.body = page.body.filter((item: Mastodon.Status) => item.id !== id) - return page - }) - return old - } - }) + queryKey && + queryClient.setQueryData | undefined>(queryKey, old => { + if (old) { + old.pages = old.pages.map(page => { + page.body = page.body.filter( + (item: Mastodon.Status) => item.id !== id + ) + return page + }) + return old + } + }) + + rootQueryKey && + queryClient.setQueryData | undefined>( + rootQueryKey, + old => { + if (old) { + old.pages = old.pages.map(page => { + page.body = page.body.filter( + (item: Mastodon.Status) => item.id !== id + ) + return page + }) + return old + } + } + ) } export default deleteItem diff --git a/src/utils/queryHooks/timeline/updateStatusProperty.ts b/src/utils/queryHooks/timeline/updateStatusProperty.ts index 52602c69..0263bdb8 100644 --- a/src/utils/queryHooks/timeline/updateStatusProperty.ts +++ b/src/utils/queryHooks/timeline/updateStatusProperty.ts @@ -11,12 +11,14 @@ import updateStatus from './update/status' const updateStatusProperty = ({ queryClient, queryKey, + rootQueryKey, id, reblog, payload }: { queryClient: QueryClient queryKey: MutationVarsTimelineUpdateStatusProperty['queryKey'] + rootQueryKey?: MutationVarsTimelineUpdateStatusProperty['rootQueryKey'] id: MutationVarsTimelineUpdateStatusProperty['id'] reblog?: MutationVarsTimelineUpdateStatusProperty['reblog'] payload: MutationVarsTimelineUpdateStatusProperty['payload'] @@ -72,6 +74,60 @@ const updateStatusProperty = ({ return old } ) + + rootQueryKey && + queryClient.setQueryData | undefined>( + rootQueryKey, + old => { + if (old) { + let foundToot = false + old.pages = old.pages.map(page => { + // Skip rest of the pages if any toot is found + if (foundToot) { + return page + } else { + if ( + typeof (page.body as Mastodon.Conversation[])[0].unread === + 'boolean' + ) { + const items = page.body as Mastodon.Conversation[] + const tootIndex = findIndex(items, ['last_status.id', id]) + if (tootIndex >= 0) { + foundToot = true + updateConversation({ item: items[tootIndex], payload }) + } + return page + } else if ( + typeof (page.body as Mastodon.Notification[])[0].type === + 'string' + ) { + const items = page.body as Mastodon.Notification[] + const tootIndex = findIndex(items, ['status.id', id]) + if (tootIndex >= 0) { + foundToot = true + updateNotification({ item: items[tootIndex], payload }) + } + } else { + const items = page.body as Mastodon.Status[] + const tootIndex = findIndex(items, [ + reblog ? 'reblog.id' : 'id', + id + ]) + // if favouriets page and notifications page, remove the item instead + if (tootIndex >= 0) { + foundToot = true + updateStatus({ item: items[tootIndex], reblog, payload }) + } + } + + return page + } + }) + } + + return old + } + ) } export default updateStatusProperty