optimistic fetch convo data

This commit is contained in:
Samuel Newman
2024-12-12 01:25:20 +00:00
parent e49dad2889
commit d9ffaddec1
6 changed files with 81 additions and 28 deletions
-1
View File
@@ -226,7 +226,6 @@ export function MessagesScreen({navigation, route}: Props) {
onEndReachedThreshold={isNative ? 1.5 : 0} onEndReachedThreshold={isNative ? 1.5 : 0}
initialNumToRender={initialNumToRender} initialNumToRender={initialNumToRender}
windowSize={11} windowSize={11}
// @ts-ignore our .web version only -sfn
desktopFixedHeight desktopFixedHeight
sideBorders={false} sideBorders={false}
/> />
+40 -21
View File
@@ -1,6 +1,11 @@
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {
AppBskyActorDefs,
moderateProfile,
ModerationCause,
ModerationDecision,
} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useFocusEffect, useNavigation} from '@react-navigation/native'
@@ -86,6 +91,23 @@ function Inner() {
!convoState.isFetchingHistory && !convoState.isFetchingHistory &&
convoState.items.length === 0) convoState.items.length === 0)
const moderation = React.useMemo(() => {
if (!recipient || !moderationOpts) return
return moderateProfile(recipient, moderationOpts)
}, [recipient, moderationOpts])
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])
// Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this // Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this
// state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added. // state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added.
React.useEffect(() => { React.useEffect(() => {
@@ -110,11 +132,18 @@ function Inner() {
return ( return (
<Layout.Center style={[a.flex_1]}> <Layout.Center style={[a.flex_1]}>
{!readyToShow && <MessagesListHeader />} {!readyToShow && (
<MessagesListHeader
profile={recipient}
moderation={moderation}
blockInfo={blockInfo}
/>
)}
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
{moderationOpts && recipient ? ( {moderation && blockInfo && recipient ? (
<InnerReady <InnerReady
moderationOpts={moderationOpts} moderation={moderation}
blockInfo={blockInfo}
recipient={recipient} recipient={recipient}
hasScrolled={hasScrolled} hasScrolled={hasScrolled}
setHasScrolled={setHasScrolled} setHasScrolled={setHasScrolled}
@@ -144,12 +173,17 @@ function Inner() {
} }
function InnerReady({ function InnerReady({
moderationOpts, moderation,
blockInfo,
recipient: recipientUnshadowed, recipient: recipientUnshadowed,
hasScrolled, hasScrolled,
setHasScrolled, setHasScrolled,
}: { }: {
moderationOpts: ModerationOpts moderation: ModerationDecision
blockInfo: {
listBlocks: ModerationCause[]
userBlock: ModerationCause | undefined
}
recipient: AppBskyActorDefs.ProfileViewBasic recipient: AppBskyActorDefs.ProfileViewBasic
hasScrolled: boolean hasScrolled: boolean
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
@@ -161,21 +195,6 @@ function InnerReady({
const verifyEmailControl = useDialogControl() const verifyEmailControl = useDialogControl()
const {needsEmailVerification} = useEmail() 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(() => { React.useEffect(() => {
if (needsEmailVerification) { if (needsEmailVerification) {
verifyEmailControl.open() verifyEmailControl.open()
@@ -9,6 +9,7 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {GestureActionView} from '#/lib/custom-animations/GestureActionView' import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
@@ -24,6 +25,7 @@ import {isNative} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation' import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
import {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
@@ -77,6 +79,7 @@ function ChatListItemReady({
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}) { }) {
const t = useTheme() const t = useTheme()
const queryClient = useQueryClient()
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const menuControl = useMenuControl() const menuControl = useMenuControl()
@@ -189,6 +192,7 @@ function ChatListItemReady({
const onPress = useCallback( const onPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
decrementBadgeCount(convo.unreadCount) decrementBadgeCount(convo.unreadCount)
precacheProfile(queryClient, profile)
if (isDeletedAccount) { if (isDeletedAccount) {
e.preventDefault() e.preventDefault()
menuControl.open() menuControl.open()
@@ -197,7 +201,7 @@ function ChatListItemReady({
logEvent('chat:open', {logContext: 'ChatsList'}) logEvent('chat:open', {logContext: 'ChatsList'})
} }
}, },
[convo.unreadCount, isDeletedAccount, menuControl], [convo.unreadCount, isDeletedAccount, menuControl, queryClient, profile],
) )
const onLongPress = useCallback(() => { const onLongPress = useCallback(() => {
+33 -5
View File
@@ -3,9 +3,11 @@ import {
BskyAgent, BskyAgent,
ChatBskyConvoDefs, ChatBskyConvoDefs,
ChatBskyConvoGetLog, ChatBskyConvoGetLog,
ChatBskyConvoListConvos,
ChatBskyConvoSendMessage, ChatBskyConvoSendMessage,
} from '@atproto/api' } from '@atproto/api'
import {XRPCError} from '@atproto/xrpc' import {XRPCError} from '@atproto/xrpc'
import {InfiniteData, QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3' import EventEmitter from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
@@ -33,6 +35,7 @@ import {
import {MessagesEventBus} from '#/state/messages/events/agent' import {MessagesEventBus} from '#/state/messages/events/agent'
import {MessagesEventBusError} from '#/state/messages/events/types' import {MessagesEventBusError} from '#/state/messages/events/types'
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
import {RQKEY as RQKEY_LIST_CONVOS} from '#/state/queries/messages/list-conversations'
export function isConvoItemMessage( export function isConvoItemMessage(
item: ConvoItem, item: ConvoItem,
@@ -83,6 +86,7 @@ export class Convo {
sender: AppBskyActorDefs.ProfileViewBasic | undefined sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined
snapshot: ConvoState | undefined snapshot: ConvoState | undefined
queryClient: QueryClient | undefined
constructor(params: ConvoParams) { constructor(params: ConvoParams) {
this.id = nanoid(3) this.id = nanoid(3)
@@ -90,6 +94,7 @@ export class Convo {
this.agent = params.agent this.agent = params.agent
this.events = params.events this.events = params.events
this.senderUserDid = params.agent.session?.did! this.senderUserDid = params.agent.session?.did!
this.queryClient = params.queryClient
this.subscribe = this.subscribe.bind(this) this.subscribe = this.subscribe.bind(this)
this.getSnapshot = this.getSnapshot.bind(this) this.getSnapshot = this.getSnapshot.bind(this)
@@ -99,6 +104,8 @@ export class Convo {
this.ingestFirehose = this.ingestFirehose.bind(this) this.ingestFirehose = this.ingestFirehose.bind(this)
this.onFirehoseConnect = this.onFirehoseConnect.bind(this) this.onFirehoseConnect = this.onFirehoseConnect.bind(this)
this.onFirehoseError = this.onFirehoseError.bind(this) this.onFirehoseError = this.onFirehoseError.bind(this)
this.optimisticFetchConvoFromCache =
this.optimisticFetchConvoFromCache.bind(this)
} }
private commit() { private commit() {
@@ -426,6 +433,8 @@ export class Convo {
private async setup() { private async setup() {
try { try {
this.optimisticFetchConvoFromCache()
const {convo, sender, recipients} = await this.fetchConvo() const {convo, sender, recipients} = await this.fetchConvo()
this.convo = convo this.convo = convo
@@ -508,6 +517,25 @@ export class Convo {
} }
} }
private optimisticFetchConvoFromCache() {
if (!this.queryClient) return
if (this.convo) return // bail if already loaded
const convoList =
this.queryClient.getQueryData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema, string | undefined>
>(RQKEY_LIST_CONVOS)
const convo = convoList?.pages
?.flatMap(c => c.convos)
?.find(c => c.id === this.convoId)
if (convo) {
this.convo = convo
this.sender = convo.members.find(m => m.did === this.senderUserDid)
this.recipients = convo.members.filter(m => m.did !== this.senderUserDid)
}
}
private pendingFetchConvo: private pendingFetchConvo:
| Promise<{ | Promise<{
convo: ChatBskyConvoDefs.ConvoView convo: ChatBskyConvoDefs.ConvoView
@@ -525,7 +553,7 @@ export class Convo {
}>(async (resolve, reject) => { }>(async (resolve, reject) => {
try { try {
const response = await networkRetry(2, () => { const response = await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.getConvo( return this.agent.chat.bsky.convo.getConvo(
{ {
convoId: this.convoId, convoId: this.convoId,
}, },
@@ -592,7 +620,7 @@ export class Convo {
const nextCursor = this.oldestRev // for TS const nextCursor = this.oldestRev // for TS
const response = await networkRetry(2, () => { const response = await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.getMessages( return this.agent.chat.bsky.convo.getMessages(
{ {
cursor: nextCursor, cursor: nextCursor,
convoId: this.convoId, convoId: this.convoId,
@@ -793,7 +821,7 @@ export class Convo {
const {id, message} = pendingMessage const {id, message} = pendingMessage
const response = await this.agent.api.chat.bsky.convo.sendMessage( const response = await this.agent.chat.bsky.convo.sendMessage(
{ {
convoId: this.convoId, convoId: this.convoId,
message, message,
@@ -888,7 +916,7 @@ export class Convo {
) )
try { try {
const {data} = await this.agent.api.chat.bsky.convo.sendMessageBatch( const {data} = await this.agent.chat.bsky.convo.sendMessageBatch(
{ {
items: messageArray.map(({message}) => ({ items: messageArray.map(({message}) => ({
convoId: this.convoId, convoId: this.convoId,
@@ -935,7 +963,7 @@ export class Convo {
try { try {
await networkRetry(2, () => { await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.deleteMessageForSelf( return this.agent.chat.bsky.convo.deleteMessageForSelf(
{ {
convoId: this.convoId, convoId: this.convoId,
messageId, messageId,
+1
View File
@@ -66,6 +66,7 @@ export function ConvoProvider({
convoId, convoId,
agent, agent,
events, events,
queryClient,
}), }),
) )
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot) const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
+2
View File
@@ -4,6 +4,7 @@ import {
ChatBskyConvoDefs, ChatBskyConvoDefs,
ChatBskyConvoSendMessage, ChatBskyConvoSendMessage,
} from '@atproto/api' } from '@atproto/api'
import {QueryClient} from '@tanstack/react-query'
import {MessagesEventBus} from '#/state/messages/events/agent' import {MessagesEventBus} from '#/state/messages/events/agent'
@@ -11,6 +12,7 @@ export type ConvoParams = {
convoId: string convoId: string
agent: BskyAgent agent: BskyAgent
events: MessagesEventBus events: MessagesEventBus
queryClient?: QueryClient
} }
export enum ConvoStatus { export enum ConvoStatus {