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 01219a2f0..2d1b70381 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' @@ -405,7 +406,7 @@ export async function conversationMessages< beforeNs?: number | undefined, afterNs?: number | undefined, direction?: MessageOrder | undefined -): Promise[]> { +): Promise[]> { const messages = await XMTPModule.conversationMessages( client.installationId, conversationId, @@ -420,11 +421,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) } @@ -960,7 +962,7 @@ export async function processMessage< client: Client, id: ConversationId, encryptedMessage: string -): Promise> { +): Promise> { const json = await XMTPModule.processMessage( client.installationId, id, @@ -1146,3 +1148,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]