From 13303c42696d8dfc67d62c235e14ced8adfef628 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 31 Dec 2022 15:53:02 +0100 Subject: [PATCH] Early demo of #638 Actions are not working yet --- src/components/Timeline/Default.tsx | 7 +- src/components/Timeline/Shared/Context.tsx | 1 + .../Timeline/Shared/HeaderDefault.tsx | 10 +- src/screens/Tabs/Shared/Toot.tsx | 242 +++++++++++++++--- src/utils/queryHooks/timeline.ts | 63 +---- 5 files changed, 221 insertions(+), 102 deletions(-) diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 26c8da51..ae264a35 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -37,6 +37,7 @@ export interface Props { disableDetails?: boolean disableOnPress?: boolean isConversation?: boolean + isRemote?: boolean } // When the poll is long @@ -47,7 +48,8 @@ const TimelineDefault: React.FC = ({ highlighted = false, disableDetails = false, disableOnPress = false, - isConversation = false + isConversation = false, + isRemote = false }) => { const status = item.reblog ? item.reblog : item const rawContent = useRef([]) @@ -175,7 +177,8 @@ const TimelineDefault: React.FC = ({ inThread: queryKey?.[1].page === 'Toot', disableDetails, disableOnPress, - isConversation + isConversation, + isRemote }} > {disableOnPress ? ( diff --git a/src/components/Timeline/Shared/Context.tsx b/src/components/Timeline/Shared/Context.tsx index 80f1079e..ea689fc2 100644 --- a/src/components/Timeline/Shared/Context.tsx +++ b/src/components/Timeline/Shared/Context.tsx @@ -21,6 +21,7 @@ type StatusContextType = { disableDetails?: boolean disableOnPress?: boolean isConversation?: boolean + isRemote?: boolean } const StatusContext = createContext({} as StatusContextType) diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx index c96bf9ef..e5795207 100644 --- a/src/components/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timeline/Shared/HeaderDefault.tsx @@ -17,7 +17,7 @@ import HeaderSharedReplies from './HeaderShared/Replies' import HeaderSharedVisibility from './HeaderShared/Visibility' const TimelineHeaderDefault: React.FC = () => { - const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent } = + const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent, isRemote } = useContext(StatusContext) if (!status) return null @@ -58,6 +58,14 @@ const TimelineHeaderDefault: React.FC = () => { : { marginTop: StyleConstants.Spacing.XS, marginBottom: StyleConstants.Spacing.S }) }} > + {isRemote ? ( + + ) : null} > = ({ const { colors } = useTheme() const { t } = useTranslation(['componentTimeline', 'screenTabs']) + const [hasRemoteContent, setHasRemoteContent] = useState(false) + useEffect(() => { navigation.setOptions({ - title: t('screenTabs:shared.toot.name'), + headerTitle: () => ( + + {hasRemoteContent ? ( + + ) : null} + + + ), headerLeft: () => navigation.goBack()} /> }) - }, []) + }, [hasRemoteContent]) const flRef = useRef(null) const scrolled = useRef(false) - const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }] - const { data, status, refetch } = useTootQuery({ - ...queryKey[1], - options: { - meta: { toot }, + const finalData = useRef<(Mastodon.Status & { _level?: number; _remote?: boolean })[]>([ + { ...toot, _level: 0, _remote: false } + ]) + const highlightIndex = useRef(0) + const queryLocal = useQuery( + ['Timeline', { page: 'Toot', toot: toot.id, remote: false }], + async () => { + const context = await apiInstance<{ + ancestors: Mastodon.Status[] + descendants: Mastodon.Status[] + }>({ + method: 'get', + url: `statuses/${toot.id}/context` + }) + + const statuses: (Mastodon.Status & { _level?: number })[] = [ + ...context.body.ancestors, + toot, + ...context.body.descendants + ] + + const highlight = context.body.ancestors.length + highlightIndex.current = highlight + + for (const [index, status] of statuses.entries()) { + if (index < highlight || status.id === toot.id) { + statuses[index]._level = 0 + continue + } + + const repliedLevel = statuses.find(s => s.id === status.in_reply_to_id)?._level + statuses[index]._level = (repliedLevel || 0) + 1 + } + + return { pages: [{ body: statuses }] } + }, + { + staleTime: 0, + refetchOnMount: true, onSuccess: data => { if (data.pages[0].body.length < 1) { navigation.goBack() return } - if (!scrolled.current) { - scrolled.current = true - const pointer = data.pages[0].body.findIndex(({ id }) => id === toot.id) - if (pointer < 1) return - const length = flRef.current?.props.data?.length - if (!length) return - try { - setTimeout(() => { - try { - flRef.current?.scrollToIndex({ - index: pointer, - viewOffset: 100 - }) - } catch {} - }, 500) - } catch (error) { - return + if (finalData.current.length < data.pages[0].body.length) { + // if the remote has been loaded first + finalData.current = data.pages[0].body + + if (!scrolled.current) { + scrolled.current = true + const pointer = data.pages[0].body.findIndex(({ id }) => id === toot.id) + if (pointer < 1) return + const length = flRef.current?.props.data?.length + if (!length) return + try { + setTimeout(() => { + try { + flRef.current?.scrollToIndex({ + index: pointer, + viewOffset: 100 + }) + } catch {} + }, 500) + } catch (error) { + return + } } } } } - }) + ) + useQuery( + ['Timeline', { page: 'Toot', toot: toot.id, remote: true }], + async () => { + let context: + | { + ancestors: Mastodon.Status[] + descendants: Mastodon.Status[] + } + | undefined + + try { + const domain = getHost(toot.url || toot.uri) + if (!domain?.length) { + throw new Error() + } + const id = (toot.url || toot.uri).match(new RegExp(/\/([0-9]+)$/))?.[1] + if (!id?.length) { + throw new Error() + } + + context = await apiGeneral<{ + ancestors: Mastodon.Status[] + descendants: Mastodon.Status[] + }>({ + method: 'get', + domain, + url: `api/v1/statuses/${id}/context` + }).then(res => res.body) + } catch {} + + if (!context) { + throw new Error() + } + + const statuses: (Mastodon.Status & { _level?: number })[] = [ + ...context.ancestors, + toot, + ...context.descendants + ] + + const highlight = context.ancestors.length + highlightIndex.current = highlight + + for (const [index, status] of statuses.entries()) { + if (index < highlight || status.id === toot.id) { + statuses[index]._level = 0 + continue + } + + const repliedLevel = statuses.find(s => s.id === status.in_reply_to_id)?._level + statuses[index]._level = (repliedLevel || 0) + 1 + } + + return { pages: [{ body: statuses }] } + }, + { + enabled: toot.account.acct !== toot.account.username, // When on the same instance, these two values are the same + staleTime: 0, + refetchOnMount: false, + onSuccess: data => { + if (finalData.current.length < 1 && data.pages[0].body.length < 1) { + navigation.goBack() + return + } + + if (finalData.current?.length < data.pages[0].body.length) { + finalData.current = data.pages[0].body.map(remote => { + const localMatch = finalData.current?.find(local => local.uri === remote.uri) + return localMatch ? { ...localMatch, _remote: false } : { ...remote, _remote: true } + }) + setHasRemoteContent(true) + } + + scrolled.current = true + const pointer = data.pages[0].body.findIndex(({ id }) => id === toot.id) + if (pointer < 1) return + const length = flRef.current?.props.data?.length + if (!length) return + try { + setTimeout(() => { + try { + flRef.current?.scrollToIndex({ + index: pointer, + viewOffset: 100 + }) + } catch {} + }, 500) + } catch (error) { + return + } + } + } + ) const empty = () => { - switch (status) { - case 'loading': - return + switch (queryLocal.status) { case 'error': return ( <> @@ -88,7 +241,7 @@ const TabSharedToot: React.FC> = ({