diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-02-03 14:37:24 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-02-03 22:37:24 +0000 |
commit | 32b28d666229ac24cf7b1ac328d1566fb089e1a1 (patch) | |
tree | 2e721117c9a859ca1cae52e1c15642d5e6db4d5b | |
parent | fa8607b861e0719d76778aa14af0745313640e33 (diff) | |
download | voidsky-32b28d666229ac24cf7b1ac328d1566fb089e1a1.tar.zst |
Fix convo header loading state (#7603)
* get initial convo state from cache * undo useConvoQuery changes * fix shadowing situation with new hook
-rw-r--r-- | src/components/dms/ConvoMenu.tsx | 13 | ||||
-rw-r--r-- | src/components/dms/MessagesListBlockedFooter.tsx | 21 | ||||
-rw-r--r-- | src/components/dms/MessagesListHeader.tsx | 26 | ||||
-rw-r--r-- | src/screens/Messages/ChatList.tsx | 1 | ||||
-rw-r--r-- | src/screens/Messages/Conversation.tsx | 59 | ||||
-rw-r--r-- | src/screens/Messages/components/ChatListItem.tsx | 12 | ||||
-rw-r--r-- | src/state/cache/profile-shadow.ts | 38 | ||||
-rw-r--r-- | src/state/messages/convo/agent.ts | 32 | ||||
-rw-r--r-- | src/state/messages/convo/index.tsx | 25 | ||||
-rw-r--r-- | src/state/messages/convo/types.ts | 15 | ||||
-rw-r--r-- | src/state/queries/messages/conversation.ts | 16 |
11 files changed, 174 insertions, 84 deletions
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 5b4b68149..29b6aeab1 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -73,13 +73,14 @@ let ConvoMenu = ({ const isBlocking = userBlock || !!listBlocks.length const isDeletedAccount = profile.handle === 'missing.invalid' + const convoId = initialConvo.id const {data: convo} = useConvoQuery(initialConvo) const onNavigateToProfile = useCallback(() => { navigation.navigate('Profile', {name: profile.did}) }, [navigation, profile.did]) - const {mutate: muteConvo} = useMuteConvo(convo?.id, { + const {mutate: muteConvo} = useMuteConvo(convoId, { onSuccess: data => { if (data.convo.muted) { Toast.show(_(msg`Chat muted`)) @@ -152,11 +153,7 @@ let ConvoMenu = ({ {showMarkAsRead && ( <Menu.Item label={_(msg`Mark as read`)} - onPress={() => - markAsRead({ - convoId: convo?.id, - }) - }> + onPress={() => markAsRead({convoId})}> <Menu.ItemText> <Trans>Mark as read</Trans> </Menu.ItemText> @@ -222,7 +219,7 @@ let ConvoMenu = ({ <LeaveConvoPrompt control={leaveConvoControl} - convoId={convo.id} + convoId={convoId} currentScreen={currentScreen} /> {latestReportableMessage ? ( @@ -230,7 +227,7 @@ let ConvoMenu = ({ currentScreen={currentScreen} params={{ type: 'convoMessage', - convoId: convo.id, + convoId: convoId, message: latestReportableMessage, }} control={reportControl} diff --git a/src/components/dms/MessagesListBlockedFooter.tsx b/src/components/dms/MessagesListBlockedFooter.tsx index ec7ba2855..19a7cc9c2 100644 --- a/src/components/dms/MessagesListBlockedFooter.tsx +++ b/src/components/dms/MessagesListBlockedFooter.tsx @@ -1,6 +1,6 @@ import React from 'react' import {View} from 'react-native' -import {AppBskyActorDefs, ModerationCause} from '@atproto/api' +import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -19,15 +19,12 @@ export function MessagesListBlockedFooter({ recipient: initialRecipient, convoId, hasMessages, - blockInfo, + moderation, }: { recipient: AppBskyActorDefs.ProfileViewBasic convoId: string hasMessages: boolean - blockInfo: { - listBlocks: ModerationCause[] - userBlock: ModerationCause | undefined - } + moderation: ModerationDecision }) { const t = useTheme() const {gtMobile} = useBreakpoints() @@ -39,7 +36,17 @@ export function MessagesListBlockedFooter({ const reportControl = useDialogControl() const blockedByListControl = useDialogControl() - const {listBlocks, userBlock} = blockInfo + const {listBlocks, userBlock} = React.useMemo(() => { + const modui = moderation.ui('profileView') + const blocks = modui.alerts.filter(alert => alert.type === 'blocking') + const listBlocks = blocks.filter(alert => alert.source.type === 'list') + const userBlock = blocks.find(alert => alert.source.type === 'user') + return { + listBlocks, + userBlock, + } + }, [moderation]) + const isBlocking = !!userBlock || !!listBlocks.length const onUnblockPress = React.useCallback(() => { diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 6ac64a712..f8d9b290d 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -15,7 +15,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {isWeb} from '#/platform/detection' -import {useProfileShadow} from '#/state/cache/profile-shadow' +import {Shadow} from '#/state/cache/profile-shadow' import {isConvoActive, useConvo} from '#/state/messages/convo' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' @@ -30,20 +30,27 @@ const PFP_SIZE = isWeb ? 40 : 34 export let MessagesListHeader = ({ profile, moderation, - blockInfo, }: { - profile?: AppBskyActorDefs.ProfileViewBasic + profile?: Shadow<AppBskyActorDefs.ProfileViewBasic> moderation?: ModerationDecision - blockInfo?: { - listBlocks: ModerationCause[] - userBlock?: ModerationCause - } }): React.ReactNode => { const t = useTheme() const {_} = useLingui() const {gtTablet} = useBreakpoints() const navigation = useNavigation<NavigationProp>() + const blockInfo = React.useMemo(() => { + if (!moderation) return + const modui = moderation.ui('profileView') + const blocks = modui.alerts.filter(alert => alert.type === 'blocking') + const listBlocks = blocks.filter(alert => alert.source.type === 'list') + const userBlock = blocks.find(alert => alert.source.type === 'user') + return { + listBlocks, + userBlock, + } + }, [moderation]) + const onPressBack = useCallback(() => { if (isWeb) { navigation.replace('Messages', {}) @@ -127,11 +134,11 @@ export let MessagesListHeader = ({ MessagesListHeader = React.memo(MessagesListHeader) function HeaderReady({ - profile: profileUnshadowed, + profile, moderation, blockInfo, }: { - profile: AppBskyActorDefs.ProfileViewBasic + profile: Shadow<AppBskyActorDefs.ProfileViewBasic> moderation: ModerationDecision blockInfo: { listBlocks: ModerationCause[] @@ -141,7 +148,6 @@ function HeaderReady({ const {_} = useLingui() const t = useTheme() const convoState = useConvo() - const profile = useProfileShadow(profileUnshadowed) const isDeletedAccount = profile?.handle === 'missing.invalid' const displayName = isDeletedAccount diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 178e94dd4..9647d6902 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -236,7 +236,6 @@ export function MessagesScreen({navigation, route}: Props) { onEndReachedThreshold={isNative ? 1.5 : 0} initialNumToRender={initialNumToRender} windowSize={11} - // @ts-ignore our .web version only -sfn desktopFixedHeight sideBorders={false} /> diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index b8b0bfe0d..f51822952 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -1,6 +1,10 @@ import React, {useCallback} from 'react' import {View} from 'react-native' -import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' +import { + AppBskyActorDefs, + moderateProfile, + ModerationDecision, +} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect, useNavigation} from '@react-navigation/native' @@ -10,7 +14,7 @@ import {useEmail} from '#/lib/hooks/useEmail' import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {isWeb} from '#/platform/detection' -import {useProfileShadow} from '#/state/cache/profile-shadow' +import {Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' import {ConvoStatus} from '#/state/messages/convo/types' import {useCurrentConvoId} from '#/state/messages/current-convo-id' @@ -72,9 +76,15 @@ function Inner() { const {_} = useLingui() const moderationOpts = useModerationOpts() - const {data: recipient} = useProfileQuery({ + const {data: recipientUnshadowed} = useProfileQuery({ did: convoState.recipients?.[0].did, }) + const recipient = useMaybeProfileShadow(recipientUnshadowed) + + const moderation = React.useMemo(() => { + if (!recipient || !moderationOpts) return null + return moderateProfile(recipient, moderationOpts) + }, [recipient, moderationOpts]) // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be @@ -110,11 +120,16 @@ function Inner() { return ( <Layout.Center style={[a.flex_1]}> - {!readyToShow && <MessagesListHeader />} + {!readyToShow && + (moderation ? ( + <MessagesListHeader moderation={moderation} profile={recipient} /> + ) : ( + <MessagesListHeader /> + ))} <View style={[a.flex_1]}> - {moderationOpts && recipient ? ( + {moderation && recipient ? ( <InnerReady - moderationOpts={moderationOpts} + moderation={moderation} recipient={recipient} hasScrolled={hasScrolled} setHasScrolled={setHasScrolled} @@ -144,38 +159,22 @@ function Inner() { } function InnerReady({ - moderationOpts, - recipient: recipientUnshadowed, + moderation, + recipient, hasScrolled, setHasScrolled, }: { - moderationOpts: ModerationOpts - recipient: AppBskyActorDefs.ProfileViewBasic + moderation: ModerationDecision + recipient: Shadow<AppBskyActorDefs.ProfileViewBasic> hasScrolled: boolean setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> }) { const {_} = useLingui() const convoState = useConvo() const navigation = useNavigation<NavigationProp>() - const recipient = useProfileShadow(recipientUnshadowed) const verifyEmailControl = useDialogControl() const {needsEmailVerification} = useEmail() - const moderation = React.useMemo(() => { - return moderateProfile(recipient, moderationOpts) - }, [recipient, moderationOpts]) - - const blockInfo = React.useMemo(() => { - const modui = moderation.ui('profileView') - const blocks = modui.alerts.filter(alert => alert.type === 'blocking') - const listBlocks = blocks.filter(alert => alert.source.type === 'list') - const userBlock = blocks.find(alert => alert.source.type === 'user') - return { - listBlocks, - userBlock, - } - }, [moderation]) - React.useEffect(() => { if (needsEmailVerification) { verifyEmailControl.open() @@ -184,11 +183,7 @@ function InnerReady({ return ( <> - <MessagesListHeader - profile={recipient} - moderation={moderation} - blockInfo={blockInfo} - /> + <MessagesListHeader profile={recipient} moderation={moderation} /> {isConvoActive(convoState) && ( <MessagesList hasScrolled={hasScrolled} @@ -199,7 +194,7 @@ function InnerReady({ recipient={recipient} convoId={convoState.convo.id} hasMessages={convoState.items.length > 0} - blockInfo={blockInfo} + moderation={moderation} /> } /> diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 11aada71b..a64e9e549 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -9,6 +9,7 @@ import { } from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' import {GestureActionView} from '#/lib/custom-animations/GestureActionView' import {useHaptics} from '#/lib/haptics' @@ -23,7 +24,11 @@ import { import {isNative} from '#/platform/detection' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useMarkAsReadMutation} from '#/state/queries/messages/conversation' +import { + precacheConvoQuery, + useMarkAsReadMutation, +} from '#/state/queries/messages/conversation' +import {precacheProfile} from '#/state/queries/profile' import {useSession} from '#/state/session' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' @@ -89,6 +94,7 @@ function ChatListItemReady({ [profile, moderationOpts], ) const playHaptic = useHaptics() + const queryClient = useQueryClient() const isUnread = convo.unreadCount > 0 const blockInfo = useMemo(() => { @@ -198,6 +204,8 @@ function ChatListItemReady({ const onPress = useCallback( (e: GestureResponderEvent) => { + precacheProfile(queryClient, profile) + precacheConvoQuery(queryClient, convo) decrementBadgeCount(convo.unreadCount) if (isDeletedAccount) { e.preventDefault() @@ -207,7 +215,7 @@ function ChatListItemReady({ logEvent('chat:open', {logContext: 'ChatsList'}) } }, - [convo.unreadCount, isDeletedAccount, menuControl], + [isDeletedAccount, menuControl, queryClient, profile, convo], ) const onLongPress = useCallback(() => { diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index afd3f1935..4d823ec8e 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -63,6 +63,44 @@ export function useProfileShadow< }, [profile, shadow]) } +/** + * Same as useProfileShadow, but allows for the profile to be undefined. + * This is useful for when the profile is not guaranteed to be loaded yet. + */ +export function useMaybeProfileShadow< + TProfileView extends AppBskyActorDefs.ProfileView, +>(profile?: TProfileView): Shadow<TProfileView> | undefined { + const [shadow, setShadow] = useState(() => + profile ? shadows.get(profile) : undefined, + ) + const [prevPost, setPrevPost] = useState(profile) + if (profile !== prevPost) { + setPrevPost(profile) + setShadow(profile ? shadows.get(profile) : undefined) + } + + useEffect(() => { + if (!profile) return + function onUpdate() { + if (!profile) return + setShadow(shadows.get(profile)) + } + emitter.addListener(profile.did, onUpdate) + return () => { + emitter.removeListener(profile.did, onUpdate) + } + }, [profile]) + + return useMemo(() => { + if (!profile) return undefined + if (shadow) { + return mergeShadow(profile, shadow) + } else { + return castAsShadow(profile) + } + }, [profile, shadow]) +} + export function updateProfileShadow( queryClient: QueryClient, did: string, diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index 53d77046a..91dd59813 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -81,7 +81,7 @@ export class Convo { convoId: string convo: ChatBskyConvoDefs.ConvoView | undefined sender: AppBskyActorDefs.ProfileViewBasic | undefined - recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined + recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined snapshot: ConvoState | undefined constructor(params: ConvoParams) { @@ -91,6 +91,10 @@ export class Convo { this.events = params.events this.senderUserDid = params.agent.session?.did! + if (params.placeholderData) { + this.setupPlaceholderData(params.placeholderData) + } + this.subscribe = this.subscribe.bind(this) this.getSnapshot = this.getSnapshot.bind(this) this.sendMessage = this.sendMessage.bind(this) @@ -131,10 +135,10 @@ export class Convo { return { status: ConvoStatus.Initializing, items: [], - convo: undefined, + convo: this.convo, error: undefined, - sender: undefined, - recipients: undefined, + sender: this.sender, + recipients: this.recipients, isFetchingHistory: this.isFetchingHistory, deleteMessage: undefined, sendMessage: undefined, @@ -176,10 +180,10 @@ export class Convo { return { status: ConvoStatus.Uninitialized, items: [], - convo: undefined, + convo: this.convo, error: undefined, - sender: undefined, - recipients: undefined, + sender: this.sender, + recipients: this.recipients, isFetchingHistory: false, deleteMessage: undefined, sendMessage: undefined, @@ -424,6 +428,20 @@ export class Convo { } } + /** + * Initialises the convo with placeholder data, if provided. We still refetch it before rendering the convo, + * but this allows us to render the convo header immediately. + */ + private setupPlaceholderData( + data: NonNullable<ConvoParams['placeholderData']>, + ) { + this.convo = data.convo + this.sender = data.convo.members.find(m => m.did === this.senderUserDid) + this.recipients = data.convo.members.filter( + m => m.did !== this.senderUserDid, + ) + } + private async setup() { try { const {convo, sender, recipients} = await this.fetchConvo() diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index 10ec2a348..a1750bdf0 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -1,4 +1,5 @@ import React, {useContext, useState, useSyncExternalStore} from 'react' +import {ChatBskyConvoDefs} from '@atproto/api' import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -14,7 +15,10 @@ import { } from '#/state/messages/convo/types' import {isConvoActive} from '#/state/messages/convo/util' import {useMessagesEventBus} from '#/state/messages/events' -import {useMarkAsReadMutation} from '#/state/queries/messages/conversation' +import { + RQKEY as getConvoKey, + useMarkAsReadMutation, +} from '#/state/queries/messages/conversation' import {RQKEY as ListConvosQueryKey} from '#/state/queries/messages/list-conversations' import {RQKEY as createProfileQueryKey} from '#/state/queries/profile' import {useAgent} from '#/state/session' @@ -60,14 +64,17 @@ export function ConvoProvider({ const queryClient = useQueryClient() const agent = useAgent() const events = useMessagesEventBus() - const [convo] = useState( - () => - new Convo({ - convoId, - agent, - events, - }), - ) + const [convo] = useState(() => { + const placeholder = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>( + getConvoKey(convoId), + ) + return new Convo({ + convoId, + agent, + events, + placeholderData: placeholder ? {convo: placeholder} : undefined, + }) + }) const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot) const {mutate: markAsRead} = useMarkAsReadMutation() diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts index 21772262e..9f1707c71 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -11,6 +11,9 @@ export type ConvoParams = { convoId: string agent: BskyAgent events: MessagesEventBus + placeholderData?: { + convo: ChatBskyConvoDefs.ConvoView + } } export enum ConvoStatus { @@ -142,10 +145,10 @@ type FetchMessageHistory = () => Promise<void> export type ConvoStateUninitialized = { status: ConvoStatus.Uninitialized items: [] - convo: undefined + convo: ChatBskyConvoDefs.ConvoView | undefined error: undefined - sender: undefined - recipients: undefined + sender: AppBskyActorDefs.ProfileViewBasic | undefined + recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined isFetchingHistory: false deleteMessage: undefined sendMessage: undefined @@ -154,10 +157,10 @@ export type ConvoStateUninitialized = { export type ConvoStateInitializing = { status: ConvoStatus.Initializing items: [] - convo: undefined + convo: ChatBskyConvoDefs.ConvoView | undefined error: undefined - sender: undefined - recipients: undefined + sender: AppBskyActorDefs.ProfileViewBasic | undefined + recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined isFetchingHistory: boolean deleteMessage: undefined sendMessage: undefined diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts index 9edde4aaf..260524524 100644 --- a/src/state/queries/messages/conversation.ts +++ b/src/state/queries/messages/conversation.ts @@ -1,5 +1,10 @@ import {ChatBskyConvoDefs} from '@atproto/api' -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' +import { + QueryClient, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' import {STALE} from '#/state/queries' import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' @@ -20,7 +25,7 @@ export function useConvoQuery(convo: ChatBskyConvoDefs.ConvoView) { return useQuery({ queryKey: RQKEY(convo.id), queryFn: async () => { - const {data} = await agent.api.chat.bsky.convo.getConvo( + const {data} = await agent.chat.bsky.convo.getConvo( {convoId: convo.id}, {headers: DM_SERVICE_HEADERS}, ) @@ -31,6 +36,13 @@ export function useConvoQuery(convo: ChatBskyConvoDefs.ConvoView) { }) } +export function precacheConvoQuery( + queryClient: QueryClient, + convo: ChatBskyConvoDefs.ConvoView, +) { + queryClient.setQueryData(RQKEY(convo.id), convo) +} + export function useMarkAsReadMutation() { const optimisticUpdate = useOnMarkAsRead() const queryClient = useQueryClient() |