From 31af769019985568a6cbe67f8544888bc19388c1 Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Thu, 28 Nov 2024 08:02:35 -0500 Subject: [PATCH] feat: Decoded Message Types Improved Decoded Message types and returns from listing Added DecodedMessageUnion utility type to manage returned values from list methods Added more type tests to help check against type usage --- example/src/types/typeTests.ts | 62 ++++++++++++++++++++++++++-- src/index.ts | 11 +++-- src/lib/Conversation.ts | 11 +++-- src/lib/Conversations.ts | 16 ++++--- src/lib/DecodedMessage.ts | 31 +++++++------- src/lib/Dm.ts | 15 ++++--- src/lib/Group.ts | 16 ++++--- src/lib/types/DecodedMessageUnion.ts | 11 +++++ 8 files changed, 130 insertions(+), 43 deletions(-) create mode 100644 src/lib/types/DecodedMessageUnion.ts diff --git a/example/src/types/typeTests.ts b/example/src/types/typeTests.ts index 226b7d49d..7accec7aa 100644 --- a/example/src/types/typeTests.ts +++ b/example/src/types/typeTests.ts @@ -2,6 +2,7 @@ import { Client, ContentTypeId, ConversationVersion, + DecodedMessage, Dm, EncodedContent, JSContentCodec, @@ -9,6 +10,7 @@ import { ReplyCodec, sendMessage, TextCodec, + DecodedMessageUnion, } from 'xmtp-react-native-sdk' const ContentTypeNumber: ContentTypeId = { @@ -169,8 +171,8 @@ export const typeTests = async () => { topNumber: { bottomNumber: 12, }, - }, - { contentType: ContentTypeNumber } + } + // { contentType: ContentTypeNumber } ) const customContentGroup = (await customContentClient.conversations.list())[0] @@ -180,8 +182,8 @@ export const typeTests = async () => { topNumber: { bottomNumber: 12, }, - }, - { contentType: ContentTypeNumber } + } + // { contentType: ContentTypeNumber } ) const customContentMessages = await customContentConvo.messages() customContentMessages[0].content() @@ -268,4 +270,56 @@ export const typeTests = async () => { // @ts-expect-error const peerAddress2 = firstConvo.peerInboxId() } + + const multiCodecClient = await Client.createRandom({ + env: 'local', + codecs: [new TextCodec(), new ReactionCodec(), new ReplyCodec()] as const, + dbEncryptionKey: keyBytes, + }) + const multiCodecConvo = (await multiCodecClient.conversations.list())[0] + const decodedMessageClient = await multiCodecConvo.messages() + + for (const message of decodedMessageClient) { + if (isReactionMessage(message)) { + const { content, action, reference, schema } = message.content() + } else if (isReplyMessage(message)) { + const { content } = message.content() + } else if (isTextMessage(message)) { + const text = message.content() + text.toLowerCase() + } + } + const textMessage = decodedMessageClient[0] as DecodedMessage + const text = textMessage.content() + text.toLowerCase() + + // Message Can infer additional codecs + const message = DecodedMessage.fromObject( + decodedMessageClient[0], + textClient + ) + if (isTextMessage(message)) { + const text = message.content() + } else { + // @ts-expect-error + message.content() + } +} + +const isTextMessage = ( + message: DecodedMessageUnion +): message is DecodedMessage => { + return message.contentTypeId.includes('text') +} + +const isReactionMessage = ( + message: DecodedMessageUnion +): message is DecodedMessage => { + return message.contentTypeId.includes('reaction') +} + +const isReplyMessage = ( + message: DecodedMessageUnion +): message is DecodedMessage => { + return message.contentTypeId.includes('reply') } diff --git a/src/index.ts b/src/index.ts index 7ed81248c..54bbbb9eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import { ConversationId, ConversationTopic, } from './lib/types/ConversationOptions' +import { DecodedMessageUnion } from './lib/types/DecodedMessageUnion' import { DefaultContentTypes } from './lib/types/DefaultContentType' import { MessageId, MessageOrder } from './lib/types/MessagesOptions' import { PermissionPolicySet } from './lib/types/PermissionPolicySet' @@ -403,7 +404,7 @@ export async function conversationMessages< beforeNs?: number | undefined, afterNs?: number | undefined, direction?: MessageOrder | undefined -): Promise[]> { +): Promise[]> { const messages = await XMTPModule.conversationMessages( client.installationId, conversationId, @@ -418,11 +419,12 @@ export async function conversationMessages< } export async function findMessage< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, + ContentType extends DefaultContentTypes[number] = DefaultContentTypes[number], + ContentTypes extends DefaultContentTypes = [ContentType], // Adjusted to work with arrays >( client: Client, messageId: MessageId -): Promise | undefined> { +): Promise | undefined> { const message = await XMTPModule.findMessage(client.installationId, messageId) return DecodedMessage.from(message, client) } @@ -958,7 +960,7 @@ export async function processMessage< client: Client, id: ConversationId, encryptedMessage: string -): Promise> { +): Promise> { const json = await XMTPModule.processMessage( client.installationId, id, @@ -1144,3 +1146,4 @@ export { ConversationType, } from './lib/types/ConversationOptions' export { MessageId, MessageOrder } from './lib/types/MessagesOptions' +export { DecodedMessageUnion } from './lib/types/DecodedMessageUnion' diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index 115219720..ab472c8c3 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,5 +1,6 @@ import { ConsentState } from './ConsentRecord' import { ConversationSendPayload, MessageId, MessagesOptions } from './types' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' import { DecodedMessage, Member, Dm, Group } from '../index' @@ -16,21 +17,23 @@ export interface ConversationBase { version: ConversationVersion id: string state: ConsentState - lastMessage?: DecodedMessage + lastMessage?: DecodedMessage send( content: ConversationSendPayload ): Promise sync() - messages(opts?: MessagesOptions): Promise[]> + messages(opts?: MessagesOptions): Promise[]> streamMessages( - callback: (message: DecodedMessage) => Promise + callback: ( + message: DecodedMessage + ) => Promise ): Promise<() => void> consentState(): Promise updateConsent(state: ConsentState): Promise processMessage( encryptedMessage: string - ): Promise> + ): Promise> members(): Promise } diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 61d7e63cb..dd25aebc1 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -8,13 +8,14 @@ import { ConversationOptions, } from './types/ConversationOptions' import { CreateGroupOptions } from './types/CreateGroupOptions' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' +import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { PermissionPolicySet } from './types/PermissionPolicySet' import * as XMTPModule from '../index' import { Address, ConsentState, - ContentCodec, Conversation, ConversationId, ConversationTopic, @@ -24,7 +25,7 @@ import { import { getAddress } from '../utils/address' export default class Conversations< - ContentTypes extends ContentCodec[] = [], + ContentTypes extends DefaultContentTypes = DefaultContentTypes, > { client: Client private subscriptions: { [key: string]: { remove: () => void } } = {} @@ -101,7 +102,7 @@ export default class Conversations< */ async findMessage( messageId: MessageId - ): Promise | undefined> { + ): Promise | undefined> { return await XMTPModule.findMessage(this.client, messageId) } @@ -327,7 +328,7 @@ export default class Conversations< * @returns {Promise} A Promise that resolves when the stream is set up. */ async streamAllMessages( - callback: (message: DecodedMessage) => Promise, + callback: (message: DecodedMessageUnion) => Promise, type: ConversationType = 'all' ): Promise { XMTPModule.subscribeToAllMessages(this.client.installationId, type) @@ -343,7 +344,12 @@ export default class Conversations< if (installationId !== this.client.installationId) { return } - await callback(DecodedMessage.fromObject(message, this.client)) + await callback( + DecodedMessage.fromObject( + message, + this.client + ) as DecodedMessageUnion + ) } ) this.subscriptions[EventTypes.Message] = subscription diff --git a/src/lib/DecodedMessage.ts b/src/lib/DecodedMessage.ts index c8644f782..f5dd48976 100644 --- a/src/lib/DecodedMessage.ts +++ b/src/lib/DecodedMessage.ts @@ -6,7 +6,7 @@ import { NativeContentCodec, NativeMessageContent, } from './ContentCodec' -import { TextCodec } from './NativeCodecs/TextCodec' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' const allowEmptyProperties: (keyof NativeMessageContent)[] = [ @@ -21,6 +21,7 @@ export enum MessageDeliveryStatus { } export class DecodedMessage< + ContentType extends DefaultContentTypes[number] = DefaultContentTypes[number], ContentTypes extends DefaultContentTypes = DefaultContentTypes, > { client: Client @@ -33,12 +34,16 @@ export class DecodedMessage< fallback: string | undefined deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED - static from( + static from< + ContentType extends + DefaultContentTypes[number] = DefaultContentTypes[number], + ContentTypes extends DefaultContentTypes = ContentType[], + >( json: string, client: Client - ): DecodedMessage { + ): DecodedMessageUnion { const decoded = JSON.parse(json) - return new DecodedMessage( + return new DecodedMessage( client, decoded.id, decoded.topic, @@ -48,11 +53,13 @@ export class DecodedMessage< decoded.content, decoded.fallback, decoded.deliveryStatus - ) + ) as DecodedMessageUnion } static fromObject< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, + ContentType extends + DefaultContentTypes[number] = DefaultContentTypes[number], + ContentTypes extends DefaultContentTypes = [ContentType], >( object: { id: string @@ -65,7 +72,7 @@ export class DecodedMessage< deliveryStatus: MessageDeliveryStatus | undefined }, client: Client - ): DecodedMessage { + ): DecodedMessage { return new DecodedMessage( client, object.id, @@ -102,15 +109,13 @@ export class DecodedMessage< this.deliveryStatus = deliveryStatus } - content(): ExtractDecodedType<[...ContentTypes, TextCodec][number] | string> { + content(): ExtractDecodedType { const encodedJSON = this.nativeContent.encoded if (encodedJSON) { const encoded = JSON.parse(encodedJSON) const codec = this.client.codecRegistry[ this.contentTypeId - ] as JSContentCodec< - ExtractDecodedType<[...ContentTypes, TextCodec][number]> - > + ] as JSContentCodec> if (!codec) { throw new Error( `no content type found ${JSON.stringify(this.contentTypeId)}` @@ -129,9 +134,7 @@ export class DecodedMessage< ) ) { return ( - codec as NativeContentCodec< - ExtractDecodedType<[...ContentTypes, TextCodec][number]> - > + codec as NativeContentCodec> ).decode(this.nativeContent) } } diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index 4dfb3196e..9136898da 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -4,6 +4,7 @@ import { ConversationVersion, ConversationBase } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { MessageId, MessagesOptions } from './types/MessagesOptions' @@ -27,12 +28,12 @@ export class Dm version = ConversationVersion.DM as const topic: ConversationTopic state: ConsentState - lastMessage?: DecodedMessage + lastMessage?: DecodedMessageUnion constructor( client: XMTP.Client, params: DmParams, - lastMessage?: DecodedMessage + lastMessage?: DecodedMessageUnion ) { this.client = client this.id = params.id @@ -141,7 +142,7 @@ export class Dm */ async messages( opts?: MessagesOptions - ): Promise[]> { + ): Promise[]> { return await XMTP.conversationMessages( this.client, this.id, @@ -171,7 +172,9 @@ export class Dm * @returns {Function} A function that, when called, unsubscribes from the message stream and ends real-time updates. */ async streamMessages( - callback: (message: DecodedMessage) => Promise + callback: ( + message: DecodedMessage + ) => Promise ): Promise<() => void> { await XMTP.subscribeToMessages(this.client.installationId, this.id) const messageSubscription = XMTP.emitter.addListener( @@ -182,7 +185,7 @@ export class Dm conversationId, }: { installationId: string - message: DecodedMessage + message: DecodedMessage conversationId: string }) => { if (installationId !== this.client.installationId) { @@ -204,7 +207,7 @@ export class Dm async processMessage( encryptedMessage: string - ): Promise> { + ): Promise> { try { return await XMTP.processMessage(this.client, this.id, encryptedMessage) } catch (e) { diff --git a/src/lib/Group.ts b/src/lib/Group.ts index e431d4a63..ec197464b 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -4,6 +4,7 @@ import { ConversationBase, ConversationVersion } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { MessageId, MessagesOptions } from './types/MessagesOptions' @@ -41,12 +42,12 @@ export class Group< imageUrlSquare: string description: string state: ConsentState - lastMessage?: DecodedMessage + lastMessage?: DecodedMessageUnion constructor( client: XMTP.Client, params: GroupParams, - lastMessage?: DecodedMessage + lastMessage?: DecodedMessageUnion ) { this.client = client this.id = params.id @@ -167,9 +168,10 @@ export class Group< * @param direction - Optional parameter to specify the time ordering of the messages to return. * @returns {Promise[]>} A Promise that resolves to an array of DecodedMessage objects. */ + async messages( opts?: MessagesOptions - ): Promise[]> { + ): Promise[]> { return await XMTP.conversationMessages( this.client, this.id, @@ -199,7 +201,9 @@ export class Group< * @returns {Function} A function that, when called, unsubscribes from the message stream and ends real-time updates. */ async streamMessages( - callback: (message: DecodedMessage) => Promise + callback: ( + message: DecodedMessage + ) => Promise ): Promise<() => void> { await XMTP.subscribeToMessages(this.client.installationId, this.id) const messageSubscription = XMTP.emitter.addListener( @@ -210,7 +214,7 @@ export class Group< conversationId, }: { installationId: string - message: DecodedMessage + message: DecodedMessage conversationId: string }) => { if (installationId !== this.client.installationId) { @@ -595,7 +599,7 @@ export class Group< async processMessage( encryptedMessage: string - ): Promise> { + ): Promise> { try { return await XMTP.processMessage(this.client, this.id, encryptedMessage) } catch (e) { diff --git a/src/lib/types/DecodedMessageUnion.ts b/src/lib/types/DecodedMessageUnion.ts new file mode 100644 index 000000000..52c359f04 --- /dev/null +++ b/src/lib/types/DecodedMessageUnion.ts @@ -0,0 +1,11 @@ +import { DefaultContentTypes } from './DefaultContentType' +import { ContentCodec } from '../ContentCodec' +import { DecodedMessage } from '../DecodedMessage' + +export type DecodedMessageUnion< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +> = { + [K in keyof ContentTypes]: ContentTypes[K] extends ContentCodec + ? DecodedMessage + : never +}[number]