import { type Actions, type Attachment, type ConsentState, type EncodedContent, type Intent, type ListMessagesOptions, type MultiRemoteAttachment, type Reaction, type RemoteAttachment, type Reply, type SendMessageOpts, type TransactionReference, type WalletSendCalls, type DecodedMessage as XmtpDecodedMessage, } from "@xmtp/wasm-bindings"; import type { CodecRegistry } from "@/CodecRegistry"; import { DecodedMessage } from "@/DecodedMessage"; import type { ClientWorkerAction } from "@/types/actions"; import type { SafeConversation } from "@/utils/conversions"; import { nsToDate } from "@/utils/date"; import { createStream, type StreamCallback, type StreamOptions, } from "@/utils/streams"; import { uuid } from "@/utils/uuid"; import type { WorkerBridge } from "@/utils/WorkerBridge"; /** * Represents a conversation * * This class is not intended to be initialized directly. */ export class Conversation { #addedByInboxId?: SafeConversation["addedByInboxId"]; #codecRegistry: CodecRegistry; #createdAtNs?: SafeConversation["createdAtNs"]; #id: string; #metadata?: SafeConversation["metadata"]; #worker: WorkerBridge; /** * Creates a new conversation instance * * @param worker - The worker bridge instance for client communication * @param codecRegistry - The codec registry instance * @param id - The unique identifier for this conversation * @param data - Optional conversation data to initialize with */ constructor( worker: WorkerBridge, codecRegistry: CodecRegistry, id: string, data?: SafeConversation, ) { this.#worker = worker; this.#codecRegistry = codecRegistry; this.#id = id; this.#syncData(data); } #syncData(data?: SafeConversation) { this.#addedByInboxId = data?.addedByInboxId; this.#metadata = data?.metadata; this.#createdAtNs = data?.createdAtNs; } get id() { return this.#id; } get addedByInboxId() { return this.#addedByInboxId; } get createdAtNs() { return this.#createdAtNs; } get createdAt() { return this.#createdAtNs ? nsToDate(this.#createdAtNs) : undefined; } get metadata() { return this.#metadata; } get topic() { return `/xmtp/mls/1/g-${this.id}/proto`; } async lastMessage() { const lastMessage = await this.#worker.action("conversation.lastMessage", { id: this.#id, }); return lastMessage ? new DecodedMessage(this.#codecRegistry, lastMessage) : undefined; } async isActive() { return this.#worker.action("conversation.isActive", { id: this.#id, }); } /** * Gets the conversation members * * @returns Promise that resolves with the conversation members */ async members() { return this.#worker.action("conversation.members", { id: this.#id, }); } /** * Synchronizes conversation data from the network * * @returns Promise that resolves with the updated conversation data */ async sync() { const data = await this.#worker.action("conversation.sync", { id: this.#id, }); this.#syncData(data); return data; } /** * Publishes pending messages that were sent optimistically * * @returns Promise that resolves when publishing is complete */ async publishMessages() { return this.#worker.action("conversation.publishMessages", { id: this.#id, }); } /** * Decodes, decrypts, and persists a raw envelope from a group message stream. * * @param envelopeBytes - Raw protobuf-encoded envelope bytes from the stream * @returns The processed and stored messages */ async processStreamedMessage(envelopeBytes: Uint8Array) { return this.#worker.action("conversation.processStreamedMessage", { id: this.#id, envelopeBytes, }); } /** * Sends a message * * @param content - The encoded content to send * @param options - Optional send options * @param options.shouldPush - Indicates whether this message should be * included in push notifications * @param options.isOptimistic - Indicates whether this message should be * sent optimistically and published later via `publishMessages` * @returns Promise that resolves with the message ID after it has been sent */ async send(content: EncodedContent, options?: SendMessageOpts) { return this.#worker.action("conversation.send", { id: this.#id, content, options, }); } /** * Sends a text message * * @param text - The text to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendText(text: string, isOptimistic?: boolean) { return this.#worker.action("conversation.sendText", { id: this.#id, text, isOptimistic, }); } /** * Sends a markdown message * * @param markdown - The markdown to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendMarkdown(markdown: string, isOptimistic?: boolean) { return this.#worker.action("conversation.sendMarkdown", { id: this.#id, markdown, isOptimistic, }); } /** * Sends a reaction message * * @param reaction - The reaction to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendReaction(reaction: Reaction, isOptimistic?: boolean) { return this.#worker.action("conversation.sendReaction", { id: this.#id, reaction, isOptimistic, }); } /** * Sends a read receipt message * * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendReadReceipt(isOptimistic?: boolean) { return this.#worker.action("conversation.sendReadReceipt", { id: this.#id, isOptimistic, }); } /** * Sends a reply message * * @param reply - The reply to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendReply(reply: Reply, isOptimistic?: boolean) { return this.#worker.action("conversation.sendReply", { id: this.#id, reply, isOptimistic, }); } /** * Sends a transaction reference message * * @param transactionReference - The transaction reference to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendTransactionReference( transactionReference: TransactionReference, isOptimistic?: boolean, ) { return this.#worker.action("conversation.sendTransactionReference", { id: this.#id, transactionReference, isOptimistic, }); } /** * Sends a wallet send calls message * * @param walletSendCalls - The wallet send calls to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendWalletSendCalls( walletSendCalls: WalletSendCalls, isOptimistic?: boolean, ) { return this.#worker.action("conversation.sendWalletSendCalls", { id: this.#id, walletSendCalls, isOptimistic, }); } /** * Sends an actions message * * @param actions - The actions to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendActions(actions: Actions, isOptimistic?: boolean) { return this.#worker.action("conversation.sendActions", { id: this.#id, actions, isOptimistic, }); } /** * Sends an intent message * * @param intent - The intent to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendIntent(intent: Intent, isOptimistic?: boolean) { return this.#worker.action("conversation.sendIntent", { id: this.#id, intent, isOptimistic, }); } /** * Sends an attachment message * * @param attachment - The attachment to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendAttachment(attachment: Attachment, isOptimistic?: boolean) { return this.#worker.action("conversation.sendAttachment", { id: this.#id, attachment, isOptimistic, }); } /** * Sends a multi remote attachment message * * @param multiRemoteAttachment - The multi remote attachment to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendMultiRemoteAttachment( multiRemoteAttachment: MultiRemoteAttachment, isOptimistic?: boolean, ) { return this.#worker.action("conversation.sendMultiRemoteAttachment", { id: this.#id, multiRemoteAttachment, isOptimistic, }); } /** * Sends a remote attachment message * * @param remoteAttachment - The remote attachment to send * @param isOptimistic - Whether to send the message optimistically * @returns Promise that resolves with the message ID after it has been sent */ async sendRemoteAttachment( remoteAttachment: RemoteAttachment, isOptimistic?: boolean, ) { return this.#worker.action("conversation.sendRemoteAttachment", { id: this.#id, remoteAttachment, isOptimistic, }); } /** * Lists messages in this conversation * * @param options - Optional filtering and pagination options * @returns Promise that resolves with an array of decoded messages */ async messages(options?: ListMessagesOptions) { const messages = await this.#worker.action("conversation.messages", { id: this.#id, options, }); return messages.map( (message) => new DecodedMessage(this.#codecRegistry, message), ); } /** * Counts messages in this conversation * * @param options - Optional filtering options * @returns Promise that resolves with the count of messages */ async countMessages( options?: Omit, ) { const count = await this.#worker.action("conversation.countMessages", { id: this.#id, options, }); return count; } /** * Gets the consent state for this conversation * * @returns Promise that resolves with the current consent state */ async consentState(): Promise { return this.#worker.action("conversation.consentState", { id: this.#id, }); } /** * Updates the consent state for this conversation * * @param state - The new consent state to set * @returns Promise that resolves when the update is complete */ async updateConsentState(state: ConsentState) { return this.#worker.action("conversation.updateConsentState", { id: this.#id, state, }); } /** * Gets the message disappearing settings for this conversation * * @returns Promise that resolves with the current message disappearing settings */ async messageDisappearingSettings() { return this.#worker.action("conversation.messageDisappearingSettings", { id: this.#id, }); } /** * Updates message disappearing settings for this conversation * * @param fromNs - The timestamp from which messages should start disappearing * @param inNs - The duration after which messages should disappear * @returns Promise that resolves when the update is complete */ async updateMessageDisappearingSettings(fromNs: bigint, inNs: bigint) { return this.#worker.action( "conversation.updateMessageDisappearingSettings", { id: this.#id, fromNs, inNs, }, ); } /** * Removes message disappearing settings from this conversation * * @returns Promise that resolves when the settings are removed */ async removeMessageDisappearingSettings() { return this.#worker.action( "conversation.removeMessageDisappearingSettings", { id: this.#id, }, ); } /** * Checks if message disappearing is enabled for this conversation * * @returns Promise that resolves with whether message disappearing is enabled */ async isMessageDisappearingEnabled() { return this.#worker.action("conversation.isMessageDisappearingEnabled", { id: this.#id, }); } /** * Creates a stream for new messages in this conversation * * @param callback - Optional callback function for handling new stream values * @returns Stream instance for new messages */ async stream( options?: StreamOptions>, ) { const stream = async ( callback: StreamCallback, onFail: () => void, ) => { const streamId = uuid(); if (!options?.disableSync) { // sync the conversation await this.sync(); } // start the stream await this.#worker.action("conversation.stream", { groupId: this.#id, streamId, }); // handle stream messages return this.#worker.handleStreamMessage< XmtpDecodedMessage, DecodedMessage >(streamId, callback, { ...options, onFail, }); }; const convertMessage = (value: XmtpDecodedMessage) => { return new DecodedMessage(this.#codecRegistry, value); }; return createStream(stream, convertMessage, options); } async pausedForVersion() { return this.#worker.action("conversation.pausedForVersion", { id: this.#id, }); } /** * Gets HMAC keys for this conversation * * @returns Promise that resolves with the HMAC keys */ async hmacKeys() { return this.#worker.action("conversation.hmacKeys", { id: this.#id, }); } /** * Retrieves information for this conversation to help with debugging * * @returns The debug information for this conversation */ async debugInfo() { return this.#worker.action("conversation.debugInfo", { id: this.#id, }); } /** * Retrieves the last read times for this conversation * * @returns A map keyed by inbox ID with the last read timestamp * (nanoseconds since epoch) */ async lastReadTimes() { return this.#worker.action("conversation.lastReadTimes", { id: this.#id, }); } }