Skip to content

Commit

Permalink
Merge pull request #552 from xmtp/ar/decoded-message-types
Browse files Browse the repository at this point in the history
feat: Decoded Message Types
  • Loading branch information
alexrisch authored Dec 3, 2024
2 parents ef59026 + 31af769 commit d396e0b
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 43 deletions.
62 changes: 58 additions & 4 deletions example/src/types/typeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {
Client,
ContentTypeId,
ConversationVersion,
DecodedMessage,
Dm,
EncodedContent,
JSContentCodec,
ReactionCodec,
ReplyCodec,
sendMessage,
TextCodec,
DecodedMessageUnion,
} from 'xmtp-react-native-sdk'

const ContentTypeNumber: ContentTypeId = {
Expand Down Expand Up @@ -169,8 +171,8 @@ export const typeTests = async () => {
topNumber: {
bottomNumber: 12,
},
},
{ contentType: ContentTypeNumber }
}
// { contentType: ContentTypeNumber }
)

const customContentGroup = (await customContentClient.conversations.list())[0]
Expand All @@ -180,8 +182,8 @@ export const typeTests = async () => {
topNumber: {
bottomNumber: 12,
},
},
{ contentType: ContentTypeNumber }
}
// { contentType: ContentTypeNumber }
)
const customContentMessages = await customContentConvo.messages()
customContentMessages[0].content()
Expand Down Expand Up @@ -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<TextCodec>
const text = textMessage.content()
text.toLowerCase()

// Message Can infer additional codecs
const message = DecodedMessage.fromObject<TextCodec>(
decodedMessageClient[0],
textClient
)
if (isTextMessage(message)) {
const text = message.content()
} else {
// @ts-expect-error
message.content()
}
}

const isTextMessage = (
message: DecodedMessageUnion
): message is DecodedMessage<TextCodec> => {
return message.contentTypeId.includes('text')
}

const isReactionMessage = (
message: DecodedMessageUnion
): message is DecodedMessage<ReactionCodec> => {
return message.contentTypeId.includes('reaction')
}

