Group Clops Feature Branch (#10360)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
+2
@@ -11,6 +11,7 @@ import {findAllProfilesInQueryData as findAllProfilesInContactMatchesQueryData}
|
||||
import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '#/state/queries/known-followers'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '#/state/queries/list-members'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '#/state/queries/messages/list-conversations'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInMessagesQueryData} from '#/state/queries/messages/list-convo-members'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '#/state/queries/my-muted-accounts'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInNotifsQueryData} from '#/state/queries/notifications/feed'
|
||||
@@ -264,4 +265,5 @@ function* findProfilesInCache(
|
||||
yield* findAllProfilesInActivitySubscriptionsQueryData(queryClient, did)
|
||||
yield* findAllProfilesInNotifsQueryData(queryClient, did)
|
||||
yield* findAllProfilesInContactMatchesQueryData(queryClient, did)
|
||||
yield* findAllProfilesInMessagesQueryData(queryClient, did)
|
||||
}
|
||||
|
||||
+224
-192
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
type AtpAgent,
|
||||
ChatBskyActorDefs,
|
||||
type ChatBskyActorDefs,
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyConvoGetLog,
|
||||
type ChatBskyConvoSendMessage,
|
||||
type ChatBskyGroupDefs,
|
||||
} from '@atproto/api'
|
||||
import {XRPCError} from '@atproto/api'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
@@ -36,8 +37,12 @@ import {
|
||||
} from '#/state/messages/convo/types'
|
||||
import {type MessagesEventBus} from '#/state/messages/events/agent'
|
||||
import {type MessagesEventBusError} from '#/state/messages/events/types'
|
||||
import {
|
||||
type ConvoWithDetails,
|
||||
type GroupConvoMember,
|
||||
parseConvoView,
|
||||
} from '#/components/dms/util'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const logger = Logger.create(Logger.Context.ConversationAgent)
|
||||
|
||||
@@ -102,10 +107,8 @@ export class Convo {
|
||||
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
|
||||
> = new Map()
|
||||
private deletedMessages: Set<string> = new Set()
|
||||
private systemMessageProfiles: Map<
|
||||
string,
|
||||
ChatBskyActorDefs.ProfileViewBasic
|
||||
> = new Map()
|
||||
private relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic> =
|
||||
new Map()
|
||||
|
||||
private isProcessingPendingMessages = false
|
||||
|
||||
@@ -114,7 +117,7 @@ export class Convo {
|
||||
private emitter = new EventEmitter<{event: [ConvoEvent]}>()
|
||||
|
||||
convoId: string
|
||||
convo: ChatBskyConvoDefs.ConvoView | undefined
|
||||
convo: ConvoWithDetails | undefined
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
|
||||
snapshot: ConvoState | undefined
|
||||
@@ -130,6 +133,7 @@ export class Convo {
|
||||
this.setupPlaceholderData(params.placeholderData)
|
||||
}
|
||||
|
||||
this.setConvo = this.setConvo.bind(this)
|
||||
this.subscribe = this.subscribe.bind(this)
|
||||
this.getSnapshot = this.getSnapshot.bind(this)
|
||||
this.sendMessage = this.sendMessage.bind(this)
|
||||
@@ -141,10 +145,10 @@ export class Convo {
|
||||
this.markConvoAccepted = this.markConvoAccepted.bind(this)
|
||||
this.addReaction = this.addReaction.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)
|
||||
this.updateGroupMembers = this.updateGroupMembers.bind(this)
|
||||
this.updateJoinLink = this.updateJoinLink.bind(this)
|
||||
this.updateLockStatus = this.updateLockStatus.bind(this)
|
||||
}
|
||||
|
||||
private commit() {
|
||||
@@ -172,6 +176,30 @@ export class Convo {
|
||||
}
|
||||
|
||||
private generateSnapshot(): ConvoState {
|
||||
const shared = {
|
||||
isFetchingHistory: this.isFetchingHistory,
|
||||
// Explicit null check since the value is initially undefined.
|
||||
hasAllHistory: this.oldestRev === null,
|
||||
}
|
||||
|
||||
const methods = {
|
||||
deleteMessage: this.deleteMessage,
|
||||
sendMessage: this.sendMessage,
|
||||
fetchMessageHistory: this.fetchMessageHistory,
|
||||
markConvoAccepted: this.markConvoAccepted,
|
||||
addReaction: this.addReaction,
|
||||
removeReaction: this.removeReaction,
|
||||
}
|
||||
|
||||
const emptyMethods = {
|
||||
deleteMessage: undefined,
|
||||
sendMessage: undefined,
|
||||
fetchMessageHistory: undefined,
|
||||
markConvoAccepted: undefined,
|
||||
addReaction: undefined,
|
||||
removeReaction: undefined,
|
||||
}
|
||||
|
||||
switch (this.status) {
|
||||
case ConvoStatus.Initializing: {
|
||||
return {
|
||||
@@ -179,45 +207,48 @@ export class Convo {
|
||||
items: [],
|
||||
convo: this.convo,
|
||||
error: undefined,
|
||||
sender: this.sender,
|
||||
recipients: this.recipients,
|
||||
isFetchingHistory: this.isFetchingHistory,
|
||||
// Explicit null check since the value is initially undefined.
|
||||
hasAllHistory: this.oldestRev === null,
|
||||
deleteMessage: undefined,
|
||||
sendMessage: undefined,
|
||||
fetchMessageHistory: undefined,
|
||||
markConvoAccepted: undefined,
|
||||
addReaction: undefined,
|
||||
removeReaction: undefined,
|
||||
isGroup: this.isGroup,
|
||||
getGroupInfo: this.getGroupInfo,
|
||||
getPrimaryMember: this.getPrimaryMember,
|
||||
...shared,
|
||||
...emptyMethods,
|
||||
}
|
||||
}
|
||||
case ConvoStatus.Disabled: {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
error: undefined,
|
||||
...shared,
|
||||
...methods,
|
||||
}
|
||||
}
|
||||
case ConvoStatus.Suspended: {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
error: undefined,
|
||||
...shared,
|
||||
...methods,
|
||||
}
|
||||
}
|
||||
case ConvoStatus.Backgrounded: {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
error: undefined,
|
||||
...shared,
|
||||
...methods,
|
||||
}
|
||||
}
|
||||
case ConvoStatus.Disabled:
|
||||
case ConvoStatus.Suspended:
|
||||
case ConvoStatus.Backgrounded:
|
||||
case ConvoStatus.Ready: {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
error: undefined,
|
||||
sender: this.sender!,
|
||||
recipients: this.recipients!,
|
||||
isFetchingHistory: this.isFetchingHistory,
|
||||
// Explicit null check since the value is initially undefined.
|
||||
hasAllHistory: this.oldestRev === null,
|
||||
deleteMessage: this.deleteMessage,
|
||||
sendMessage: this.sendMessage,
|
||||
fetchMessageHistory: this.fetchMessageHistory,
|
||||
markConvoAccepted: this.markConvoAccepted,
|
||||
addReaction: this.addReaction,
|
||||
removeReaction: this.removeReaction,
|
||||
isGroup: this.isGroup,
|
||||
getGroupInfo: this.getGroupInfo,
|
||||
getPrimaryMember: this.getPrimaryMember,
|
||||
...shared,
|
||||
...methods,
|
||||
}
|
||||
}
|
||||
case ConvoStatus.Error: {
|
||||
@@ -226,19 +257,9 @@ export class Convo {
|
||||
items: [],
|
||||
convo: undefined,
|
||||
error: this.error!,
|
||||
sender: undefined,
|
||||
recipients: undefined,
|
||||
isFetchingHistory: false,
|
||||
hasAllHistory: false,
|
||||
deleteMessage: undefined,
|
||||
sendMessage: undefined,
|
||||
fetchMessageHistory: undefined,
|
||||
markConvoAccepted: undefined,
|
||||
addReaction: undefined,
|
||||
removeReaction: undefined,
|
||||
isGroup: undefined,
|
||||
getGroupInfo: undefined,
|
||||
getPrimaryMember: undefined,
|
||||
...emptyMethods,
|
||||
}
|
||||
}
|
||||
default: {
|
||||
@@ -247,20 +268,10 @@ export class Convo {
|
||||
items: [],
|
||||
convo: this.convo,
|
||||
error: undefined,
|
||||
sender: this.sender,
|
||||
recipients: this.recipients,
|
||||
isFetchingHistory: false,
|
||||
// Explicit null check since the value is initially undefined.
|
||||
hasAllHistory: this.oldestRev === null,
|
||||
deleteMessage: undefined,
|
||||
sendMessage: undefined,
|
||||
fetchMessageHistory: undefined,
|
||||
markConvoAccepted: undefined,
|
||||
addReaction: undefined,
|
||||
removeReaction: undefined,
|
||||
isGroup: this.isGroup,
|
||||
getGroupInfo: this.getGroupInfo,
|
||||
getPrimaryMember: this.getPrimaryMember,
|
||||
...emptyMethods,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,8 +471,6 @@ export class Convo {
|
||||
|
||||
private reset() {
|
||||
this.convo = undefined
|
||||
this.sender = undefined
|
||||
this.recipients = undefined
|
||||
this.snapshot = undefined
|
||||
|
||||
this.status = ConvoStatus.Uninitialized
|
||||
@@ -473,7 +482,7 @@ export class Convo {
|
||||
this.newMessages = new Map()
|
||||
this.pendingMessages = new Map()
|
||||
this.deletedMessages = new Set()
|
||||
this.systemMessageProfiles = new Map()
|
||||
this.relatedProfiles = new Map()
|
||||
|
||||
this.pendingMessageFailure = null
|
||||
this.fetchMessageHistoryError = undefined
|
||||
@@ -498,6 +507,26 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
private setConvo(convo: ChatBskyConvoDefs.ConvoView) {
|
||||
this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo
|
||||
if (this.convo) {
|
||||
for (const member of this.convo.members) {
|
||||
this.relatedProfiles.set(member.did, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateConvo(convo: Partial<ChatBskyConvoDefs.ConvoView>) {
|
||||
if (this.convo) {
|
||||
this.convo =
|
||||
parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ??
|
||||
this.convo
|
||||
for (const member of this.convo.members) {
|
||||
this.relatedProfiles.set(member.did, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -505,20 +534,14 @@ export class Convo {
|
||||
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,
|
||||
)
|
||||
this.setConvo(data.convo)
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
try {
|
||||
const {convo, sender, recipients} = await this.fetchConvo()
|
||||
const {convo} = await this.fetchConvo()
|
||||
|
||||
this.convo = convo
|
||||
this.sender = sender
|
||||
this.recipients = recipients
|
||||
this.setConvo(convo)
|
||||
|
||||
/*
|
||||
* Some validation prior to `Ready` status
|
||||
@@ -526,14 +549,14 @@ export class Convo {
|
||||
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 self = this.convo.members.find(m => m.did === this.senderUserDid)
|
||||
|
||||
if (!self) {
|
||||
throw new Error('could not find self in convo')
|
||||
}
|
||||
|
||||
const userIsDisabled = Boolean(this.sender.chatDisabled)
|
||||
const userIsDisabled = Boolean(self.chatDisabled)
|
||||
|
||||
if (userIsDisabled) {
|
||||
this.dispatch({event: ConvoDispatchEvent.Disable})
|
||||
@@ -602,22 +625,19 @@ export class Convo {
|
||||
}
|
||||
|
||||
private pendingFetchConvo:
|
||||
| Promise<{
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
}>
|
||||
| Promise<{convo: ChatBskyConvoDefs.ConvoView}>
|
||||
| undefined
|
||||
async fetchConvo() {
|
||||
if (this.pendingFetchConvo) return this.pendingFetchConvo
|
||||
|
||||
// non-blocking
|
||||
void this.fetchMemberList()
|
||||
|
||||
this.pendingFetchConvo = (async () => {
|
||||
try {
|
||||
const response = await networkRetry(2, () => {
|
||||
return this.agent.api.chat.bsky.convo.getConvo(
|
||||
{
|
||||
convoId: this.convoId,
|
||||
},
|
||||
return this.agent.chat.bsky.convo.getConvo(
|
||||
{convoId: this.convoId},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
})
|
||||
@@ -626,8 +646,6 @@ export class 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
|
||||
@@ -639,11 +657,10 @@ export class Convo {
|
||||
|
||||
async refreshConvo() {
|
||||
try {
|
||||
const {convo, sender, recipients} = await this.fetchConvo()
|
||||
void this.fetchMemberList()
|
||||
const {convo} = 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
|
||||
this.setConvo(convo)
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) {
|
||||
@@ -654,11 +671,32 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
private fetchMessageHistoryError:
|
||||
| {
|
||||
retry: () => void
|
||||
// purely for populating `this.relatedProfiles` - we do not pipe it
|
||||
// into the ConvoWithDetails. If you want to drive UI based on the member list,
|
||||
// use `useListConvoMembersQuery`
|
||||
// we shouldn't also block loading off of this - the UI should be resilient
|
||||
async fetchMemberList() {
|
||||
let cursor: string | undefined
|
||||
do {
|
||||
const result = await networkRetry(2, () => {
|
||||
return this.agent.chat.bsky.convo.getConvoMembers(
|
||||
{
|
||||
convoId: this.convoId,
|
||||
limit: 50,
|
||||
cursor,
|
||||
},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
})
|
||||
cursor = result.data.cursor
|
||||
|
||||
for (const member of result.data.members) {
|
||||
this.relatedProfiles.set(member.did, member)
|
||||
}
|
||||
| undefined
|
||||
} while (cursor)
|
||||
}
|
||||
|
||||
private fetchMessageHistoryError: {retry: () => void} | undefined
|
||||
async fetchMessageHistory() {
|
||||
logger.debug('fetch message history', {})
|
||||
|
||||
@@ -700,7 +738,7 @@ export class Convo {
|
||||
|
||||
if (relatedProfiles) {
|
||||
for (const profile of relatedProfiles) {
|
||||
this.systemMessageProfiles.set(profile.did, profile)
|
||||
this.relatedProfiles.set(profile.did, profile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,6 +858,12 @@ export class Convo {
|
||||
*/
|
||||
this.latestRev = ev.rev
|
||||
|
||||
if ('relatedProfiles' in ev && Array.isArray(ev.relatedProfiles)) {
|
||||
for (const profile of ev.relatedProfiles) {
|
||||
this.relatedProfiles.set(profile.did, profile)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
ChatBskyConvoDefs.isLogCreateMessage(ev) &&
|
||||
ChatBskyConvoDefs.isMessageView(ev.message)
|
||||
@@ -872,14 +916,6 @@ export class Convo {
|
||||
const systemView = toSystemMessageView(ev)
|
||||
if (systemView) {
|
||||
this.newMessages.set(systemView.id, systemView)
|
||||
if (
|
||||
'relatedProfiles' in ev &&
|
||||
Array.isArray(ev.relatedProfiles)
|
||||
) {
|
||||
for (const profile of ev.relatedProfiles) {
|
||||
this.systemMessageProfiles.set(profile.did, profile)
|
||||
}
|
||||
}
|
||||
needsCommit = true
|
||||
}
|
||||
}
|
||||
@@ -907,11 +943,10 @@ export class Convo {
|
||||
id: tempId,
|
||||
message,
|
||||
})
|
||||
if (this.convo?.status === 'request') {
|
||||
this.convo = {
|
||||
...this.convo,
|
||||
if (this.convo?.view.status === 'request') {
|
||||
this.updateConvo({
|
||||
status: 'accepted',
|
||||
}
|
||||
})
|
||||
}
|
||||
this.commit()
|
||||
|
||||
@@ -921,41 +956,79 @@ export class Convo {
|
||||
}
|
||||
|
||||
markConvoAccepted() {
|
||||
if (this.convo) {
|
||||
this.convo = {
|
||||
...this.convo,
|
||||
status: 'accepted',
|
||||
}
|
||||
}
|
||||
this.updateConvo({
|
||||
status: 'accepted',
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateMuted(muted: boolean) {
|
||||
if (this.convo) {
|
||||
this.convo = {
|
||||
...this.convo,
|
||||
muted,
|
||||
}
|
||||
}
|
||||
this.updateConvo({
|
||||
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,
|
||||
},
|
||||
}
|
||||
if (this.convo?.kind !== 'group') {
|
||||
throw new Error('updateGroupName can only be called on group convo')
|
||||
}
|
||||
|
||||
this.updateConvo({
|
||||
kind: {
|
||||
...this.convo.details,
|
||||
name,
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateGroupMembers(members: GroupConvoMember[], memberCount: number) {
|
||||
if (this.convo?.kind !== 'group') {
|
||||
throw new Error('updateGroupMembers can only be called on group convo')
|
||||
}
|
||||
|
||||
this.updateConvo({
|
||||
members,
|
||||
kind: {
|
||||
...this.convo.details,
|
||||
memberCount,
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateJoinLink(joinLink: ChatBskyGroupDefs.JoinLinkView | undefined) {
|
||||
if (this.convo?.kind !== 'group') {
|
||||
throw new Error('updateJoinLink can only be called on group convo')
|
||||
}
|
||||
|
||||
this.updateConvo({
|
||||
kind: {
|
||||
...this.convo.details,
|
||||
joinLink,
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
updateLockStatus(lockStatus: ChatBskyConvoDefs.ConvoLockStatus) {
|
||||
if (this.convo?.kind !== 'group') {
|
||||
throw new Error('updateLockStatus can only be called on group convo')
|
||||
}
|
||||
|
||||
this.updateConvo({
|
||||
kind: {
|
||||
...this.convo.details,
|
||||
lockStatus,
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
@@ -980,7 +1053,7 @@ export class Convo {
|
||||
|
||||
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,
|
||||
message,
|
||||
@@ -1023,7 +1096,7 @@ export class Convo {
|
||||
this.emitter.emit('event', {
|
||||
type: 'invalidate-block-state',
|
||||
accountDids: [
|
||||
this.sender!.did,
|
||||
this.senderUserDid,
|
||||
...this.recipients!.map(r => r.did),
|
||||
],
|
||||
})
|
||||
@@ -1075,7 +1148,7 @@ export class Convo {
|
||||
)
|
||||
|
||||
try {
|
||||
const {data} = await this.agent.api.chat.bsky.convo.sendMessageBatch(
|
||||
const {data} = await this.agent.chat.bsky.convo.sendMessageBatch(
|
||||
{
|
||||
items: messageArray.map(({message}) => ({
|
||||
convoId: this.convoId,
|
||||
@@ -1117,7 +1190,7 @@ export class Convo {
|
||||
|
||||
try {
|
||||
await networkRetry(2, () => {
|
||||
return this.agent.api.chat.bsky.convo.deleteMessageForSelf(
|
||||
return this.agent.chat.bsky.convo.deleteMessageForSelf(
|
||||
{
|
||||
convoId: this.convoId,
|
||||
messageId,
|
||||
@@ -1158,6 +1231,7 @@ export class Convo {
|
||||
type: 'message',
|
||||
key: m.id,
|
||||
message: m,
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
nextMessage: null,
|
||||
prevMessage: null,
|
||||
})
|
||||
@@ -1166,6 +1240,7 @@ export class Convo {
|
||||
type: 'deleted-message',
|
||||
key: m.id,
|
||||
message: m,
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
nextMessage: null,
|
||||
prevMessage: null,
|
||||
})
|
||||
@@ -1174,7 +1249,7 @@ export class Convo {
|
||||
type: 'system-message',
|
||||
key: m.id,
|
||||
message: m,
|
||||
relatedProfiles: Array.from(this.systemMessageProfiles.values()),
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1196,6 +1271,7 @@ export class Convo {
|
||||
type: 'message',
|
||||
key: m.id,
|
||||
message: m,
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
nextMessage: null,
|
||||
prevMessage: null,
|
||||
})
|
||||
@@ -1204,6 +1280,7 @@ export class Convo {
|
||||
type: 'deleted-message',
|
||||
key: m.id,
|
||||
message: m,
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
nextMessage: null,
|
||||
prevMessage: null,
|
||||
})
|
||||
@@ -1212,7 +1289,7 @@ export class Convo {
|
||||
type: 'system-message',
|
||||
key: m.id,
|
||||
message: m,
|
||||
relatedProfiles: Array.from(this.systemMessageProfiles.values()),
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1228,15 +1305,12 @@ export class Convo {
|
||||
id: nanoid(),
|
||||
rev: '__fake__',
|
||||
sentAt: new Date().toISOString(),
|
||||
/*
|
||||
* `getItems` is only run in "active" status states, where
|
||||
* `this.sender` is defined
|
||||
*/
|
||||
sender: {
|
||||
$type: 'chat.bsky.convo.defs#messageViewSender',
|
||||
did: this.sender!.did,
|
||||
did: this.senderUserDid,
|
||||
},
|
||||
},
|
||||
relatedProfiles: this.relatedProfiles,
|
||||
nextMessage: null,
|
||||
prevMessage: null,
|
||||
failed: this.pendingMessageFailure !== null,
|
||||
@@ -1448,46 +1522,4 @@ export class Convo {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,19 @@ import {
|
||||
import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations'
|
||||
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {type GroupConvoMember} from '#/components/dms/util'
|
||||
|
||||
export * from '#/state/messages/convo/util'
|
||||
|
||||
function membersChanged(
|
||||
a: ChatBskyConvoDefs.ConvoView['members'],
|
||||
b: ChatBskyConvoDefs.ConvoView['members'],
|
||||
) {
|
||||
if (a.length !== b.length) return true
|
||||
const aDids = new Set(a.map(m => m.did))
|
||||
return b.some(m => !aDids.has(m.did))
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ConvoState | null>(null)
|
||||
ChatContext.displayName = 'ChatContext'
|
||||
|
||||
@@ -107,11 +117,11 @@ export function ConvoProvider({
|
||||
switch (event.type) {
|
||||
case 'invalidate-block-state': {
|
||||
for (const did of event.accountDids) {
|
||||
queryClient.invalidateQueries({
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: createProfileQueryKey(did),
|
||||
})
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ListConvosQueryKeyRoot],
|
||||
})
|
||||
}
|
||||
@@ -127,17 +137,35 @@ export function ConvoProvider({
|
||||
const data = event.query.state.data as
|
||||
| ChatBskyConvoDefs.ConvoView
|
||||
| undefined
|
||||
if (data && convo.convo && data.muted !== convo.convo.muted) {
|
||||
if (data && convo.convo && data.muted !== convo.convo.view.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.convo?.kind === 'group'
|
||||
) {
|
||||
convo.updateGroupName(data.kind.name)
|
||||
if (data.kind.name !== convo.convo.details.name) {
|
||||
convo.updateGroupName(data.kind.name)
|
||||
}
|
||||
if (data.kind.joinLink !== convo.convo.details.joinLink) {
|
||||
convo.updateJoinLink(data.kind.joinLink)
|
||||
}
|
||||
if (data.kind.lockStatus !== convo.convo.details.lockStatus) {
|
||||
convo.updateLockStatus(data.kind.lockStatus)
|
||||
}
|
||||
}
|
||||
if (
|
||||
data &&
|
||||
ChatBskyConvoDefs.isGroupConvo(data.kind) &&
|
||||
convo.convo?.kind === 'group' &&
|
||||
(membersChanged(data.members, convo.convo.members) ||
|
||||
data.kind.memberCount !== convo.convo.details.memberCount)
|
||||
) {
|
||||
convo.updateGroupMembers(
|
||||
data.members as GroupConvoMember[],
|
||||
data.kind.memberCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@atproto/api'
|
||||
|
||||
import {type MessagesEventBus} from '#/state/messages/events/agent'
|
||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
||||
|
||||
export type ConvoParams = {
|
||||
convoId: string
|
||||
@@ -58,34 +59,20 @@ export enum ConvoDispatchEvent {
|
||||
}
|
||||
|
||||
export type ConvoDispatch =
|
||||
| {
|
||||
event: ConvoDispatchEvent.Init
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Ready
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Resume
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Background
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Suspend
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Error
|
||||
payload: ConvoError
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Disable
|
||||
}
|
||||
| {event: ConvoDispatchEvent.Init}
|
||||
| {event: ConvoDispatchEvent.Ready}
|
||||
| {event: ConvoDispatchEvent.Resume}
|
||||
| {event: ConvoDispatchEvent.Background}
|
||||
| {event: ConvoDispatchEvent.Suspend}
|
||||
| {event: ConvoDispatchEvent.Error; payload: ConvoError}
|
||||
| {event: ConvoDispatchEvent.Disable}
|
||||
|
||||
export type ConvoItem =
|
||||
| {
|
||||
type: 'message'
|
||||
key: string
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
nextMessage:
|
||||
| ChatBskyConvoDefs.MessageView
|
||||
| ChatBskyConvoDefs.DeletedMessageView
|
||||
@@ -99,6 +86,7 @@ export type ConvoItem =
|
||||
type: 'pending-message'
|
||||
key: string
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
nextMessage:
|
||||
| ChatBskyConvoDefs.MessageView
|
||||
| ChatBskyConvoDefs.DeletedMessageView
|
||||
@@ -117,6 +105,7 @@ export type ConvoItem =
|
||||
type: 'deleted-message'
|
||||
key: string
|
||||
message: ChatBskyConvoDefs.DeletedMessageView
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
nextMessage:
|
||||
| ChatBskyConvoDefs.MessageView
|
||||
| ChatBskyConvoDefs.DeletedMessageView
|
||||
@@ -130,7 +119,7 @@ export type ConvoItem =
|
||||
type: 'system-message'
|
||||
key: string
|
||||
message: ChatBskyConvoDefs.SystemMessageView
|
||||
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
}
|
||||
| {
|
||||
type: 'error'
|
||||
@@ -150,17 +139,12 @@ type FetchMessageHistory = () => Promise<void>
|
||||
type MarkConvoAccepted = () => void
|
||||
type AddReaction = (messageId: string, reaction: string) => Promise<void>
|
||||
type RemoveReaction = (messageId: string, reaction: string) => Promise<void>
|
||||
type IsGroup = () => boolean | undefined
|
||||
type GetGroupInfo = () => ChatBskyConvoDefs.GroupConvo | undefined
|
||||
type GetPrimaryMember = () => ChatBskyActorDefs.ProfileViewBasic | undefined
|
||||
|
||||
export type ConvoStateUninitialized = {
|
||||
status: ConvoStatus.Uninitialized
|
||||
items: []
|
||||
convo: ChatBskyConvoDefs.ConvoView | undefined
|
||||
convo: ConvoWithDetails | undefined
|
||||
error: undefined
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
|
||||
isFetchingHistory: false
|
||||
hasAllHistory: boolean
|
||||
deleteMessage: undefined
|
||||
@@ -169,17 +153,12 @@ export type ConvoStateUninitialized = {
|
||||
markConvoAccepted: undefined
|
||||
addReaction: undefined
|
||||
removeReaction: undefined
|
||||
isGroup: IsGroup
|
||||
getGroupInfo: GetGroupInfo
|
||||
getPrimaryMember: GetPrimaryMember
|
||||
}
|
||||
export type ConvoStateInitializing = {
|
||||
status: ConvoStatus.Initializing
|
||||
items: []
|
||||
convo: ChatBskyConvoDefs.ConvoView | undefined
|
||||
convo: ConvoWithDetails | undefined
|
||||
error: undefined
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
|
||||
isFetchingHistory: boolean
|
||||
hasAllHistory: boolean
|
||||
deleteMessage: undefined
|
||||
@@ -188,17 +167,12 @@ export type ConvoStateInitializing = {
|
||||
markConvoAccepted: undefined
|
||||
addReaction: undefined
|
||||
removeReaction: undefined
|
||||
isGroup: IsGroup
|
||||
getGroupInfo: GetGroupInfo
|
||||
getPrimaryMember: GetPrimaryMember
|
||||
}
|
||||
export type ConvoStateReady = {
|
||||
status: ConvoStatus.Ready
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
convo: ConvoWithDetails
|
||||
error: undefined
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
isFetchingHistory: boolean
|
||||
hasAllHistory: boolean
|
||||
deleteMessage: DeleteMessage
|
||||
@@ -207,17 +181,12 @@ export type ConvoStateReady = {
|
||||
markConvoAccepted: MarkConvoAccepted
|
||||
addReaction: AddReaction
|
||||
removeReaction: RemoveReaction
|
||||
isGroup: IsGroup
|
||||
getGroupInfo: GetGroupInfo
|
||||
getPrimaryMember: GetPrimaryMember
|
||||
}
|
||||
export type ConvoStateBackgrounded = {
|
||||
status: ConvoStatus.Backgrounded
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
convo: ConvoWithDetails
|
||||
error: undefined
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
isFetchingHistory: boolean
|
||||
hasAllHistory: boolean
|
||||
deleteMessage: DeleteMessage
|
||||
@@ -226,17 +195,12 @@ export type ConvoStateBackgrounded = {
|
||||
markConvoAccepted: MarkConvoAccepted
|
||||
addReaction: AddReaction
|
||||
removeReaction: RemoveReaction
|
||||
isGroup: IsGroup
|
||||
getGroupInfo: GetGroupInfo
|
||||
getPrimaryMember: GetPrimaryMember
|
||||
}
|
||||
export type ConvoStateSuspended = {
|
||||
status: ConvoStatus.Suspended
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
convo: ConvoWithDetails
|
||||
error: undefined
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
isFetchingHistory: boolean
|
||||
hasAllHistory: boolean
|
||||
deleteMessage: DeleteMessage
|
||||
@@ -245,17 +209,12 @@ export type ConvoStateSuspended = {
|
||||
markConvoAccepted: MarkConvoAccepted
|
||||
addReaction: AddReaction
|
||||
removeReaction: RemoveReaction
|
||||
isGroup: IsGroup
|
||||
getGroupInfo: GetGroupInfo
|
||||
getPrimaryMember: GetPrimaryMember
|
||||
}
|
||||
export type ConvoStateError = {
|
||||
status: ConvoStatus.Error
|
||||
items: []
|
||||
convo: undefined
|
||||
error: ConvoError
|
||||
sender: undefined
|
||||
recipients: undefined
|
||||
isFetchingHistory: false
|
||||
hasAllHistory: false
|
||||
deleteMessage: undefined
|
||||
@@ -264,17 +223,12 @@ export type ConvoStateError = {
|
||||
markConvoAccepted: undefined
|
||||
addReaction: undefined
|
||||
removeReaction: undefined
|
||||
isGroup: undefined
|
||||
getGroupInfo: undefined
|
||||
getPrimaryMember: undefined
|
||||
}
|
||||
export type ConvoStateDisabled = {
|
||||
status: ConvoStatus.Disabled
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
convo: ConvoWithDetails
|
||||
error: undefined
|
||||
sender: ChatBskyActorDefs.ProfileViewBasic
|
||||
recipients: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
isFetchingHistory: boolean
|
||||
hasAllHistory: boolean
|
||||
deleteMessage: DeleteMessage
|
||||
@@ -283,9 +237,6 @@ export type ConvoStateDisabled = {
|
||||
markConvoAccepted: MarkConvoAccepted
|
||||
addReaction: AddReaction
|
||||
removeReaction: RemoveReaction
|
||||
isGroup: IsGroup
|
||||
getGroupInfo: GetGroupInfo
|
||||
getPrimaryMember: GetPrimaryMember
|
||||
}
|
||||
export type ConvoState =
|
||||
| ConvoStateUninitialized
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
type ChatBskyActorDefs,
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyConvoListConvos,
|
||||
type ChatBskyGroupAddMembers,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {RQKEY as CONVO_KEY} from './conversation'
|
||||
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
|
||||
import {listConvoMembersQueryKey} from './list-convo-members'
|
||||
|
||||
export function useAddGroupMembers(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupAddMembers.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
const {currentAccount} = useSession()
|
||||
const {data: myProfile} = useProfileQuery({did: currentAccount?.did})
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
members,
|
||||
}: {
|
||||
members: string[]
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.addMembers(
|
||||
{convoId, members},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: ({profiles}) => {
|
||||
if (!convoId) return
|
||||
|
||||
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
)
|
||||
const prevListEntries = queryClient.getQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]})
|
||||
const prevMemberList = queryClient.getQueryData<
|
||||
ChatBskyActorDefs.ProfileViewBasic[]
|
||||
>(listConvoMembersQueryKey(convoId))
|
||||
|
||||
const addedBy: ChatBskyActorDefs.ProfileViewBasic | undefined = myProfile
|
||||
? {
|
||||
...myProfile,
|
||||
$type: 'chat.bsky.actor.defs#profileViewBasic',
|
||||
}
|
||||
: undefined
|
||||
|
||||
const optimisticMembers: ChatBskyActorDefs.ProfileViewBasic[] =
|
||||
profiles.map(profile => ({
|
||||
...profile,
|
||||
$type: 'chat.bsky.actor.defs#profileViewBasic',
|
||||
kind: {
|
||||
$type: 'chat.bsky.actor.defs#groupConvoMember',
|
||||
role: 'standard',
|
||||
addedBy,
|
||||
},
|
||||
}))
|
||||
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return prev
|
||||
return {
|
||||
...prev,
|
||||
members: [...prev.members, ...optimisticMembers],
|
||||
kind: {
|
||||
...prev.kind,
|
||||
memberCount: prev.kind.memberCount + optimisticMembers.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
queryClient.setQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]}, prev => {
|
||||
if (!prev?.pages) return
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.map(page => ({
|
||||
...page,
|
||||
convos: page.convos.map(convo => {
|
||||
if (convo.id !== convoId) return convo
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo
|
||||
return {
|
||||
...convo,
|
||||
members: [...convo.members, ...optimisticMembers],
|
||||
kind: {
|
||||
...convo.kind,
|
||||
memberCount:
|
||||
convo.kind.memberCount + optimisticMembers.length,
|
||||
},
|
||||
}
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
return [...prev, ...optimisticMembers]
|
||||
},
|
||||
)
|
||||
|
||||
return {prevConvo, prevListEntries, prevMemberList}
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
data.convo,
|
||||
)
|
||||
|
||||
queryClient.setQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]}, prev => {
|
||||
if (!prev?.pages) return
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.map(page => ({
|
||||
...page,
|
||||
convos: page.convos.map(convo =>
|
||||
convo.id === convoId ? data.convo : convo,
|
||||
),
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (context?.prevConvo && convoId) {
|
||||
queryClient.setQueryData(CONVO_KEY(convoId), context.prevConvo)
|
||||
}
|
||||
if (context?.prevListEntries) {
|
||||
for (const [key, data] of context.prevListEntries) {
|
||||
queryClient.setQueryData(key, data)
|
||||
}
|
||||
}
|
||||
if (context?.prevMemberList && convoId) {
|
||||
queryClient.setQueryData(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
context.prevMemberList,
|
||||
)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
RQKEY_ROOT as LIST_CONVOS_KEY,
|
||||
} from './list-conversations'
|
||||
|
||||
const RQKEY_ROOT = 'convo'
|
||||
export const RQKEY_ROOT = 'convo'
|
||||
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
|
||||
|
||||
export function useConvoQuery({convoId}: {convoId: string}) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyGroupCreateJoinLink,
|
||||
type ChatBskyGroupDefs,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useCreateJoinLink(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupCreateJoinLink.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
joinRule,
|
||||
requireApproval,
|
||||
}: {
|
||||
joinRule: ChatBskyGroupDefs.JoinRule
|
||||
requireApproval: boolean
|
||||
}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.createJoinLink(
|
||||
{convoId, joinRule, requireApproval},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: ({joinRule, requireApproval}) => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
joinLink: {
|
||||
$type: 'chat.bsky.group.defs#joinLinkView',
|
||||
code: '',
|
||||
enabledStatus: 'enabled',
|
||||
joinRule,
|
||||
requireApproval,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, joinLink: data.joinLink},
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyGroupDisableJoinLink,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useDisableJoinLink(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupDisableJoinLink.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.disableJoinLink(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: () => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
joinLink: {...prev.kind.joinLink, enabledStatus: 'disabled'},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, joinLink: data.joinLink},
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {ChatBskyConvoDefs, type ChatBskyGroupEditGroup} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useEditGroupChatName(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupEditGroup.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({name: groupName}: {name: string}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.editGroup(
|
||||
{convoId, name: groupName},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: ({name: groupName}) => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, name: groupName},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyGroupDefs,
|
||||
type ChatBskyGroupEditJoinLink,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useEditJoinLink(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupEditJoinLink.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
joinRule,
|
||||
requireApproval,
|
||||
}: {
|
||||
joinRule: ChatBskyGroupDefs.JoinRule
|
||||
requireApproval: boolean
|
||||
}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.editJoinLink(
|
||||
{convoId, joinRule, requireApproval},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: ({joinRule, requireApproval}) => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
joinLink: {...prev.kind.joinLink, joinRule, requireApproval},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, joinLink: data.joinLink},
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import {ChatBskyConvoDefs, type ChatBskyGroupEnableJoinLink} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useEnableJoinLink(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupEnableJoinLink.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.enableJoinLink(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: () => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
joinLink: {...prev.kind.joinLink, enabledStatus: 'enabled'},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, joinLink: data.joinLink},
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,10 @@ import {STALE} from '..'
|
||||
const RQKEY_ROOT = 'convo-availability'
|
||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||
|
||||
export function useGetConvoAvailabilityQuery(did: string) {
|
||||
export function useGetConvoAvailabilityQuery(
|
||||
did: string,
|
||||
{enabled = true}: {enabled?: boolean} = {},
|
||||
) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
@@ -21,5 +24,6 @@ export function useGetConvoAvailabilityQuery(did: string) {
|
||||
return data
|
||||
},
|
||||
staleTime: STALE.INFINITY,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export function useLeaveConvo(
|
||||
return {prevPages}
|
||||
},
|
||||
onSuccess: data => {
|
||||
queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (error, _, context) => {
|
||||
@@ -89,7 +89,7 @@ export function useLeaveConvo(
|
||||
}
|
||||
},
|
||||
)
|
||||
queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
onError?.(error)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -105,8 +105,8 @@ export function ListConvosProviderInner({
|
||||
|
||||
const debouncedRefetch = useMemo(() => {
|
||||
const refetchAndInvalidate = () => {
|
||||
refetch()
|
||||
queryClient.invalidateQueries({queryKey: [RQKEY_ROOT]})
|
||||
void refetch()
|
||||
void queryClient.invalidateQueries({queryKey: [RQKEY_ROOT]})
|
||||
}
|
||||
return throttle(refetchAndInvalidate, 500, {
|
||||
leading: true,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import {useEffect} from 'react'
|
||||
import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {useMessagesEventBus} from '#/state/messages/events'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const RQKEY_ROOT = 'listConvoMembers'
|
||||
export const listConvoMembersQueryKey = (convoId: string) =>
|
||||
createQueryKey(RQKEY_ROOT, {convoId})
|
||||
|
||||
// group chat size is 50, so should fetch the whole list in one go
|
||||
const LIMIT = 50
|
||||
|
||||
export function useListConvoMembersQuery({
|
||||
convoId,
|
||||
placeholderData,
|
||||
}: {
|
||||
convoId: string
|
||||
placeholderData?: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const messagesBus = useMessagesEventBus()
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = messagesBus.on(
|
||||
ev => {
|
||||
if (ev.type !== 'logs') return
|
||||
|
||||
function mutateList(
|
||||
fn: (
|
||||
update: ChatBskyActorDefs.ProfileViewBasic[],
|
||||
) => ChatBskyActorDefs.ProfileViewBasic[],
|
||||
) {
|
||||
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
old => {
|
||||
if (!old) return // query doesn't exist yet, skip
|
||||
return fn(old)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for (const log of ev.logs) {
|
||||
if (ChatBskyConvoDefs.isLogAddMember(log)) {
|
||||
const data = log.message.data
|
||||
if (
|
||||
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataAddMember>(
|
||||
data,
|
||||
ChatBskyConvoDefs.isSystemMessageDataAddMember,
|
||||
)
|
||||
) {
|
||||
const newMember = log.relatedProfiles.find(
|
||||
r => r.did === data.member.did,
|
||||
)
|
||||
if (newMember) {
|
||||
mutateList(list => list.concat(newMember))
|
||||
}
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isLogRemoveMember(log)) {
|
||||
const data = log.message.data
|
||||
if (
|
||||
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataRemoveMember>(
|
||||
data,
|
||||
ChatBskyConvoDefs.isSystemMessageDataRemoveMember,
|
||||
)
|
||||
) {
|
||||
mutateList(list => list.filter(m => m.did !== data.member.did))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{convoId},
|
||||
)
|
||||
return () => unsub()
|
||||
}, [convoId, messagesBus, queryClient])
|
||||
|
||||
return useQuery({
|
||||
queryKey: listConvoMembersQueryKey(convoId),
|
||||
queryFn: async () => {
|
||||
const members = []
|
||||
let cursor
|
||||
|
||||
do {
|
||||
const {data} = await agent.chat.bsky.convo.getConvoMembers(
|
||||
{convoId, cursor, limit: LIMIT},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
members.push(...data.members)
|
||||
cursor = data.cursor
|
||||
} while (cursor)
|
||||
|
||||
return members
|
||||
},
|
||||
staleTime: STALE.MINUTES.THIRTY,
|
||||
placeholderData,
|
||||
})
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
): Generator<ChatBskyActorDefs.ProfileViewBasic, void> {
|
||||
const queryDatas = queryClient.getQueriesData<
|
||||
ChatBskyActorDefs.ProfileViewBasic[]
|
||||
>({
|
||||
queryKey: [RQKEY_ROOT],
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData) continue
|
||||
for (const member of queryData) {
|
||||
if (member.did === did) {
|
||||
yield member
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {ChatBskyConvoDefs, type ChatBskyConvoLockConvo} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useLockConvo(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyConvoLockConvo.OutputSchema) => void
|
||||
onError?: (error: Error, variables: {lock: boolean}) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({lock}: {lock: boolean}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
if (lock) {
|
||||
const {data} = await agent.chat.bsky.convo.lockConvo(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
} else {
|
||||
const {data} = await agent.chat.bsky.convo.unlockConvo(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
}
|
||||
},
|
||||
onMutate: ({lock}) => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
lockStatus: lock ? 'locked' : 'unlocked',
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, variables, context) => {
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e, variables)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
import {
|
||||
type ChatBskyConvoDefs,
|
||||
type ChatBskyConvoListConvos,
|
||||
type ChatBskyConvoMuteConvo,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import {type ChatBskyConvoMuteConvo} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {RQKEY as CONVO_KEY} from './conversation'
|
||||
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useMuteConvo(
|
||||
convoId: string | undefined,
|
||||
@@ -46,59 +40,17 @@ export function useMuteConvo(
|
||||
},
|
||||
onMutate: ({mute}) => {
|
||||
if (!convoId) return
|
||||
|
||||
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
)
|
||||
const prevListEntries = queryClient.getQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]})
|
||||
|
||||
// Update for a single chat thread
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
return {
|
||||
...prev,
|
||||
muted: mute,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Update for the chat list
|
||||
queryClient.setQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]}, prev => {
|
||||
if (!prev?.pages) return
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.map(page => ({
|
||||
...page,
|
||||
convos: page.convos.map(convo => {
|
||||
if (convo.id !== convoId) return convo
|
||||
return {
|
||||
...convo,
|
||||
muted: mute,
|
||||
}
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
return {prevConvo, prevListEntries}
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => ({
|
||||
...prev,
|
||||
muted: mute,
|
||||
}))
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
if (context?.prevConvo && convoId) {
|
||||
queryClient.setQueryData(CONVO_KEY(convoId), context.prevConvo)
|
||||
}
|
||||
if (context?.prevListEntries) {
|
||||
for (const [key, data] of context.prevListEntries) {
|
||||
queryClient.setQueryData(key, data)
|
||||
}
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
|
||||
+30
-21
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyActorDefs,
|
||||
type ChatBskyConvoDefs,
|
||||
type ChatBskyConvoListConvos,
|
||||
type ChatBskyGroupEditGroup,
|
||||
type ChatBskyGroupRemoveMembers,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
@@ -14,14 +15,15 @@ import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {RQKEY as CONVO_KEY} from './conversation'
|
||||
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
|
||||
import {listConvoMembersQueryKey} from './list-convo-members'
|
||||
|
||||
export function useEditGroupName(
|
||||
export function useRemoveFromGroupChat(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupEditGroup.OutputSchema) => void
|
||||
onSuccess?: (data: ChatBskyGroupRemoveMembers.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
@@ -29,15 +31,15 @@ export function useEditGroupName(
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({name: groupName}: {name: string}) => {
|
||||
mutationFn: async ({members}: {members: string[]}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.editGroup(
|
||||
{convoId, name: groupName},
|
||||
const {data} = await agent.chat.bsky.group.removeMembers(
|
||||
{convoId, members},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: ({name: groupName}) => {
|
||||
onMutate: ({members}) => {
|
||||
if (!convoId) return
|
||||
|
||||
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
@@ -46,24 +48,21 @@ export function useEditGroupName(
|
||||
const prevListEntries = queryClient.getQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]})
|
||||
const prevMemberList = queryClient.getQueryData<
|
||||
ChatBskyActorDefs.ProfileViewBasic[]
|
||||
>(listConvoMembersQueryKey(convoId))
|
||||
|
||||
// Update for a single chat thread
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return prev
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
name: groupName,
|
||||
},
|
||||
members: prev.members.filter(m => !members.includes(m.did)),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Update for the chat list
|
||||
queryClient.setQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]}, prev => {
|
||||
@@ -74,20 +73,24 @@ export function useEditGroupName(
|
||||
...page,
|
||||
convos: page.convos.map(convo => {
|
||||
if (convo.id !== convoId) return convo
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo
|
||||
return {
|
||||
...convo,
|
||||
kind: {
|
||||
...convo.kind,
|
||||
name: groupName,
|
||||
},
|
||||
members: convo.members.filter(m => !members.includes(m.did)),
|
||||
}
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
return {prevConvo, prevListEntries}
|
||||
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
return prev.filter(m => !members.includes(m.did))
|
||||
},
|
||||
)
|
||||
|
||||
return {prevConvo, prevListEntries, prevMemberList}
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
@@ -102,6 +105,12 @@ export function useEditGroupName(
|
||||
queryClient.setQueryData(key, data)
|
||||
}
|
||||
}
|
||||
if (context?.prevMemberList && convoId) {
|
||||
queryClient.setQueryData(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
context.prevMemberList,
|
||||
)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
type ChatBskyConvoDefs,
|
||||
type ChatBskyConvoListConvos,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
type QueryClient,
|
||||
type QueryKey,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {RQKEY as CONVO_KEY} from '../conversation'
|
||||
import {RQKEY_ROOT as CONVO_LIST_KEY} from '../list-conversations'
|
||||
|
||||
type ConvoUpdater = (
|
||||
prev: ChatBskyConvoDefs.ConvoView,
|
||||
) => ChatBskyConvoDefs.ConvoView | undefined
|
||||
|
||||
export type ConvoCacheSnapshot = {
|
||||
prevConvo: ChatBskyConvoDefs.ConvoView | undefined
|
||||
prevListEntries: Array<
|
||||
[QueryKey, InfiniteData<ChatBskyConvoListConvos.OutputSchema> | undefined]
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an optimistic update to a convo across both the single-convo and
|
||||
* convo-list caches. The updater receives the current ConvoView and returns
|
||||
* the next one - return undefined to bail out (e.g. when the convo's kind
|
||||
* doesn't match what the mutation requires). Returns a snapshot that can be
|
||||
* passed to `rollbackConvoOptimistic`.
|
||||
*/
|
||||
export function updateConvoOptimistic(
|
||||
queryClient: QueryClient,
|
||||
convoId: string,
|
||||
updater: ConvoUpdater,
|
||||
): ConvoCacheSnapshot {
|
||||
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
)
|
||||
const prevListEntries = queryClient.getQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]})
|
||||
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
const next = updater(prev)
|
||||
return next ?? prev
|
||||
},
|
||||
)
|
||||
|
||||
queryClient.setQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]}, prev => {
|
||||
if (!prev?.pages) return
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.map(page => ({
|
||||
...page,
|
||||
convos: page.convos.map(convo => {
|
||||
if (convo.id !== convoId) return convo
|
||||
const next = updater(convo)
|
||||
return next ?? convo
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
return {prevConvo, prevListEntries}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the caches to the state captured by `updateConvoOptimistic`.
|
||||
*/
|
||||
export function rollbackConvoOptimistic(
|
||||
queryClient: QueryClient,
|
||||
convoId: string,
|
||||
snapshot: ConvoCacheSnapshot,
|
||||
) {
|
||||
if (snapshot.prevConvo) {
|
||||
queryClient.setQueryData(CONVO_KEY(convoId), snapshot.prevConvo)
|
||||
}
|
||||
for (const [key, data] of snapshot.prevListEntries) {
|
||||
queryClient.setQueryData(key, data)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user