diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index 5b4b68149c..29b6aeab1a 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 && (
- 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..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'
@@ -30,20 +30,27 @@ const PFP_SIZE = isWeb ? 40 : 34
export let MessagesListHeader = ({
profile,
moderation,
- blockInfo,
}: {
- profile?: AppBskyActorDefs.ProfileViewBasic
+ profile?: Shadow
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', {})
@@ -127,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[]
@@ -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 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..f51822952f 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 (
- {!readyToShow && }
+ {!readyToShow &&
+ (moderation ? (
+
+ ) : (
+
+ ))}
- {moderationOpts && recipient ? (
+ {moderation && recipient ? (
hasScrolled: boolean
setHasScrolled: React.Dispatch>
}) {
const {_} = useLingui()
const convoState = useConvo()
const navigation = useNavigation()
- 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 (
<>
-
+
{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/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,
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..2605245243 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()