Prefetch data needed for interaction settings

This commit is contained in:
Eric Bailey
2024-08-12 16:18:06 -05:00
parent 5d78f69d95
commit 4004d9eeda
7 changed files with 169 additions and 46 deletions
+55 -25
View File
@@ -1,6 +1,8 @@
import React from 'react' import React from 'react'
import { import {
AccessibilityProps, AccessibilityProps,
GestureResponderEvent,
MouseEvent,
Pressable, Pressable,
PressableProps, PressableProps,
StyleProp, StyleProp,
@@ -65,7 +67,15 @@ type NonTextElements =
export type ButtonProps = Pick< export type ButtonProps = Pick<
PressableProps, PressableProps,
'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop' | 'disabled'
| 'onPress'
| 'testID'
| 'onLongPress'
| 'hitSlop'
| 'onHoverIn'
| 'onHoverOut'
| 'onPressIn'
| 'onPressOut'
> & > &
AccessibilityProps & AccessibilityProps &
VariantProps & { VariantProps & {
@@ -115,30 +125,50 @@ export const Button = React.forwardRef<View, ButtonProps>(
focused: false, focused: false,
}) })
const onPressIn = React.useCallback(() => { const onPressInOuter = rest.onPressIn
setState(s => ({ const onPressIn = React.useCallback(
...s, (e: GestureResponderEvent) => {
pressed: true, setState(s => ({
})) ...s,
}, [setState]) pressed: true,
const onPressOut = React.useCallback(() => { }))
setState(s => ({ onPressInOuter?.(e)
...s, },
pressed: false, [setState, onPressInOuter],
})) )
}, [setState]) const onPressOutOuter = rest.onPressOut
const onHoverIn = React.useCallback(() => { const onPressOut = React.useCallback(
setState(s => ({ (e: GestureResponderEvent) => {
...s, setState(s => ({
hovered: true, ...s,
})) pressed: false,
}, [setState]) }))
const onHoverOut = React.useCallback(() => { onPressOutOuter?.(e)
setState(s => ({ },
...s, [setState, onPressOutOuter],
hovered: false, )
})) const onHoverInOuter = rest.onHoverIn
}, [setState]) 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(() => { const onFocus = React.useCallback(() => {
setState(s => ({ setState(s => ({
...s, ...s,
+8 -2
View File
@@ -125,8 +125,14 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
}} }}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
onPressIn={onPressIn} onPressIn={e => {
onPressOut={onPressOut} onPressIn()
rest.onPressIn?.(e)
}}
onPressOut={e => {
onPressOut()
rest.onPressOut?.(e)
}}
style={[ style={[
a.flex_row, a.flex_row,
a.align_center, a.align_center,
+20 -2
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native' import {Keyboard, Platform, StyleProp, View, ViewStyle} from 'react-native'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
@@ -20,12 +20,15 @@ import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {useDialogControl} 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 {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {PostInteractionSettingsDialog} from './dialogs/PostInteractionSettingsDialog'
import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil' import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
interface WhoCanReplyProps { interface WhoCanReplyProps {
@@ -52,6 +55,11 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
return threadgateViewToAllowUISetting(post.threadgate) return threadgateViewToAllowUISetting(post.threadgate)
}, [post.threadgate]) }, [post.threadgate])
const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
postUri: post.uri,
rootPostUri: rootUri,
})
const anyoneCanReply = const anyoneCanReply =
settings.length === 1 && settings[0].type === 'everybody' settings.length === 1 && settings[0].type === 'everybody'
const noOneCanReply = settings.length === 1 && settings[0].type === 'nobody' 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`) isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`)
} }
onPress={onPressOpen} onPress={onPressOpen}
{...(isThreadAuthor
? Platform.select({
web: {
onHoverIn: prefetchPostInteractionSettings,
},
native: {
onPressIn: prefetchPostInteractionSettings,
},
})
: {})}
hitSlop={HITSLOP_10}> hitSlop={HITSLOP_10}>
{({hovered}) => ( {({hovered}) => (
<View style={[a.flex_row, a.align_center, a.gap_xs, style]}> <View style={[a.flex_row, a.align_center, a.gap_xs, style]}>
@@ -8,11 +8,15 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useMyListsQuery} from '#/state/queries/my-lists' import {useMyListsQuery} from '#/state/queries/my-lists'
import { import {
createPostgateQueryKey,
getPostgateRecord,
usePostgateQuery, usePostgateQuery,
useWritePostgateMutation, useWritePostgateMutation,
} from '#/state/queries/postgate' } from '#/state/queries/postgate'
@@ -21,18 +25,19 @@ import {
embeddingRules, embeddingRules,
} from '#/state/queries/postgate/util' } from '#/state/queries/postgate/util'
import { import {
createThreadgateViewQueryKey,
getThreadgateView,
ThreadgateAllowUISetting, ThreadgateAllowUISetting,
threadgateViewToAllowUISetting, threadgateViewToAllowUISetting,
useSetThreadgateAllowMutation, useSetThreadgateAllowMutation,
useThreadgateViewQuery, useThreadgateViewQuery,
} from '#/state/queries/threadgate' } from '#/state/queries/threadgate'
import {useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/toast' import * as Toast from '#/view/com/util/toast'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
@@ -116,14 +121,13 @@ export function PostInteractionSettingsDialogControlledInner(
const {mutateAsync: writePostgateRecord} = useWritePostgateMutation() const {mutateAsync: writePostgateRecord} = useWritePostgateMutation()
const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation() const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation()
const naturalLoading = useDelayedLoading(500) // TODO
const [editedPostgate, setEditedPostgate] = const [editedPostgate, setEditedPostgate] =
React.useState<AppBskyFeedPostgate.Record>() React.useState<AppBskyFeedPostgate.Record>()
const [editedAllowUISettings, setEditedAllowUISettings] = const [editedAllowUISettings, setEditedAllowUISettings] =
React.useState<ThreadgateAllowUISetting[]>() React.useState<ThreadgateAllowUISetting[]>()
const isLoading = isLoadingThreadgate || isLoadingPostgate || naturalLoading const isLoading = isLoadingThreadgate || isLoadingPostgate
const threadgateView = threadgateViewLoaded || props.initialThreadgateView const threadgateView = threadgateViewLoaded || props.initialThreadgateView
const isThreadgateOwnedByViewer = React.useMemo(() => { const isThreadgateOwnedByViewer = React.useMemo(() => {
if (AppBskyFeedThreadgate.isRecord(threadgateView?.record)) { if (AppBskyFeedThreadgate.isRecord(threadgateView?.record)) {
@@ -504,3 +508,35 @@ function Selectable({
</Button> </Button>
) )
} }
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])
}
+3 -1
View File
@@ -9,6 +9,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {networkRetry, retry} from '#/lib/async/retry' import {networkRetry, retry} from '#/lib/async/retry'
import {logger} from '#/logger' import {logger} from '#/logger'
import {updatePostShadow} from '#/state/cache/post-shadow' import {updatePostShadow} from '#/state/cache/post-shadow'
import {STALE} from '#/state/queries'
import {useGetPosts} from '#/state/queries/post' import {useGetPosts} from '#/state/queries/post'
import { import {
createMaybeDetachedQuoteEmbed, createMaybeDetachedQuoteEmbed,
@@ -128,6 +129,7 @@ export const createPostgateQueryKey = (postUri: string) => [
export function usePostgateQuery({postUri}: {postUri: string}) { export function usePostgateQuery({postUri}: {postUri: string}) {
const agent = useAgent() const agent = useAgent()
return useQuery({ return useQuery({
staleTime: STALE.SECONDS.THIRTY,
queryKey: createPostgateQueryKey(postUri), queryKey: createPostgateQueryKey(postUri),
async queryFn() { async queryFn() {
return (await getPostgateRecord({agent, postUri})) ?? null return (await getPostgateRecord({agent, postUri})) ?? null
@@ -154,7 +156,7 @@ export function useWritePostgateMutation() {
}, },
onSuccess(_, {postUri}) { onSuccess(_, {postUri}) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [createPostgateQueryKey(postUri)], queryKey: createPostgateQueryKey(postUri),
}) })
}, },
}) })
+22 -10
View File
@@ -72,21 +72,33 @@ export function useThreadgateViewQuery({
placeholderData: initialData, placeholderData: initialData,
staleTime: STALE.MINUTES.ONE, staleTime: STALE.MINUTES.ONE,
async queryFn() { async queryFn() {
const {data} = await agent.app.bsky.feed.getPostThread({ return getThreadgateView({
uri: postUri!, agent,
depth: 0, 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({ export async function getThreadgateRecord({
agent, agent,
postUri, postUri,
+21 -2
View File
@@ -1,5 +1,6 @@
import React, {memo} from 'react' import React, {memo} from 'react'
import { import {
Platform,
Pressable, Pressable,
type PressableProps, type PressableProps,
type StyleProp, type StyleProp,
@@ -44,7 +45,10 @@ import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {EmbedDialog} from '#/components/dialogs/Embed' 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 {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
@@ -144,6 +148,11 @@ let PostDropdownBtn = ({
const {mutateAsync: toggleQuoteDetachment, isPending} = const {mutateAsync: toggleQuoteDetachment, isPending} =
useToggleQuoteDetachmentMutation() useToggleQuoteDetachmentMutation()
const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
postUri: post.uri,
rootPostUri: rootUri,
})
const href = React.useMemo(() => { const href = React.useMemo(() => {
const urip = new AtUri(postUri) const urip = new AtUri(postUri)
return makeProfileLink(postAuthor, 'post', urip.rkey) return makeProfileLink(postAuthor, 'post', urip.rkey)
@@ -556,7 +565,17 @@ let PostDropdownBtn = ({
<Menu.Item <Menu.Item
testID="postDropdownEditPostInteractions" testID="postDropdownEditPostInteractions"
label={_(msg`Edit interaction settings`)} label={_(msg`Edit interaction settings`)}
onPress={postInteractionSettingsDialogControl.open}> onPress={postInteractionSettingsDialogControl.open}
{...(isAuthor
? Platform.select({
web: {
onHoverIn: prefetchPostInteractionSettings,
},
native: {
onPressIn: prefetchPostInteractionSettings,
},
})
: {})}>
<Menu.ItemText> <Menu.ItemText>
{_(msg`Edit interaction settings`)} {_(msg`Edit interaction settings`)}
</Menu.ItemText> </Menu.ItemText>