From 4004d9eeda692ae37ccd2beee81f8f02dad533e1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 12 Aug 2024 16:18:06 -0500 Subject: [PATCH] Prefetch data needed for interaction settings --- src/components/Button.tsx | 80 +++++++++++++------ src/components/Menu/index.tsx | 10 ++- src/components/WhoCanReply.tsx | 22 ++++- .../dialogs/PostInteractionSettingsDialog.tsx | 44 +++++++++- src/state/queries/postgate/index.ts | 4 +- src/state/queries/threadgate/index.ts | 32 +++++--- src/view/com/util/forms/PostDropdownBtn.tsx | 23 +++++- 7 files changed, 169 insertions(+), 46 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 4fe0ab4b12..aa1f3f1867 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,6 +1,8 @@ import React from 'react' import { AccessibilityProps, + GestureResponderEvent, + MouseEvent, Pressable, PressableProps, StyleProp, @@ -65,7 +67,15 @@ type NonTextElements = export type ButtonProps = Pick< PressableProps, - 'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop' + | 'disabled' + | 'onPress' + | 'testID' + | 'onLongPress' + | 'hitSlop' + | 'onHoverIn' + | 'onHoverOut' + | 'onPressIn' + | 'onPressOut' > & AccessibilityProps & VariantProps & { @@ -115,30 +125,50 @@ export const Button = React.forwardRef( focused: false, }) - const onPressIn = React.useCallback(() => { - setState(s => ({ - ...s, - pressed: true, - })) - }, [setState]) - const onPressOut = React.useCallback(() => { - setState(s => ({ - ...s, - pressed: false, - })) - }, [setState]) - const onHoverIn = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: true, - })) - }, [setState]) - const onHoverOut = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: false, - })) - }, [setState]) + const onPressInOuter = rest.onPressIn + const onPressIn = React.useCallback( + (e: GestureResponderEvent) => { + setState(s => ({ + ...s, + pressed: true, + })) + onPressInOuter?.(e) + }, + [setState, onPressInOuter], + ) + const onPressOutOuter = rest.onPressOut + const onPressOut = React.useCallback( + (e: GestureResponderEvent) => { + setState(s => ({ + ...s, + pressed: false, + })) + onPressOutOuter?.(e) + }, + [setState, onPressOutOuter], + ) + const onHoverInOuter = rest.onHoverIn + const onHoverIn = React.useCallback( + (e: MouseEvent) => { + setState(s => ({ + ...s, + hovered: true, + })) + onHoverInOuter?.(e) + }, + [setState, onHoverInOuter], + ) + const onHoverOutOuter = rest.onHoverOut + const onHoverOut = React.useCallback( + (e: MouseEvent) => { + setState(s => ({ + ...s, + hovered: false, + })) + onHoverOutOuter?.(e) + }, + [setState, onHoverOutOuter], + ) const onFocus = React.useCallback(() => { setState(s => ({ ...s, diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index b0b9d1cda7..754f68c97b 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -125,8 +125,14 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) { }} onFocus={onFocus} onBlur={onBlur} - onPressIn={onPressIn} - onPressOut={onPressOut} + onPressIn={e => { + onPressIn() + rest.onPressIn?.(e) + }} + onPressOut={e => { + onPressOut() + rest.onPressOut?.(e) + }} style={[ a.flex_row, a.align_center, diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx index 72f373bc09..37c824083f 100644 --- a/src/components/WhoCanReply.tsx +++ b/src/components/WhoCanReply.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Keyboard, StyleProp, View, ViewStyle} from 'react-native' +import {Keyboard, Platform, StyleProp, View, ViewStyle} from 'react-native' import { AppBskyFeedDefs, AppBskyFeedPost, @@ -20,12 +20,15 @@ import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog' +import { + PostInteractionSettingsDialog, + usePrefetchPostInteractionSettings, +} from '#/components/dialogs/PostInteractionSettingsDialog' import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' -import {PostInteractionSettingsDialog} from './dialogs/PostInteractionSettingsDialog' import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil' interface WhoCanReplyProps { @@ -52,6 +55,11 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { return threadgateViewToAllowUISetting(post.threadgate) }, [post.threadgate]) + const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({ + postUri: post.uri, + rootPostUri: rootUri, + }) + const anyoneCanReply = settings.length === 1 && settings[0].type === 'everybody' const noOneCanReply = settings.length === 1 && settings[0].type === 'nobody' @@ -79,6 +87,16 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`) } onPress={onPressOpen} + {...(isThreadAuthor + ? Platform.select({ + web: { + onHoverIn: prefetchPostInteractionSettings, + }, + native: { + onPressIn: prefetchPostInteractionSettings, + }, + }) + : {})} hitSlop={HITSLOP_10}> {({hovered}) => ( diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index 0b5d8fadca..fcd6d325ef 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -8,11 +8,15 @@ import { } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' import isEqual from 'lodash.isequal' import {logger} from '#/logger' +import {STALE} from '#/state/queries' import {useMyListsQuery} from '#/state/queries/my-lists' import { + createPostgateQueryKey, + getPostgateRecord, usePostgateQuery, useWritePostgateMutation, } from '#/state/queries/postgate' @@ -21,18 +25,19 @@ import { embeddingRules, } from '#/state/queries/postgate/util' import { + createThreadgateViewQueryKey, + getThreadgateView, ThreadgateAllowUISetting, threadgateViewToAllowUISetting, useSetThreadgateAllowMutation, useThreadgateViewQuery, } from '#/state/queries/threadgate' -import {useSession} from '#/state/session' +import {useAgent, useSession} from '#/state/session' import * as Toast from '#/view/com/util/toast' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Divider} from '#/components/Divider' -import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {Loader} from '#/components/Loader' @@ -116,14 +121,13 @@ export function PostInteractionSettingsDialogControlledInner( const {mutateAsync: writePostgateRecord} = useWritePostgateMutation() const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation() - const naturalLoading = useDelayedLoading(500) // TODO const [editedPostgate, setEditedPostgate] = React.useState() const [editedAllowUISettings, setEditedAllowUISettings] = React.useState() - const isLoading = isLoadingThreadgate || isLoadingPostgate || naturalLoading + const isLoading = isLoadingThreadgate || isLoadingPostgate const threadgateView = threadgateViewLoaded || props.initialThreadgateView const isThreadgateOwnedByViewer = React.useMemo(() => { if (AppBskyFeedThreadgate.isRecord(threadgateView?.record)) { @@ -504,3 +508,35 @@ function Selectable({ ) } + +export function usePrefetchPostInteractionSettings({ + postUri, + rootPostUri, +}: { + postUri: string + rootPostUri: string +}) { + const queryClient = useQueryClient() + const agent = useAgent() + + return React.useCallback(async () => { + try { + await Promise.all([ + queryClient.prefetchQuery({ + queryKey: createPostgateQueryKey(postUri), + queryFn: () => getPostgateRecord({agent, postUri}), + staleTime: STALE.SECONDS.THIRTY, + }), + queryClient.prefetchQuery({ + queryKey: createThreadgateViewQueryKey(rootPostUri), + queryFn: () => getThreadgateView({agent, postUri: rootPostUri}), + staleTime: STALE.SECONDS.THIRTY, + }), + ]) + } catch (e: any) { + logger.error(`Failed to prefetch post interaction settings`, { + safeMessage: e.message, + }) + } + }, [queryClient, agent, postUri, rootPostUri]) +} diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts index ee7c3aa2d8..ef3cebefb8 100644 --- a/src/state/queries/postgate/index.ts +++ b/src/state/queries/postgate/index.ts @@ -9,6 +9,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {networkRetry, retry} from '#/lib/async/retry' import {logger} from '#/logger' import {updatePostShadow} from '#/state/cache/post-shadow' +import {STALE} from '#/state/queries' import {useGetPosts} from '#/state/queries/post' import { createMaybeDetachedQuoteEmbed, @@ -128,6 +129,7 @@ export const createPostgateQueryKey = (postUri: string) => [ export function usePostgateQuery({postUri}: {postUri: string}) { const agent = useAgent() return useQuery({ + staleTime: STALE.SECONDS.THIRTY, queryKey: createPostgateQueryKey(postUri), async queryFn() { return (await getPostgateRecord({agent, postUri})) ?? null @@ -154,7 +156,7 @@ export function useWritePostgateMutation() { }, onSuccess(_, {postUri}) { queryClient.invalidateQueries({ - queryKey: [createPostgateQueryKey(postUri)], + queryKey: createPostgateQueryKey(postUri), }) }, }) diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index e6aea6f243..968e7fc8ba 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -72,21 +72,33 @@ export function useThreadgateViewQuery({ placeholderData: initialData, staleTime: STALE.MINUTES.ONE, async queryFn() { - const {data} = await agent.app.bsky.feed.getPostThread({ - uri: postUri!, - depth: 0, + return getThreadgateView({ + agent, + postUri: postUri!, }) - console.log(data.thread) - - if (AppBskyFeedDefs.isThreadViewPost(data.thread)) { - return data.thread.post.threadgate ?? null - } - - return null }, }) } +export async function getThreadgateView({ + agent, + postUri, +}: { + agent: BskyAgent + postUri: string +}) { + const {data} = await agent.app.bsky.feed.getPostThread({ + uri: postUri!, + depth: 0, + }) + + if (AppBskyFeedDefs.isThreadViewPost(data.thread)) { + return data.thread.post.threadgate ?? null + } + + return null +} + export async function getThreadgateRecord({ agent, postUri, diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index bdff4ddbee..c684635a6a 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -1,5 +1,6 @@ import React, {memo} from 'react' import { + Platform, Pressable, type PressableProps, type StyleProp, @@ -44,7 +45,10 @@ import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {EmbedDialog} from '#/components/dialogs/Embed' -import {PostInteractionSettingsDialog} from '#/components/dialogs/PostInteractionSettingsDialog' +import { + PostInteractionSettingsDialog, + usePrefetchPostInteractionSettings, +} from '#/components/dialogs/PostInteractionSettingsDialog' import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' @@ -144,6 +148,11 @@ let PostDropdownBtn = ({ const {mutateAsync: toggleQuoteDetachment, isPending} = useToggleQuoteDetachmentMutation() + const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({ + postUri: post.uri, + rootPostUri: rootUri, + }) + const href = React.useMemo(() => { const urip = new AtUri(postUri) return makeProfileLink(postAuthor, 'post', urip.rkey) @@ -556,7 +565,17 @@ let PostDropdownBtn = ({ + onPress={postInteractionSettingsDialogControl.open} + {...(isAuthor + ? Platform.select({ + web: { + onHoverIn: prefetchPostInteractionSettings, + }, + native: { + onPressIn: prefetchPostInteractionSettings, + }, + }) + : {})}> {_(msg`Edit interaction settings`)}