move convo/member data fetching out of Convo class into react query

Convo class no longer owns fetchConvo/refreshConvo. Data is fetched via
useConvoQuery + useListConvoMembersQuery in the provider and synced in
via setConvoData. Convo stores ConvoWithDetails instead of raw ConvoView.
relatedProfiles map grows as message history is fetched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-04-22 20:00:43 +03:00
parent 26d1b9c96a
commit 2a8c0a9dce
4 changed files with 168 additions and 283 deletions
+7 -7
View File
@@ -87,7 +87,10 @@ export type ConvoWithDetails = {view: ChatBskyConvoDefs.ConvoView} & (
export function parseConvoView( export function parseConvoView(
convoView: ChatBskyConvoDefs.ConvoView, convoView: ChatBskyConvoDefs.ConvoView,
ownDid: string | undefined, ownDid: string | undefined,
allMembers?: ChatBskyActorDefs.ProfileViewBasic[],
): ConvoWithDetails | null { ): ConvoWithDetails | null {
const memberList = allMembers ?? convoView.members
if ( if (
bsky.dangerousIsType<ChatBskyConvoDefs.GroupConvo>( bsky.dangerousIsType<ChatBskyConvoDefs.GroupConvo>(
convoView.kind, convoView.kind,
@@ -96,7 +99,7 @@ export function parseConvoView(
) { ) {
let owner: GroupConvoMember | undefined = undefined let owner: GroupConvoMember | undefined = undefined
for (const member of convoView.members) { for (const member of memberList) {
if ( if (
bsky.dangerousIsType<ChatBskyActorDefs.GroupConvoMember>( bsky.dangerousIsType<ChatBskyActorDefs.GroupConvoMember>(
member.kind, member.kind,
@@ -104,9 +107,6 @@ export function parseConvoView(
) )
) { ) {
if (member.kind.role === 'owner') { if (member.kind.role === 'owner') {
// have to do a type assertion here
// this works: {...member, kind: member.kind}
// however that's creating a new object for no good reason
owner = member as GroupConvoMember owner = member as GroupConvoMember
} }
} else { } else {
@@ -127,7 +127,7 @@ export function parseConvoView(
kind: 'group', kind: 'group',
details: convoView.kind, details: convoView.kind,
primaryMember: owner, primaryMember: owner,
members: convoView.members as Array<GroupConvoMember>, members: memberList as Array<GroupConvoMember>,
} }
} else if ( } else if (
bsky.dangerousIsType<ChatBskyConvoDefs.DirectConvo>( bsky.dangerousIsType<ChatBskyConvoDefs.DirectConvo>(
@@ -135,7 +135,7 @@ export function parseConvoView(
ChatBskyConvoDefs.isDirectConvo, ChatBskyConvoDefs.isDirectConvo,
) )
) { ) {
const otherUser = convoView.members.find(m => m.did !== ownDid) const otherUser = memberList.find(m => m.did !== ownDid)
if (!otherUser) { if (!otherUser) {
logger.warn('No other user found in direct convo') logger.warn('No other user found in direct convo')
@@ -147,7 +147,7 @@ export function parseConvoView(
kind: 'direct', kind: 'direct',
details: convoView.kind, details: convoView.kind,
primaryMember: otherUser as DirectConvoMember, primaryMember: otherUser as DirectConvoMember,
members: convoView.members as Array<DirectConvoMember>, members: memberList as Array<DirectConvoMember>,
} }
} else { } else {
logger.warn('Unknown convo kind: ' + JSON.stringify(convoView.kind)) logger.warn('Unknown convo kind: ' + JSON.stringify(convoView.kind))
+114 -241
View File
@@ -1,6 +1,6 @@
import { import {
type AtpAgent, type AtpAgent,
ChatBskyActorDefs, type ChatBskyActorDefs,
ChatBskyConvoDefs, ChatBskyConvoDefs,
type ChatBskyConvoGetLog, type ChatBskyConvoGetLog,
type ChatBskyConvoSendMessage, type ChatBskyConvoSendMessage,
@@ -19,6 +19,7 @@ import {Logger} from '#/logger'
import { import {
ACTIVE_POLL_INTERVAL, ACTIVE_POLL_INTERVAL,
BACKGROUND_POLL_INTERVAL, BACKGROUND_POLL_INTERVAL,
GET_MESSAGE_HISTORY_LIMIT,
INACTIVE_TIMEOUT, INACTIVE_TIMEOUT,
NETWORK_FAILURE_STATUSES, NETWORK_FAILURE_STATUSES,
} from '#/state/messages/convo/const' } from '#/state/messages/convo/const'
@@ -26,7 +27,6 @@ import {
type ConvoDispatch, type ConvoDispatch,
ConvoDispatchEvent, ConvoDispatchEvent,
type ConvoError, type ConvoError,
ConvoErrorCode,
type ConvoEvent, type ConvoEvent,
type ConvoItem, type ConvoItem,
ConvoItemError, ConvoItemError,
@@ -36,8 +36,8 @@ import {
} from '#/state/messages/convo/types' } from '#/state/messages/convo/types'
import {type MessagesEventBus} from '#/state/messages/events/agent' import {type MessagesEventBus} from '#/state/messages/events/agent'
import {type MessagesEventBusError} from '#/state/messages/events/types' import {type MessagesEventBusError} from '#/state/messages/events/types'
import {IS_NATIVE} from '#/env' import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
import * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
const logger = Logger.create(Logger.Context.ConversationAgent) const logger = Logger.create(Logger.Context.ConversationAgent)
@@ -102,10 +102,7 @@ export class Convo {
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']} {id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
> = new Map() > = new Map()
private deletedMessages: Set<string> = new Set() private deletedMessages: Set<string> = new Set()
private systemMessageProfiles: Map< private relatedProfiles: Map<string, bsky.profile.AnyProfileView> = new Map()
string,
ChatBskyActorDefs.ProfileViewBasic
> = new Map()
private isProcessingPendingMessages = false private isProcessingPendingMessages = false
@@ -114,9 +111,7 @@ export class Convo {
private emitter = new EventEmitter<{event: [ConvoEvent]}>() private emitter = new EventEmitter<{event: [ConvoEvent]}>()
convoId: string convoId: string
convo: ChatBskyConvoDefs.ConvoView | undefined convo: ConvoWithDetails | undefined
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
snapshot: ConvoState | undefined snapshot: ConvoState | undefined
constructor(params: ConvoParams) { constructor(params: ConvoParams) {
@@ -126,8 +121,8 @@ export class Convo {
this.events = params.events this.events = params.events
this.senderUserDid = params.agent.assertDid this.senderUserDid = params.agent.assertDid
if (params.placeholderData) { if (params.initialData) {
this.setupPlaceholderData(params.placeholderData) this.setupInitialData(params.initialData)
} }
this.subscribe = this.subscribe.bind(this) this.subscribe = this.subscribe.bind(this)
@@ -141,10 +136,6 @@ export class Convo {
this.markConvoAccepted = this.markConvoAccepted.bind(this) this.markConvoAccepted = this.markConvoAccepted.bind(this)
this.addReaction = this.addReaction.bind(this) this.addReaction = this.addReaction.bind(this)
this.removeReaction = this.removeReaction.bind(this) this.removeReaction = this.removeReaction.bind(this)
this.isGroup = this.isGroup.bind(this)
this.getGroupInfo = this.getGroupInfo.bind(this)
this.getPrimaryMember = this.getPrimaryMember.bind(this)
this.updateGroupName = this.updateGroupName.bind(this)
} }
private commit() { private commit() {
@@ -179,8 +170,6 @@ export class Convo {
items: [], items: [],
convo: this.convo, convo: this.convo,
error: undefined, error: undefined,
sender: this.sender,
recipients: this.recipients,
isFetchingHistory: this.isFetchingHistory, isFetchingHistory: this.isFetchingHistory,
// Explicit null check since the value is initially undefined. // Explicit null check since the value is initially undefined.
hasAllHistory: this.oldestRev === null, hasAllHistory: this.oldestRev === null,
@@ -190,9 +179,6 @@ export class Convo {
markConvoAccepted: undefined, markConvoAccepted: undefined,
addReaction: undefined, addReaction: undefined,
removeReaction: undefined, removeReaction: undefined,
isGroup: this.isGroup,
getGroupInfo: this.getGroupInfo,
getPrimaryMember: this.getPrimaryMember,
} }
} }
case ConvoStatus.Disabled: case ConvoStatus.Disabled:
@@ -204,8 +190,6 @@ export class Convo {
items: this.getItems(), items: this.getItems(),
convo: this.convo!, convo: this.convo!,
error: undefined, error: undefined,
sender: this.sender!,
recipients: this.recipients!,
isFetchingHistory: this.isFetchingHistory, isFetchingHistory: this.isFetchingHistory,
// Explicit null check since the value is initially undefined. // Explicit null check since the value is initially undefined.
hasAllHistory: this.oldestRev === null, hasAllHistory: this.oldestRev === null,
@@ -215,10 +199,7 @@ export class Convo {
markConvoAccepted: this.markConvoAccepted, markConvoAccepted: this.markConvoAccepted,
addReaction: this.addReaction, addReaction: this.addReaction,
removeReaction: this.removeReaction, removeReaction: this.removeReaction,
isGroup: this.isGroup, } as ConvoState
getGroupInfo: this.getGroupInfo,
getPrimaryMember: this.getPrimaryMember,
}
} }
case ConvoStatus.Error: { case ConvoStatus.Error: {
return { return {
@@ -226,8 +207,6 @@ export class Convo {
items: [], items: [],
convo: undefined, convo: undefined,
error: this.error!, error: this.error!,
sender: undefined,
recipients: undefined,
isFetchingHistory: false, isFetchingHistory: false,
hasAllHistory: false, hasAllHistory: false,
deleteMessage: undefined, deleteMessage: undefined,
@@ -236,9 +215,6 @@ export class Convo {
markConvoAccepted: undefined, markConvoAccepted: undefined,
addReaction: undefined, addReaction: undefined,
removeReaction: undefined, removeReaction: undefined,
isGroup: undefined,
getGroupInfo: undefined,
getPrimaryMember: undefined,
} }
} }
default: { default: {
@@ -247,8 +223,6 @@ export class Convo {
items: [], items: [],
convo: this.convo, convo: this.convo,
error: undefined, error: undefined,
sender: this.sender,
recipients: this.recipients,
isFetchingHistory: false, isFetchingHistory: false,
// Explicit null check since the value is initially undefined. // Explicit null check since the value is initially undefined.
hasAllHistory: this.oldestRev === null, hasAllHistory: this.oldestRev === null,
@@ -258,9 +232,6 @@ export class Convo {
markConvoAccepted: undefined, markConvoAccepted: undefined,
addReaction: undefined, addReaction: undefined,
removeReaction: undefined, removeReaction: undefined,
isGroup: this.isGroup,
getGroupInfo: this.getGroupInfo,
getPrimaryMember: this.getPrimaryMember,
} }
} }
} }
@@ -274,7 +245,7 @@ export class Convo {
switch (action.event) { switch (action.event) {
case ConvoDispatchEvent.Init: { case ConvoDispatchEvent.Init: {
this.status = ConvoStatus.Initializing this.status = ConvoStatus.Initializing
void this.setup() this.setup()
this.setupFirehose() this.setupFirehose()
this.requestPollInterval(ACTIVE_POLL_INTERVAL) this.requestPollInterval(ACTIVE_POLL_INTERVAL)
break break
@@ -321,7 +292,6 @@ export class Convo {
case ConvoStatus.Ready: { case ConvoStatus.Ready: {
switch (action.event) { switch (action.event) {
case ConvoDispatchEvent.Resume: { case ConvoDispatchEvent.Resume: {
void this.refreshConvo()
this.requestPollInterval(ACTIVE_POLL_INTERVAL) this.requestPollInterval(ACTIVE_POLL_INTERVAL)
break break
} }
@@ -360,11 +330,10 @@ export class Convo {
} else { } else {
if (this.convo) { if (this.convo) {
this.status = ConvoStatus.Ready this.status = ConvoStatus.Ready
void this.refreshConvo()
this.maybeRecoverFromNetworkError() this.maybeRecoverFromNetworkError()
} else { } else {
this.status = ConvoStatus.Initializing this.status = ConvoStatus.Initializing
void this.setup() this.setup()
} }
this.requestPollInterval(ACTIVE_POLL_INTERVAL) this.requestPollInterval(ACTIVE_POLL_INTERVAL)
} }
@@ -460,8 +429,6 @@ export class Convo {
private reset() { private reset() {
this.convo = undefined this.convo = undefined
this.sender = undefined
this.recipients = undefined
this.snapshot = undefined this.snapshot = undefined
this.status = ConvoStatus.Uninitialized this.status = ConvoStatus.Uninitialized
@@ -473,7 +440,7 @@ export class Convo {
this.newMessages = new Map() this.newMessages = new Map()
this.pendingMessages = new Map() this.pendingMessages = new Map()
this.deletedMessages = new Set() this.deletedMessages = new Set()
this.systemMessageProfiles = new Map() this.relatedProfiles = new Map()
this.pendingMessageFailure = null this.pendingMessageFailure = null
this.fetchMessageHistoryError = undefined this.fetchMessageHistoryError = undefined
@@ -498,70 +465,64 @@ export class Convo {
} }
} }
/** private setupInitialData(data: NonNullable<ConvoParams['initialData']>) {
* Initialises the convo with placeholder data, if provided. We still refetch it before rendering the convo, if (data.convo) {
* but this allows us to render the convo header immediately. if (data.members) {
*/ this.setConvoData(data.convo, data.members)
private setupPlaceholderData( } else {
data: NonNullable<ConvoParams['placeholderData']>, const parsed = parseConvoView(data.convo, this.senderUserDid)
) { if (parsed) {
this.convo = data.convo this.convo = parsed
this.sender = data.convo.members.find(m => m.did === this.senderUserDid) }
this.recipients = data.convo.members.filter( for (const member of data.convo.members) {
m => m.did !== this.senderUserDid, this.relatedProfiles.set(member.did, member)
) }
}
}
} }
private async setup() { private setup() {
try { this.tryReady()
const {convo, sender, recipients} = await this.fetchConvo() }
this.convo = convo private tryReady() {
this.sender = sender if (this.status !== ConvoStatus.Initializing) return
this.recipients = recipients if (!this.convo) return
/* const self = this.relatedProfiles.get(this.senderUserDid)
* Some validation prior to `Ready` status if (!self) return
*/
if (!this.convo) {
throw new Error('could not find convo')
}
if (!this.sender) {
throw new Error('could not find sender in convo')
}
if (!this.recipients) {
throw new Error('could not find recipients in convo')
}
const userIsDisabled = Boolean(this.sender.chatDisabled) const userIsDisabled = Boolean('chatDisabled' in self && self.chatDisabled)
if (userIsDisabled) { if (userIsDisabled) {
this.dispatch({event: ConvoDispatchEvent.Disable}) this.dispatch({event: ConvoDispatchEvent.Disable})
} else { } else {
this.dispatch({event: ConvoDispatchEvent.Ready}) this.dispatch({event: ConvoDispatchEvent.Ready})
}
} catch (err) {
const e = err as Error
if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) {
logger.error('setup failed', {
safeMessage: e.message,
})
}
this.dispatch({
event: ConvoDispatchEvent.Error,
payload: {
exception: e,
code: ConvoErrorCode.InitFailed,
retry: () => {
this.reset()
},
},
})
this.commit()
} }
} }
setConvoData(
convo: ChatBskyConvoDefs.ConvoView,
members: ChatBskyActorDefs.ProfileViewBasic[],
) {
const parsed = parseConvoView(convo, this.senderUserDid, members)
if (!parsed) return
this.convo = parsed
for (const member of members) {
this.relatedProfiles.set(member.did, member)
}
for (const member of convo.members) {
if (!this.relatedProfiles.has(member.did)) {
this.relatedProfiles.set(member.did, member)
}
}
this.tryReady()
this.commit()
}
init() { init() {
this.dispatch({event: ConvoDispatchEvent.Init}) this.dispatch({event: ConvoDispatchEvent.Init})
} }
@@ -601,59 +562,6 @@ export class Convo {
} }
} }
private pendingFetchConvo:
| Promise<{
convo: ChatBskyConvoDefs.ConvoView
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
recipients: ChatBskyActorDefs.ProfileViewBasic[]
}>
| undefined
async fetchConvo() {
if (this.pendingFetchConvo) return this.pendingFetchConvo
this.pendingFetchConvo = (async () => {
try {
const response = await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.getConvo(
{
convoId: this.convoId,
},
{headers: DM_SERVICE_HEADERS},
)
})
const convo = response.data.convo
return {
convo,
sender: convo.members.find(m => m.did === this.senderUserDid),
recipients: convo.members.filter(m => m.did !== this.senderUserDid),
}
} finally {
this.pendingFetchConvo = undefined
}
})()
return this.pendingFetchConvo
}
async refreshConvo() {
try {
const {convo, sender, recipients} = await this.fetchConvo()
// throw new Error('UNCOMMENT TO TEST REFRESH FAILURE')
this.convo = convo || this.convo
this.sender = sender || this.sender
this.recipients = recipients || this.recipients
} catch (err) {
const e = err as Error
if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) {
logger.error(`failed to refresh convo`, {
safeMessage: e.message,
})
}
}
}
private fetchMessageHistoryError: private fetchMessageHistoryError:
| { | {
retry: () => void retry: () => void
@@ -689,7 +597,7 @@ export class Convo {
{ {
cursor: nextCursor, cursor: nextCursor,
convoId: this.convoId, convoId: this.convoId,
limit: IS_NATIVE ? 30 : 60, limit: GET_MESSAGE_HISTORY_LIMIT,
}, },
{headers: DM_SERVICE_HEADERS}, {headers: DM_SERVICE_HEADERS},
) )
@@ -700,7 +608,7 @@ export class Convo {
if (relatedProfiles) { if (relatedProfiles) {
for (const profile of relatedProfiles) { for (const profile of relatedProfiles) {
this.systemMessageProfiles.set(profile.did, profile) this.relatedProfiles.set(profile.did, profile)
} }
} }
@@ -708,7 +616,7 @@ export class Convo {
* If the response contained fewer messages than the limit, we know * If the response contained fewer messages than the limit, we know
* there are no more pages, regardless of whether a cursor was returned. * there are no more pages, regardless of whether a cursor was returned.
*/ */
if (messages.length < (IS_NATIVE ? 30 : 60)) { if (messages.length < GET_MESSAGE_HISTORY_LIMIT) {
this.oldestRev = null this.oldestRev = null
} }
@@ -729,6 +637,10 @@ export class Convo {
this.pastMessages.set(message.id, message) this.pastMessages.set(message.id, message)
} }
} }
for (const profile of relatedProfiles ?? []) {
this.relatedProfiles.set(profile.did, profile)
}
} catch (err) { } catch (err) {
const e = err as Error const e = err as Error
if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) { if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) {
@@ -877,7 +789,7 @@ export class Convo {
Array.isArray(ev.relatedProfiles) Array.isArray(ev.relatedProfiles)
) { ) {
for (const profile of ev.relatedProfiles) { for (const profile of ev.relatedProfiles) {
this.systemMessageProfiles.set(profile.did, profile) this.relatedProfiles.set(profile.did, profile)
} }
} }
needsCommit = true needsCommit = true
@@ -907,10 +819,10 @@ export class Convo {
id: tempId, id: tempId,
message, message,
}) })
if (this.convo?.status === 'request') { if (this.convo?.view.status === 'request') {
this.convo = { this.convo = {
...this.convo, ...this.convo,
status: 'accepted', view: {...this.convo.view, status: 'accepted'},
} }
} }
this.commit() this.commit()
@@ -924,36 +836,7 @@ export class Convo {
if (this.convo) { if (this.convo) {
this.convo = { this.convo = {
...this.convo, ...this.convo,
status: 'accepted', view: {...this.convo.view, status: 'accepted'},
}
}
this.commit()
}
updateMuted(muted: boolean) {
if (this.convo) {
this.convo = {
...this.convo,
muted,
}
}
this.commit()
}
updateGroupName(name: string) {
if (
this.convo &&
bsky.dangerousIsType<ChatBskyConvoDefs.GroupConvo>(
this.convo.kind,
ChatBskyConvoDefs.isGroupConvo,
)
) {
this.convo = {
...this.convo,
kind: {
...this.convo.kind,
name,
},
} }
} }
this.commit() this.commit()
@@ -980,7 +863,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,
@@ -1022,10 +905,7 @@ export class Convo {
case 'block between recipient and sender': case 'block between recipient and sender':
this.emitter.emit('event', { this.emitter.emit('event', {
type: 'invalidate-block-state', type: 'invalidate-block-state',
accountDids: [ accountDids: [this.senderUserDid, ...this.relatedProfiles.keys()],
this.sender!.did,
...this.recipients!.map(r => r.did),
],
}) })
break break
case 'Account is disabled': case 'Account is disabled':
@@ -1075,7 +955,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,
@@ -1117,7 +997,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,
@@ -1146,6 +1026,39 @@ export class Convo {
} }
} }
private getRelatedProfilesForItem(
m: ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.SystemMessageView,
): bsky.profile.AnyProfileView[] {
const seen = new Set<string>()
const profiles: bsky.profile.AnyProfileView[] = []
const add = (did: string) => {
if (seen.has(did)) return
seen.add(did)
const p = this.relatedProfiles.get(did)
if (p) profiles.push(p)
}
if (ChatBskyConvoDefs.isMessageView(m)) {
add(m.sender.did)
if (m.reactions) {
for (const reaction of m.reactions) {
add(reaction.sender.did)
}
}
} else if (ChatBskyConvoDefs.isSystemMessageView(m)) {
const data = m.data
if ('member' in data && data.member?.did) {
add(data.member.did)
}
if ('addedBy' in data && data.addedBy?.did) {
add(data.addedBy.did)
}
}
return profiles
}
/* /*
* Items in reverse order, since FlatList inverts * Items in reverse order, since FlatList inverts
*/ */
@@ -1158,6 +1071,7 @@ export class Convo {
type: 'message', type: 'message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.getRelatedProfilesForItem(m),
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1174,7 +1088,7 @@ export class Convo {
type: 'system-message', type: 'system-message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: Array.from(this.systemMessageProfiles.values()), relatedProfiles: this.getRelatedProfilesForItem(m),
}) })
} }
}) })
@@ -1196,6 +1110,7 @@ export class Convo {
type: 'message', type: 'message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.getRelatedProfilesForItem(m),
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1212,7 +1127,7 @@ export class Convo {
type: 'system-message', type: 'system-message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: Array.from(this.systemMessageProfiles.values()), relatedProfiles: this.getRelatedProfilesForItem(m),
}) })
} }
}) })
@@ -1234,7 +1149,7 @@ export class Convo {
*/ */
sender: { sender: {
$type: 'chat.bsky.convo.defs#messageViewSender', $type: 'chat.bsky.convo.defs#messageViewSender',
did: this.sender!.did, did: this.senderUserDid,
}, },
}, },
nextMessage: null, nextMessage: null,
@@ -1448,46 +1363,4 @@ export class Convo {
throw error throw error
} }
} }
// Group utilities
isGroup(): boolean | undefined {
if (!this.convo) return undefined
const info = this.getGroupInfo()
return !!info
}
getGroupInfo(): ChatBskyConvoDefs.GroupConvo | undefined {
if (
this.convo &&
bsky.dangerousIsType<ChatBskyConvoDefs.GroupConvo>(
this.convo.kind,
ChatBskyConvoDefs.isGroupConvo,
)
) {
return this.convo.kind
}
return undefined
}
getPrimaryMember(): ChatBskyActorDefs.ProfileViewBasic | undefined {
if (this.isGroup()) {
return this.convo?.members.find(m => {
if (
bsky.dangerousIsType<ChatBskyActorDefs.GroupConvoMember>(
m.kind,
ChatBskyActorDefs.isGroupConvoMember,
)
) {
return m.kind.role === 'owner'
} else {
throw new Error(
'Expected a GroupConvoMember, got an unknown kind of member',
)
}
})
} else {
return this.recipients?.find(r => r.did !== this.senderUserDid)
}
}
} }
+45 -33
View File
@@ -6,13 +6,14 @@ import {
useState, useState,
useSyncExternalStore, useSyncExternalStore,
} from 'react' } from 'react'
import {ChatBskyConvoDefs} from '@atproto/api'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useAppState} from '#/lib/appState' import {useAppState} from '#/lib/appState'
import {Convo} from '#/state/messages/convo/agent' import {Convo} from '#/state/messages/convo/agent'
import { import {
ConvoDispatchEvent,
ConvoErrorCode,
type ConvoParams, type ConvoParams,
type ConvoState, type ConvoState,
type ConvoStateBackgrounded, type ConvoStateBackgrounded,
@@ -23,10 +24,11 @@ import {
import {isConvoActive} from '#/state/messages/convo/util' import {isConvoActive} from '#/state/messages/convo/util'
import {useMessagesEventBus} from '#/state/messages/events' import {useMessagesEventBus} from '#/state/messages/events'
import { import {
RQKEY as getConvoKey, useConvoQuery,
useMarkAsReadMutation, useMarkAsReadMutation,
} from '#/state/queries/messages/conversation' } from '#/state/queries/messages/conversation'
import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations' import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations'
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile' import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
@@ -72,20 +74,52 @@ export function ConvoProvider({
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const agent = useAgent()
const events = useMessagesEventBus() const events = useMessagesEventBus()
const {
data: convoData,
error: convoError,
refetch: refetchConvo,
} = useConvoQuery({convoId})
const {
data: membersData,
error: membersError,
refetch: refetchMembers,
} = useListConvoMembersQuery({convoId})
// eslint-disable-next-line react/hook-use-state
const [convo] = useState(() => { const [convo] = useState(() => {
const placeholder = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
getConvoKey(convoId),
)
return new Convo({ return new Convo({
convoId, convoId,
agent, agent,
events, events,
placeholderData: placeholder ? {convo: placeholder} : undefined, initialData: {convo: convoData, members: membersData},
}) })
}) })
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot) const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
const {mutate: markAsRead} = useMarkAsReadMutation() const {mutate: markAsRead} = useMarkAsReadMutation()
useEffect(() => {
if (convoData && membersData) {
convo.setConvoData(convoData, membersData)
}
}, [convo, convoData, membersData])
useEffect(() => {
if ((convoError || membersError) && !convo.convo) {
convo.dispatch({
event: ConvoDispatchEvent.Error,
payload: {
exception: (convoError || membersError) as Error,
code: ConvoErrorCode.InitFailed,
retry: () => {
void refetchConvo()
void refetchMembers()
},
},
})
}
}, [convo, convoError, membersError, refetchConvo, refetchMembers])
const appState = useAppState() const appState = useAppState()
const isActive = appState === 'active' const isActive = appState === 'active'
useFocusEffect( useFocusEffect(
@@ -93,13 +127,15 @@ export function ConvoProvider({
if (isActive) { if (isActive) {
convo.resume() convo.resume()
markAsRead({convoId}) markAsRead({convoId})
void refetchConvo()
void refetchMembers()
return () => { return () => {
convo.background() convo.background()
markAsRead({convoId}) markAsRead({convoId})
} }
} }
}, [isActive, convo, convoId, markAsRead]), }, [isActive, convo, convoId, markAsRead, refetchConvo, refetchMembers]),
) )
useEffect(() => { useEffect(() => {
@@ -107,11 +143,11 @@ export function ConvoProvider({
switch (event.type) { switch (event.type) {
case 'invalidate-block-state': { case 'invalidate-block-state': {
for (const did of event.accountDids) { for (const did of event.accountDids) {
queryClient.invalidateQueries({ void queryClient.invalidateQueries({
queryKey: createProfileQueryKey(did), queryKey: createProfileQueryKey(did),
}) })
} }
queryClient.invalidateQueries({ void queryClient.invalidateQueries({
queryKey: [ListConvosQueryKeyRoot], queryKey: [ListConvosQueryKeyRoot],
}) })
} }
@@ -119,29 +155,5 @@ export function ConvoProvider({
}) })
}, [convo, queryClient]) }, [convo, queryClient])
useEffect(() => {
const [root, id] = getConvoKey(convoId)
return queryClient.getQueryCache().subscribe(event => {
const queryKey = event.query.queryKey as string[]
if (queryKey[0] === root && queryKey[1] === id) {
const data = event.query.state.data as
| ChatBskyConvoDefs.ConvoView
| undefined
if (data && convo.convo && data.muted !== convo.convo.muted) {
convo.updateMuted(data.muted)
}
if (
data &&
convo.convo &&
ChatBskyConvoDefs.isGroupConvo(data.kind) &&
ChatBskyConvoDefs.isGroupConvo(convo.convo.kind) &&
data.kind.name !== convo.convo.kind.name
) {
convo.updateGroupName(data.kind.name)
}
}
})
}, [convo, convoId, queryClient])
return <ChatContext.Provider value={service}>{children}</ChatContext.Provider> return <ChatContext.Provider value={service}>{children}</ChatContext.Provider>
} }
+2 -2
View File
@@ -16,7 +16,7 @@ import {
RQKEY_ROOT as LIST_CONVOS_KEY, RQKEY_ROOT as LIST_CONVOS_KEY,
} from './list-conversations' } from './list-conversations'
const RQKEY_ROOT = 'convo' export const RQKEY_ROOT = 'convo'
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
export function useConvoQuery({convoId}: {convoId: string}) { export function useConvoQuery({convoId}: {convoId: string}) {
@@ -31,7 +31,7 @@ export function useConvoQuery({convoId}: {convoId: string}) {
) )
return data.convo return data.convo
}, },
staleTime: STALE.INFINITY, staleTime: STALE.MINUTES.THIRTY,
}) })
} }