From e7af5cd3a8de5b26fb5807362e872fae2348cfdf Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 27 Jan 2025 06:43:39 -0800 Subject: [PATCH 1/3] get initial convo state from cache --- src/components/dms/ConvoMenu.tsx | 15 +++--- .../dms/MessagesListBlockedFooter.tsx | 21 +++++--- src/components/dms/MessagesListHeader.tsx | 17 +++++-- src/screens/Messages/ChatList.tsx | 1 - src/screens/Messages/Conversation.tsx | 49 +++++++++---------- .../Messages/components/ChatListItem.tsx | 12 ++++- src/state/messages/convo/agent.ts | 32 +++++++++--- src/state/messages/convo/index.tsx | 25 ++++++---- src/state/messages/convo/types.ts | 15 +++--- src/state/queries/messages/conversation.ts | 25 ++++++++-- 10 files changed, 134 insertions(+), 78 deletions(-) diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 5b4b68149c..1a389a92c6 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 {data: convo} = useConvoQuery(initialConvo) + const convoId = initialConvo.id + const {data: convo} = useConvoQuery(convoId, 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 && ( - markAsRead({ - convoId: convo?.id, - }) - }> + onPress={() => markAsRead({convoId})}> Mark as read @@ -222,7 +219,7 @@ let ConvoMenu = ({ {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 ec7ba28558..19a7cc9c2a 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 6ac64a7127..baa3e9cd64 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -30,20 +30,27 @@ const PFP_SIZE = isWeb ? 40 : 34 export let MessagesListHeader = ({ profile, moderation, - blockInfo, }: { profile?: AppBskyActorDefs.ProfileViewBasic moderation?: ModerationDecision - blockInfo?: { - listBlocks: ModerationCause[] - userBlock?: ModerationCause - } }): React.ReactNode => { const t = useTheme() const {_} = useLingui() const {gtTablet} = useBreakpoints() const navigation = useNavigation() + 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', {}) diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 178e94dd4b..9647d69024 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 b8b0bfe0d3..bcc6098b3e 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' @@ -76,6 +80,11 @@ function Inner() { did: convoState.recipients?.[0].did, }) + 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 // empty. So, we also check for that possible state as well and render once we can. @@ -110,11 +119,16 @@ function Inner() { return ( - {!readyToShow && } + {!readyToShow && + (moderation ? ( + + ) : ( + + ))} - {moderationOpts && recipient ? ( + {moderation && recipient ? ( > @@ -161,21 +175,6 @@ function InnerReady({ 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 ( <> - + {isConvoActive(convoState) && ( 0} - blockInfo={blockInfo} + moderation={moderation} /> } /> diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 11aada71bc..a64e9e549d 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/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index 53d77046a2..91dd598139 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, + ) { + 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 10ec2a348a..a1750bdf0b 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( + 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 21772262ea..9f1707c715 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 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 9edde4aafc..8c6d6e472a 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' @@ -14,14 +19,17 @@ import { const RQKEY_ROOT = 'convo' export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] -export function useConvoQuery(convo: ChatBskyConvoDefs.ConvoView) { +export function useConvoQuery( + convoId: string, + convo?: ChatBskyConvoDefs.ConvoView, +) { const agent = useAgent() return useQuery({ - queryKey: RQKEY(convo.id), + queryKey: RQKEY(convoId), queryFn: async () => { - const {data} = await agent.api.chat.bsky.convo.getConvo( - {convoId: convo.id}, + const {data} = await agent.chat.bsky.convo.getConvo( + {convoId: convoId}, {headers: DM_SERVICE_HEADERS}, ) return data.convo @@ -31,6 +39,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() From 876fc0cc6d22281191b3fc975bbb044169e4c71a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 3 Feb 2025 19:50:36 +0000 Subject: [PATCH 2/3] undo useConvoQuery changes --- src/components/dms/ConvoMenu.tsx | 2 +- src/state/queries/messages/conversation.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 1a389a92c6..29b6aeab1a 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -74,7 +74,7 @@ let ConvoMenu = ({ const isDeletedAccount = profile.handle === 'missing.invalid' const convoId = initialConvo.id - const {data: convo} = useConvoQuery(convoId, initialConvo) + const {data: convo} = useConvoQuery(initialConvo) const onNavigateToProfile = useCallback(() => { navigation.navigate('Profile', {name: profile.did}) diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts index 8c6d6e472a..2605245243 100644 --- a/src/state/queries/messages/conversation.ts +++ b/src/state/queries/messages/conversation.ts @@ -19,17 +19,14 @@ import { const RQKEY_ROOT = 'convo' export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] -export function useConvoQuery( - convoId: string, - convo?: ChatBskyConvoDefs.ConvoView, -) { +export function useConvoQuery(convo: ChatBskyConvoDefs.ConvoView) { const agent = useAgent() return useQuery({ - queryKey: RQKEY(convoId), + queryKey: RQKEY(convo.id), queryFn: async () => { const {data} = await agent.chat.bsky.convo.getConvo( - {convoId: convoId}, + {convoId: convo.id}, {headers: DM_SERVICE_HEADERS}, ) return data.convo From 0bc7dab41b233aa625d3350bb318e4af13b721c0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 3 Feb 2025 20:11:46 +0000 Subject: [PATCH 3/3] fix shadowing situation with new hook --- src/components/dms/MessagesListHeader.tsx | 9 +++--- src/screens/Messages/Conversation.tsx | 10 +++--- src/state/cache/profile-shadow.ts | 38 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index baa3e9cd64..f8d9b290d8 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' @@ -31,7 +31,7 @@ export let MessagesListHeader = ({ profile, moderation, }: { - profile?: AppBskyActorDefs.ProfileViewBasic + profile?: Shadow moderation?: ModerationDecision }): React.ReactNode => { const t = useTheme() @@ -134,11 +134,11 @@ export let MessagesListHeader = ({ MessagesListHeader = React.memo(MessagesListHeader) function HeaderReady({ - profile: profileUnshadowed, + profile, moderation, blockInfo, }: { - profile: AppBskyActorDefs.ProfileViewBasic + profile: Shadow moderation: ModerationDecision blockInfo: { listBlocks: ModerationCause[] @@ -148,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/Conversation.tsx b/src/screens/Messages/Conversation.tsx index bcc6098b3e..f51822952f 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -14,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' @@ -76,9 +76,10 @@ 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 @@ -159,19 +160,18 @@ function Inner() { function InnerReady({ moderation, - recipient: recipientUnshadowed, + recipient, hasScrolled, setHasScrolled, }: { moderation: ModerationDecision - recipient: AppBskyActorDefs.ProfileViewBasic + recipient: Shadow hasScrolled: boolean setHasScrolled: React.Dispatch> }) { const {_} = useLingui() const convoState = useConvo() const navigation = useNavigation() - const recipient = useProfileShadow(recipientUnshadowed) const verifyEmailControl = useDialogControl() const {needsEmailVerification} = useEmail() diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index afd3f19355..4d823ec8e4 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 | 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,