diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx index 3a923133f1..9ab4521f2f 100644 --- a/src/components/dms/MessageContextMenu.tsx +++ b/src/components/dms/MessageContextMenu.tsx @@ -183,7 +183,7 @@ export let MessageContextMenu = ({ control={reportControl} subject={{ view: 'message', - convoId: convo.convo.id, + convoId: convo.convo.view.id, message, }} onAfterSubmit={() => { @@ -197,7 +197,7 @@ export let MessageContextMenu = ({ control={blockOrDeleteControl} currentScreen="conversation" params={{ - convoId: convo.convo.id, + convoId: convo.convo.view.id, message, }} /> diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts index 5c159a72f0..5140ba64ca 100644 --- a/src/components/dms/util.ts +++ b/src/components/dms/util.ts @@ -68,13 +68,13 @@ export type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & { export type ConvoWithDetails = {view: ChatBskyConvoDefs.ConvoView} & ( | { kind: 'group' - details: ChatBskyConvoDefs.GroupConvo + details: $Typed primaryMember: GroupConvoMember // the owner members: Array } | { kind: 'direct' - details: ChatBskyConvoDefs.DirectConvo + details: $Typed primaryMember: DirectConvoMember // the other user members: Array } diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index 954e90e55a..b664d7e1e3 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -23,7 +23,7 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {AvatarBubbles} from '#/components/AvatarBubbles' import {Button, type ButtonColor, ButtonIcon} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' +import {type ConvoWithDetails} from '#/components/dms/util' import {Error} from '#/components/Error' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft' import { @@ -110,8 +110,6 @@ function SettingsInner({convoId}: {convoId: string}) { const {currentAccount} = useSession() const convo = convoState.convo - ? parseConvoView(convoState.convo, currentAccount?.did) - : null const primaryMember = convo?.primaryMember const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did diff --git a/src/screens/Messages/components/ChatStatusInfo.tsx b/src/screens/Messages/components/ChatStatusInfo.tsx index ca2b1f3685..dfdbfe8fb9 100644 --- a/src/screens/Messages/components/ChatStatusInfo.tsx +++ b/src/screens/Messages/components/ChatStatusInfo.tsx @@ -44,7 +44,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) { {otherUser && ( member.did === item.message.sender.did, )} - isGroupChat={convoState.isGroup()} + isGroupChat={convoState.convo.kind === 'group'} /> ) } else if (item.type === 'deleted-message') { @@ -446,8 +446,9 @@ export function MessagesList({ ListHeaderComponent={ <> - {convoState.isGroup() && convoState.hasAllHistory ? ( - + {convoState.convo?.kind === 'group' && + convoState.hasAllHistory ? ( + ) : null} } @@ -575,7 +576,7 @@ function getFooterState( } } - if (convoState.convo.status === 'request' && !hasAcceptOverride) { + if (convoState.convo.view.status === 'request' && !hasAcceptOverride) { return 'request' } diff --git a/src/screens/Messages/components/MessagesListInfoPanel.tsx b/src/screens/Messages/components/MessagesListInfoPanel.tsx index 02064d111c..f22b80dbc4 100644 --- a/src/screens/Messages/components/MessagesListInfoPanel.tsx +++ b/src/screens/Messages/components/MessagesListInfoPanel.tsx @@ -2,7 +2,6 @@ import {View} from 'react-native' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {logger} from '#/logger' -import {type ConvoState} from '#/state/messages/convo/types' import {useAddGroupMembers} from '#/state/queries/messages/add-group-members' import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' @@ -10,14 +9,18 @@ import {AvatarBubbles} from '#/components/AvatarBubbles' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {AddMembersFlow} from '#/components/dms/AddMembersFlow' -import {parseConvoView} from '#/components/dms/util' +import {type ConvoWithDetails} from '#/components/dms/util' import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {InviteLinkDialog} from './InviteLinkDialog' -export function MessagesListInfoPanel({convoState}: {convoState: ConvoState}) { +export function MessagesListInfoPanel({ + convo, +}: { + convo: Extract +}) { const t = useTheme() const {t: l} = useLingui() @@ -26,33 +29,25 @@ export function MessagesListInfoPanel({convoState}: {convoState: ConvoState}) { const {currentAccount} = useSession() - const convoId = convoState.convo?.id + const convoId = convo.view.id const {mutate: addGroupMembers} = useAddGroupMembers(convoId, { onError: e => { logger.error('Failed to add group chat members', {message: e}) Toast.show(l`Failed to add members`, {type: 'error'}) }, }) - const convo = convoState.convo - ? parseConvoView(convoState.convo, currentAccount?.did) - : null - const groupConvo = convo?.kind === 'group' ? convo : null + // TODO Enable this once the feature is working end-to-end. -dsb // const joinLink = groupConvo?.details.joinLink const isJoinLinkEnabled = false // (isOwner && groupConvo) || // (!isOwner && groupConvo && joinLink?.enabledStatus === 'enabled') - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did + const isOwner = convo?.primaryMember.did === currentAccount?.did // TODO Get this from @api/atproto -dsb const isLinkEnabled = false - const groupName = convoState.getGroupInfo?.()?.name - - const members = (convoState?.convo?.members ?? []).filter( + const members = (convo?.members ?? []).filter( profile => profile.did !== currentAccount?.did, ) @@ -87,9 +82,9 @@ export function MessagesListInfoPanel({convoState}: {convoState: ConvoState}) { <> - {groupName ? ( + {convo.details.name ? ( - {groupName} + {convo.details.name} ) : null} {names ? ( @@ -144,13 +139,11 @@ export function MessagesListInfoPanel({convoState}: {convoState: ConvoState}) { ) : null} - {groupConvo ? ( - - ) : null} + () convoId: string - convo: ChatBskyConvoDefs.ConvoView | undefined + convo: ConvoWithDetails | undefined sender: ChatBskyActorDefs.ProfileViewBasic | undefined recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined snapshot: ConvoState | undefined @@ -131,6 +135,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) @@ -142,9 +147,6 @@ 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) @@ -175,6 +177,15 @@ export class Convo { } private generateSnapshot(): ConvoState { + const methods = { + deleteMessage: this.deleteMessage, + sendMessage: this.sendMessage, + fetchMessageHistory: this.fetchMessageHistory, + markConvoAccepted: this.markConvoAccepted, + addReaction: this.addReaction, + removeReaction: this.removeReaction, + } + switch (this.status) { case ConvoStatus.Initializing: { return { @@ -193,14 +204,50 @@ export class Convo { markConvoAccepted: undefined, addReaction: undefined, removeReaction: undefined, - isGroup: this.isGroup, - getGroupInfo: this.getGroupInfo, - getPrimaryMember: this.getPrimaryMember, } } - case ConvoStatus.Disabled: - case ConvoStatus.Suspended: - case ConvoStatus.Backgrounded: + case ConvoStatus.Disabled: { + 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, + ...methods, + } + } + case ConvoStatus.Suspended: { + 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, + ...methods, + } + } + case ConvoStatus.Backgrounded: { + 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, + ...methods, + } + } case ConvoStatus.Ready: { return { status: this.status, @@ -212,15 +259,7 @@ export class Convo { 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, + ...methods, } } case ConvoStatus.Error: { @@ -239,9 +278,6 @@ export class Convo { markConvoAccepted: undefined, addReaction: undefined, removeReaction: undefined, - isGroup: undefined, - getGroupInfo: undefined, - getPrimaryMember: undefined, } } default: { @@ -261,9 +297,6 @@ export class Convo { markConvoAccepted: undefined, addReaction: undefined, removeReaction: undefined, - isGroup: this.isGroup, - getGroupInfo: this.getGroupInfo, - getPrimaryMember: this.getPrimaryMember, } } } @@ -501,6 +534,18 @@ export class Convo { } } + private setConvo(convo: ChatBskyConvoDefs.ConvoView) { + this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo + } + + private updateConvo(convo: Partial) { + if (this.convo) { + this.convo = + parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ?? + this.convo + } + } + /** * Initialises the convo with placeholder data, if provided. We still refetch it before rendering the convo, * but this allows us to render the convo header immediately. @@ -508,7 +553,7 @@ export class Convo { private setupPlaceholderData( data: NonNullable, ) { - this.convo = data.convo + this.setConvo(data.convo) this.sender = data.convo.members.find(m => m.did === this.senderUserDid) this.recipients = data.convo.members.filter( m => m.did !== this.senderUserDid, @@ -519,7 +564,7 @@ export class Convo { try { const {convo, sender, recipients} = await this.fetchConvo() - this.convo = convo + this.setConvo(convo) this.sender = sender this.recipients = recipients @@ -617,7 +662,7 @@ export class Convo { this.pendingFetchConvo = (async () => { try { const response = await networkRetry(2, () => { - return this.agent.api.chat.bsky.convo.getConvo( + return this.agent.chat.bsky.convo.getConvo( { convoId: this.convoId, }, @@ -644,7 +689,7 @@ export class Convo { try { const {convo, sender, recipients} = await this.fetchConvo() // throw new Error('UNCOMMENT TO TEST REFRESH FAILURE') - this.convo = convo || this.convo + this.setConvo(convo) this.sender = sender || this.sender this.recipients = recipients || this.recipients } catch (err) { @@ -657,11 +702,7 @@ export class Convo { } } - private fetchMessageHistoryError: - | { - retry: () => void - } - | undefined + private fetchMessageHistoryError: {retry: () => void} | undefined async fetchMessageHistory() { logger.debug('fetch message history', {}) @@ -910,11 +951,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() @@ -924,72 +964,60 @@ 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( - 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: ChatBskyActorDefs.ProfileViewBasic[]) { - if (this.convo) { - this.convo = { - ...this.convo, - members, - } - this.sender = members.find(m => m.did === this.senderUserDid) - this.recipients = members.filter(m => m.did !== this.senderUserDid) + updateGroupMembers(members: GroupConvoMember[]) { + if (this.convo?.kind !== 'group') { + throw new Error('updateGroupMembers can only be called on group convo') } + + this.updateConvo({ + members, + }) + this.commit() } updateJoinLink(joinLink: ChatBskyGroupDefs.JoinLinkView | undefined) { - if ( - this.convo && - bsky.dangerousIsType( - this.convo.kind, - ChatBskyConvoDefs.isGroupConvo, - ) - ) { - this.convo = { - ...this.convo, - kind: { - ...this.convo.kind, - joinLink, - }, - } + 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() } @@ -1014,7 +1042,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, @@ -1057,7 +1085,7 @@ export class Convo { this.emitter.emit('event', { type: 'invalidate-block-state', accountDids: [ - this.sender!.did, + this.senderUserDid, ...this.recipients!.map(r => r.did), ], }) @@ -1109,7 +1137,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, @@ -1151,7 +1179,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, @@ -1268,7 +1296,7 @@ export class Convo { */ sender: { $type: 'chat.bsky.convo.defs#messageViewSender', - did: this.sender!.did, + did: this.senderUserDid, }, }, nextMessage: null, @@ -1482,46 +1510,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( - 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( - 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) - } - } } diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index b57f625739..ca2bbdda0f 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -29,6 +29,7 @@ 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' @@ -136,19 +137,18 @@ 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) + convo.convo?.kind === 'group' ) { - if (data.kind.name !== convo.convo.kind.name) { + if (data.kind.name !== convo.convo.details.name) { convo.updateGroupName(data.kind.name) } - if (data.kind.joinLink !== convo.convo.kind.joinLink) { + if (data.kind.joinLink !== convo.convo.details.joinLink) { convo.updateJoinLink(data.kind.joinLink) } } @@ -157,7 +157,7 @@ export function ConvoProvider({ convo.convo && membersChanged(data.members, convo.convo.members) ) { - convo.updateGroupMembers(data.members) + convo.updateGroupMembers(data.members as GroupConvoMember[]) } } }) diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts index dad3b7f82a..4dbd72700d 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -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 @@ -150,14 +151,11 @@ type FetchMessageHistory = () => Promise type MarkConvoAccepted = () => void type AddReaction = (messageId: string, reaction: string) => Promise type RemoveReaction = (messageId: string, reaction: string) => Promise -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 @@ -169,14 +167,11 @@ 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 @@ -188,14 +183,11 @@ 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[] @@ -207,14 +199,11 @@ 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[] @@ -226,14 +215,11 @@ 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[] @@ -245,9 +231,6 @@ export type ConvoStateSuspended = { markConvoAccepted: MarkConvoAccepted addReaction: AddReaction removeReaction: RemoveReaction - isGroup: IsGroup - getGroupInfo: GetGroupInfo - getPrimaryMember: GetPrimaryMember } export type ConvoStateError = { status: ConvoStatus.Error @@ -264,14 +247,11 @@ 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[] @@ -283,9 +263,6 @@ export type ConvoStateDisabled = { markConvoAccepted: MarkConvoAccepted addReaction: AddReaction removeReaction: RemoveReaction - isGroup: IsGroup - getGroupInfo: GetGroupInfo - getPrimaryMember: GetPrimaryMember } export type ConvoState = | ConvoStateUninitialized