const isReplyMessage = (
message: DecodedMessageUnion
): message is DecodedMessage<ReplyCodec> => {
return message.contentTypeId.includes('reply')
}
11 changes: 7 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -403,7 +404,7 @@ export async function conversationMessages<
beforeNs?: number | undefined,
afterNs?: number | undefined,
direction?: MessageOrder | undefined
): Promise<DecodedMessage<ContentTypes>[]> {
): Promise<DecodedMessageUnion<ContentTypes>[]> {
const messages = await XMTPModule.conversationMessages(
client.installationId,
conversationId,
Expand All @@ -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<ContentTypes>,
messageId: MessageId
): Promise<DecodedMessage<ContentTypes> | undefined> {
): Promise<DecodedMessageUnion<ContentTypes> | undefined> {
const message = await XMTPModule.findMessage(client.installationId, messageId)
return DecodedMessage.from(message, client)
}
Expand Down Expand Up @@ -958,7 +960,7 @@ export async function processMessage<
client: Client<ContentTypes>,
id: ConversationId,
encryptedMessage: string
): Promise<DecodedMessage<ContentTypes>> {
): Promise<DecodedMessageUnion<ContentTypes>> {
const json = await XMTPModule.processMessage(
client.installationId,
id,
Expand Down Expand Up @@ -1144,3 +1146,4 @@ export {
ConversationType,
} from './lib/types/ConversationOptions'
export { MessageId, MessageOrder } from './lib/types/MessagesOptions'
export { DecodedMessageUnion } from './lib/types/DecodedMessageUnion'
11 changes: 7 additions & 4 deletions src/lib/Conversation.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -16,21 +17,23 @@ export interface ConversationBase<ContentTypes extends DefaultContentTypes> {
version: ConversationVersion
id: string
state: ConsentState
lastMessage?: DecodedMessage<ContentTypes>
lastMessage?: DecodedMessage<ContentTypes[number], ContentTypes>

send<SendContentTypes extends DefaultContentTypes = ContentTypes>(
content: ConversationSendPayload<SendContentTypes>
): Promise<MessageId>
sync()
messages(opts?: MessagesOptions): Promise<DecodedMessage<ContentTypes>[]>
messages(opts?: MessagesOptions): Promise<DecodedMessageUnion<ContentTypes>[]>
streamMessages(
callback: (message: DecodedMessage<ContentTypes>) => Promise<void>
callback: (
message: DecodedMessage<ContentTypes[number], ContentTypes>
) => Promise<void>
): Promise<() => void>
consentState(): Promise<ConsentState>
updateConsent(state: ConsentState): Promise<void>
processMessage(
encryptedMessage: string
): Promise<DecodedMessage<ContentTypes>>
): Promise<DecodedMessage<ContentTypes[number], ContentTypes>>
members(): Promise<Member[]>
}

Expand Down
16 changes: 11 additions & 5 deletions src/lib/Conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,7 +25,7 @@ import {
import { getAddress } from '../utils/address'

export default class Conversations<
ContentTypes extends ContentCodec<any>[] = [],
ContentTypes extends DefaultContentTypes = DefaultContentTypes,
> {
client: Client<ContentTypes>
private subscriptions: { [key: string]: { remove: () => void } } = {}
Expand Down Expand Up @@ -101,7 +102,7 @@ export default class Conversations<
*/
async findMessage(
messageId: MessageId
): Promise<DecodedMessage<ContentTypes> | undefined> {
): Promise<DecodedMessageUnion<ContentTypes> | undefined> {
return await XMTPModule.findMessage(this.client, messageId)
}

Expand Down Expand Up @@ -327,7 +328,7 @@ export default class Conversations<
* @returns {Promise<void>} A Promise that resolves when the stream is set up.
*/
async streamAllMessages(
callback: (message: DecodedMessage<ContentTypes>) => Promise<void>,
callback: (message: DecodedMessageUnion<ContentTypes>) => Promise<void>,
type: ConversationType = 'all'
): Promise<void> {
XMTPModule.subscribeToAllMessages(this.client.installationId, type)
Expand All @@ -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<ContentTypes>
)
}
)
this.subscriptions[EventTypes.Message] = subscription
Expand Down
31 changes: 17 additions & 14 deletions src/lib/DecodedMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)[] = [
Expand All @@ -21,6 +21,7 @@ export enum MessageDeliveryStatus {
}

export class DecodedMessage<
ContentType extends DefaultContentTypes[number] = DefaultContentTypes[number],
ContentTypes extends DefaultContentTypes = DefaultContentTypes,
> {
client: Client<ContentTypes>
Expand All @@ -33,12 +34,16 @@ export class DecodedMessage<
fallback: string | undefined
deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED

static from<ContentTypes extends DefaultContentTypes = DefaultContentTypes>(
static from<
ContentType extends
DefaultContentTypes[number] = DefaultContentTypes[number],
ContentTypes extends DefaultContentTypes = ContentType[],
>(
json: string,
client: Client<ContentTypes>
): DecodedMessage<ContentTypes> {
): DecodedMessageUnion<ContentTypes> {
const decoded = JSON.parse(json)
return new DecodedMessage<ContentTypes>(
return new DecodedMessage<ContentType, ContentTypes>(
client,
decoded.id,
decoded.topic,
Expand All @@ -48,11 +53,13 @@ export class DecodedMessage<
decoded.content,
decoded.fallback,
decoded.deliveryStatus
)
) as DecodedMessageUnion<ContentTypes>
}

static fromObject<
ContentTypes extends DefaultContentTypes = DefaultContentTypes,
ContentType extends
DefaultContentTypes[number] = DefaultContentTypes[number],
ContentTypes extends DefaultContentTypes = [ContentType],
>(
object: {
id: string
Expand All @@ -65,7 +72,7 @@ export class DecodedMessage<
deliveryStatus: MessageDeliveryStatus | undefined
},
client: Client<ContentTypes>
): DecodedMessage<ContentTypes> {
): DecodedMessage<ContentType, ContentTypes> {
return new DecodedMessage(
client,
object.id,
Expand Down Expand Up @@ -102,15 +109,13 @@ export class DecodedMessage<
this.deliveryStatus = deliveryStatus
}

content(): ExtractDecodedType<[...ContentTypes, TextCodec][number] | string> {
content(): ExtractDecodedType<ContentType> {
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<ExtractDecodedType<ContentType>>
if (!codec) {
throw new Error(
`no content type found ${JSON.stringify(this.contentTypeId)}`
Expand All @@ -129,9 +134,7 @@ export class DecodedMessage<
)
) {
return (
codec as NativeContentCodec<
ExtractDecodedType<[...ContentTypes, TextCodec][number]>
>
codec as NativeContentCodec<ExtractDecodedType<ContentType>>
).decode(this.nativeContent)
}
}
Expand Down
15 changes: 9 additions & 6 deletions src/lib/Dm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -27,12 +28,12 @@ export class Dm<ContentTypes extends DefaultContentTypes = DefaultContentTypes>
version = ConversationVersion.DM as const
topic: ConversationTopic
state: ConsentState
lastMessage?: DecodedMessage<ContentTypes>
lastMessage?: DecodedMessageUnion<ContentTypes>

constructor(
client: XMTP.Client<ContentTypes>,
params: DmParams,
lastMessage?: DecodedMessage<ContentTypes>
lastMessage?: DecodedMessageUnion<ContentTypes>
) {
this.client = client
this.id = params.id
Expand Down Expand Up @@ -141,7 +142,7 @@ export class Dm<ContentTypes extends DefaultContentTypes = DefaultContentTypes>
*/
async messages(
opts?: MessagesOptions
): Promise<DecodedMessage<ContentTypes>[]> {
): Promise<DecodedMessageUnion<ContentTypes>[]> {
return await XMTP.conversationMessages(
this.client,
this.id,
Expand Down Expand Up @@ -171,7 +172,9 @@ export class Dm<ContentTypes extends DefaultContentTypes = DefaultContentTypes>
* @returns {Function} A function that, when called, unsubscribes from the message stream and ends real-time updates.
*/
async streamMessages(
callback: (message: DecodedMessage<ContentTypes>) => Promise<void>
callback: (
message: DecodedMessage<ContentTypes[number], ContentTypes>
) => Promise<void>
): Promise<() => void> {
await XMTP.subscribeToMessages(this.client.installationId, this.id)
const messageSubscription = XMTP.emitter.addListener(
Expand All @@ -182,7 +185,7 @@ export class Dm<ContentTypes extends DefaultContentTypes = DefaultContentTypes>
conversationId,
}: {
installationId: string
message: DecodedMessage<ContentTypes>
message: DecodedMessage<ContentTypes[number], ContentTypes>
conversationId: string
}) => {
if (installationId !== this.client.installationId) {
Expand All @@ -204,7 +207,7 @@ export class Dm<ContentTypes extends DefaultContentTypes = DefaultContentTypes>

async processMessage(
encryptedMessage: string
): Promise<DecodedMessage<ContentTypes>> {
): Promise<DecodedMessageUnion<ContentTypes>> {
try {
return await XMTP.processMessage(this.client, this.id, encryptedMessage)
} catch (e) {
Expand Down
Loading

0 comments on commit d396e0b

Please sign in to comment.