From b4dcd74a3cd43cce2624f3402c2bb85467c98597 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 14 Nov 2024 12:33:08 -0800 Subject: [PATCH 1/8] add prelimanary work for custom content types --- example/src/tests/conversationTests.ts | 1038 ++++++++++++++---------- src/index.ts | 24 + src/lib/Conversation.ts | 5 +- src/lib/Dm.ts | 32 +- src/lib/Group.ts | 32 +- 5 files changed, 701 insertions(+), 430 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 6c2c5cf65..573a334da 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,8 +1,13 @@ +import { content } from '@xmtp/proto' +import ReactNativeBlobUtil from 'react-native-blob-util' + import { Test, assert, createClients, delayToPropogate } from './test-utils' import { + Client, Conversation, ConversationId, ConversationVersion, + JSContentCodec, } from '../../../src/index' export const conversationTests: Test[] = [] @@ -14,465 +19,662 @@ function test(name: string, perform: () => Promise) { }) } -test('can find a conversations by id', async () => { - const [alixClient, boClient] = await createClients(2) - const alixGroup = await alixClient.conversations.newGroup([boClient.address]) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - - await boClient.conversations.sync() - const boGroup = await boClient.conversations.findConversation(alixGroup.id) - const boDm = await boClient.conversations.findConversation(alixDm.id) - const boDm2 = await boClient.conversations.findConversation( - 'GARBAGE' as ConversationId - ) - - assert(boDm2 === undefined, `bodm2 should be undefined`) - - assert( - boGroup?.id === alixGroup.id, - `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` - ) - - assert( - boDm?.id === alixDm.id, - `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` - ) - - return true -}) - -test('can find a conversation by topic', async () => { - const [alixClient, boClient] = await createClients(2) - const alixGroup = await alixClient.conversations.newGroup([boClient.address]) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - - await boClient.conversations.sync() - const boGroup = await boClient.conversations.findConversationByTopic( - alixGroup.topic - ) - const boDm = await boClient.conversations.findConversationByTopic( - alixDm.topic - ) - - assert( - boGroup?.id === alixGroup.id, - `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` - ) - - assert( - boDm?.id === alixDm.id, - `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` - ) - - return true -}) - -test('can find a dm by inbox id', async () => { - const [alixClient, boClient] = await createClients(2) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - - await boClient.conversations.sync() - const boDm = await boClient.conversations.findDmByInboxId(alixClient.inboxId) - - assert( - boDm?.id === alixDm.id, - `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` - ) - - return true -}) - -test('can find a dm by address', async () => { - const [alixClient, boClient] = await createClients(2) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - - await boClient.conversations.sync() - const boDm = await boClient.conversations.findDmByAddress(alixClient.address) +type EncodedContent = content.EncodedContent +type ContentTypeId = content.ContentTypeId - assert( - boDm?.id === alixDm.id, - `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` - ) +const { fs } = ReactNativeBlobUtil - return true -}) - -test('can list conversations with params', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) - const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) - const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) - const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) - - await boGroup1.send({ text: `first message` }) - await boGroup1.send({ text: `second message` }) - await boGroup1.send({ text: `third message` }) - await boDm2.send({ text: `third message` }) - await boGroup2.send({ text: `first message` }) - await boDm1.send({ text: `dm message` }) - // Order should be [Dm1, Group2, Dm2, Group1] - - await boClient.conversations.syncAllConversations() - const boConvosOrderCreated = await boClient.conversations.list() - const boConvosOrderLastMessage = await boClient.conversations.list( - { lastMessage: true }, - 'lastMessage' - ) - const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) - - assert( - boConvosOrderCreated.map((group: any) => group.id).toString() === - [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), - `Conversation created at order should be ${[boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString()} but was ${boConvosOrderCreated.map((group: any) => group.id).toString()}` - ) - - assert( - boConvosOrderLastMessage.map((group: any) => group.id).toString() === - [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), - `Conversation last message order should be ${[boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString()} but was ${boConvosOrderLastMessage.map((group: any) => group.id).toString()}` - ) - - const messages = await boConvosOrderLastMessage[0].messages() - assert( - messages[0].content() === 'dm message', - `last message 1 should be dm message ${messages[0].content()}` - ) - // assert( - // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', - // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` - // ) - assert( - boGroupsLimit.length === 1, - `List length should be 1 but was ${boGroupsLimit.length}` - ) - assert( - boGroupsLimit[0].id === boGroup1.id, - `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` - ) - - return true -}) - -test('can list groups', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const boGroup = await boClient.conversations.newGroup([alixClient.address]) - await boClient.conversations.newGroup([ - caroClient.address, - alixClient.address, - ]) - const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) - await boClient.conversations.findOrCreateDm(alixClient.address) - - const boConversations = await boClient.conversations.list() - await alixClient.conversations.sync() - const alixConversations = await alixClient.conversations.list() +const ContentTypeNumber: ContentTypeId = { + authorityId: 'org', + typeId: 'number', + versionMajor: 1, + versionMinor: 0, +} - assert( - boConversations.length === 4, - `bo conversation lengths should be 4 but was ${boConversations.length}` - ) +const ContentTypeNumberWithUndefinedFallback: ContentTypeId = { + authorityId: 'org', + typeId: 'number_undefined_fallback', + versionMajor: 1, + versionMinor: 0, +} - assert( - alixConversations.length === 3, - `alix conversation lengths should be 3 but was ${alixConversations.length}` - ) +const ContentTypeNumberWithEmptyFallback: ContentTypeId = { + authorityId: 'org', + typeId: 'number_empty_fallback', + versionMajor: 1, + versionMinor: 0, +} - if ( - boConversations[0].topic !== boGroup.topic || - boConversations[0].version !== ConversationVersion.GROUP || - boConversations[2].version !== ConversationVersion.DM || - boConversations[2].createdAt !== boDm.createdAt - ) { - throw Error('Listed containers should match streamed containers') +export type NumberRef = { + topNumber: { + bottomNumber: number } +} - return true -}) - -test('can list conversation messages', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const boGroup = await boClient.conversations.newGroup([alixClient.address]) - const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) - const boGroupConversation = await boClient.conversations.findConversation( - boGroup.id - ) - const boDmConversation = await boClient.conversations.findConversation( - boDm.id - ) - - await boGroupConversation?.send('hello') - await boGroupConversation?.send('hello') - await boDmConversation?.send('hello') - await boDmConversation?.send('hello') - - const boGroupMessages = await boGroupConversation?.messages() - const boDmMessages = await boDmConversation?.messages() +class NumberCodec implements JSContentCodec { + contentType = ContentTypeNumber + + // a completely absurd way of encoding number values + encode(content: NumberRef): EncodedContent { + return { + type: ContentTypeNumber, + parameters: { + test: 'test', + }, + content: new TextEncoder().encode(JSON.stringify(content)), + } + } - assert( - boGroupMessages?.length === 3, - `bo conversation lengths should be 4 but was ${boGroupMessages?.length}` - ) + decode(encodedContent: EncodedContent): NumberRef { + if (encodedContent.parameters.test !== 'test') { + throw new Error(`parameters should parse ${encodedContent.parameters}`) + } + const contentReceived = JSON.parse( + new TextDecoder().decode(encodedContent.content) + ) as NumberRef + return contentReceived + } - assert( - boDmMessages?.length === 3, - `alix conversation lengths should be 3 but was ${boDmMessages?.length}` - ) + fallback(content: NumberRef): string | undefined { + return 'a billion' + } +} - return true -}) +class NumberCodecUndefinedFallback extends NumberCodec { + contentType = ContentTypeNumberWithUndefinedFallback + fallback(content: NumberRef): string | undefined { + return undefined + } +} -test('can stream both conversations and messages at same time', async () => { - const [alix, bo] = await createClients(2) +class NumberCodecEmptyFallback extends NumberCodec { + contentType = ContentTypeNumberWithEmptyFallback + fallback(content: NumberRef): string | undefined { + return '' + } +} - let conversationCallbacks = 0 - let messageCallbacks = 0 - await bo.conversations.stream(async () => { - conversationCallbacks++ +test('register and use custom content types', async () => { + const keyBytes = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + const bob = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + dbEncryptionKey: keyBytes, }) - - await bo.conversations.streamAllMessages(async () => { - messageCallbacks++ + const alice = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + dbEncryptionKey: keyBytes, }) - const group = await alix.conversations.newGroup([bo.address]) - const dm = await alix.conversations.findOrCreateDm(bo.address) - await delayToPropogate() - await group.send('hello') - await dm.send('hello') - await delayToPropogate() - - assert( - conversationCallbacks === 2, - 'conversation stream should have received 2 conversation' - ) - assert( - messageCallbacks === 2, - 'message stream should have received 2 message' - ) + bob.register(new NumberCodec()) + alice.register(new NumberCodec()) - return true -}) + await delayToPropogate() -test('can stream conversation messages', async () => { - const [alixClient, boClient] = await createClients(2) + const bobConvo = await bob.conversations.newConversation(alice.address) + await delayToPropogate() + const aliceConvo = await alice.conversations.newConversation(bob.address) - const alixGroup = await alixClient.conversations.newGroup([boClient.address]) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - const alixGroupConversation = await alixClient.conversations.findConversation( - alixGroup.id + await bobConvo.send( + { topNumber: { bottomNumber: 12 } }, + { contentType: ContentTypeNumber } ) - const alixDmConversation = await alixClient.conversations.findConversation( - alixDm.id - ) - - let dmMessageCallbacks = 0 - let conversationMessageCallbacks = 0 - await alixGroupConversation?.streamMessages(async () => { - conversationMessageCallbacks++ - }) - await alixDmConversation?.streamMessages(async () => { - dmMessageCallbacks++ - }) + const messages = await aliceConvo.messages() + assert(messages.length === 1, 'did not get messages') - await alixGroupConversation?.send({ text: `first message` }) - await alixDmConversation?.send({ text: `first message` }) + const message = messages[0] + const messageContent = message.content() assert( - conversationMessageCallbacks === 1, - 'conversation stream should have received 1 conversation' - ) - assert( - dmMessageCallbacks === 1, - 'message stream should have received 1 message' + typeof messageContent === 'object' && + 'topNumber' in messageContent && + messageContent.topNumber.bottomNumber === 12, + 'did not get content properly: ' + JSON.stringify(messageContent) ) return true }) -test('can stream all groups and conversations', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const containers: Conversation[] = [] - await alixClient.conversations.stream( - async (conversation: Conversation) => { - containers.push(conversation) - } - ) - - await boClient.conversations.newGroup([alixClient.address]) - await delayToPropogate() - if ((containers.length as number) !== 1) { - throw Error( - 'Unexpected num conversations (should be 1): ' + containers.length - ) - } - - await boClient.conversations.findOrCreateDm(alixClient.address) - await delayToPropogate() - if ((containers.length as number) !== 2) { - throw Error( - 'Unexpected num conversations (should be 2): ' + containers.length - ) - } - - await alixClient.conversations.findOrCreateDm(caroClient.address) - await delayToPropogate() - if (containers.length !== 3) { - throw Error( - 'Expected conversations length 3 but it is: ' + containers.length - ) - } - - alixClient.conversations.cancelStream() - await delayToPropogate() - - await caroClient.conversations.newGroup([alixClient.address]) - await delayToPropogate() - if ((containers.length as number) !== 3) { - throw Error( - 'Unexpected num conversations (should be 3): ' + containers.length - ) - } - - return true -}) - -test('can streamAll from multiple clients', async () => { - const [alix, bo, caro] = await createClients(3) - - // Setup stream alls - const allBoConversations: any[] = [] - const allAliConversations: any[] = [] - - await bo.conversations.stream(async (conversation) => { - allBoConversations.push(conversation) +test('handle fallback types appropriately', async () => { + const keyBytes = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + const bob = await Client.createRandom({ + env: 'local', + codecs: [ + new NumberCodecEmptyFallback(), + new NumberCodecUndefinedFallback(), + ], + dbEncryptionKey: keyBytes, }) - await alix.conversations.stream(async (conversation) => { - allAliConversations.push(conversation) + const alice = await Client.createRandom({ + env: 'local', + dbEncryptionKey: keyBytes, }) + bob.register(new NumberCodecEmptyFallback()) + bob.register(new NumberCodecUndefinedFallback()) + const bobConvo = await bob.conversations.newConversation(alice.address) + const aliceConvo = await alice.conversations.newConversation(bob.address) - // Start Caro starts a new conversation. - await caro.conversations.newConversation(alix.address) - await delayToPropogate() - if (allBoConversations.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + - allBoConversations.length + - ' and Alix had ' + - allAliConversations.length - ) - } - if (allAliConversations.length !== 1) { - throw Error( - 'Unexpected all conversations count ' + allAliConversations.length - ) - } - return true -}) - -test('can streamAll from multiple clients - swapped orderring', async () => { - const [alix, bo, caro] = await createClients(3) - - // Setup stream alls - const allBoConversations: any[] = [] - const allAliConversations: any[] = [] + await bobConvo.send(12, { contentType: ContentTypeNumberWithEmptyFallback }) - await alix.conversations.stream(async (conversation) => { - allAliConversations.push(conversation) + await bobConvo.send(12, { + contentType: ContentTypeNumberWithUndefinedFallback, }) - await bo.conversations.stream(async (conversation) => { - allBoConversations.push(conversation) - }) + const messages = await aliceConvo.messages() + assert(messages.length === 2, 'did not get messages') - // Start Caro starts a new conversation. - await caro.conversations.newConversation(alix.address) - await delayToPropogate() - if (allBoConversations.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + - allBoConversations.length + - ' and Alix had ' + - allAliConversations.length - ) - } - if (allAliConversations.length !== 1) { - throw Error( - 'Unexpected all conversations count ' + allAliConversations.length - ) - } - return true -}) - -test('can streamAllMessages from multiple clients', async () => { - const [alix, bo, caro] = await createClients(3) + const messageUndefinedFallback = messages[0] + const messageWithDefinedFallback = messages[1] - // Setup stream - const allBoMessages: any[] = [] - const allAliMessages: any[] = [] - - await bo.conversations.streamAllMessages(async (conversation) => { - allBoMessages.push(conversation) - }) - await alix.conversations.streamAllMessages(async (conversation) => { - allAliMessages.push(conversation) - }) - - // Start Caro starts a new conversation. - const caroConversation = await caro.conversations.newConversation( - alix.address - ) - await caroConversation.send({ text: `Message` }) - await delayToPropogate() - if (allBoMessages.length !== 0) { - throw Error('Unexpected all messages count for Bo ' + allBoMessages.length) - } - - if (allAliMessages.length !== 1) { - throw Error( - 'Unexpected all conversations count for Ali ' + allAliMessages.length - ) + let message1Content = undefined + try { + message1Content = messageUndefinedFallback.content() + } catch { + message1Content = messageUndefinedFallback.fallback } - return true -}) - -test('can streamAllMessages from multiple clients - swapped', async () => { - const [alix, bo, caro] = await createClients(3) - - // Setup stream - const allBoMessages: any[] = [] - const allAliMessages: any[] = [] - const caroGroup = await caro.conversations.newGroup([alix.address]) - - await alix.conversations.streamAllMessages(async (conversation) => { - allAliMessages.push(conversation) - }) - await bo.conversations.streamAllMessages(async (conversation) => { - allBoMessages.push(conversation) - }) + assert( + message1Content === undefined, + 'did not get content properly when empty fallback: ' + + JSON.stringify(message1Content) + ) - // Start Caro starts a new conversation. - const caroConvo = await caro.conversations.newConversation(alix.address) - await delayToPropogate() - await caroConvo.send({ text: `Message` }) - await caroGroup.send({ text: `Message` }) - await delayToPropogate() - if (allBoMessages.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + allBoMessages.length - ) + let message2Content = undefined + try { + message2Content = messageWithDefinedFallback.content() + } catch { + message2Content = messageWithDefinedFallback.fallback } - if (allAliMessages.length !== 2) { - throw Error( - 'Unexpected all conversations count for Ali ' + allAliMessages.length - ) - } + assert( + message2Content === '', + 'did not get content properly: ' + JSON.stringify(message2Content) + ) return true }) + +// test('can find a conversations by id', async () => { +// const [alixClient, boClient] = await createClients(2) +// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + +// await boClient.conversations.sync() +// const boGroup = await boClient.conversations.findConversation(alixGroup.id) +// const boDm = await boClient.conversations.findConversation(alixDm.id) +// const boDm2 = await boClient.conversations.findConversation( +// 'GARBAGE' as ConversationId +// ) + +// assert(boDm2 === undefined, `bodm2 should be undefined`) + +// assert( +// boGroup?.id === alixGroup.id, +// `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` +// ) + +// assert( +// boDm?.id === alixDm.id, +// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` +// ) + +// return true +// }) + +// test('can find a conversation by topic', async () => { +// const [alixClient, boClient] = await createClients(2) +// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + +// await boClient.conversations.sync() +// const boGroup = await boClient.conversations.findConversationByTopic( +// alixGroup.topic +// ) +// const boDm = await boClient.conversations.findConversationByTopic( +// alixDm.topic +// ) + +// assert( +// boGroup?.id === alixGroup.id, +// `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` +// ) + +// assert( +// boDm?.id === alixDm.id, +// `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` +// ) + +// return true +// }) + +// test('can find a dm by inbox id', async () => { +// const [alixClient, boClient] = await createClients(2) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + +// await boClient.conversations.sync() +// const boDm = await boClient.conversations.findDmByInboxId(alixClient.inboxId) + +// assert( +// boDm?.id === alixDm.id, +// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` +// ) + +// return true +// }) + +// test('can find a dm by address', async () => { +// const [alixClient, boClient] = await createClients(2) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + +// await boClient.conversations.sync() +// const boDm = await boClient.conversations.findDmByAddress(alixClient.address) + +// assert( +// boDm?.id === alixDm.id, +// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` +// ) + +// return true +// }) + +// test('can list conversations with params', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) +// const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) +// const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) +// const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) + +// await boGroup1.send({ text: `first message` }) +// await boGroup1.send({ text: `second message` }) +// await boGroup1.send({ text: `third message` }) +// await boDm2.send({ text: `third message` }) +// await boGroup2.send({ text: `first message` }) +// await boDm1.send({ text: `dm message` }) +// // Order should be [Dm1, Group2, Dm2, Group1] + +// await boClient.conversations.syncAllConversations() +// const boConvosOrderCreated = await boClient.conversations.list() +// const boConvosOrderLastMessage = await boClient.conversations.list( +// { lastMessage: true }, +// 'lastMessage' +// ) +// const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) + +// assert( +// boConvosOrderCreated.map((group: any) => group.id).toString() === +// [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), +// `Conversation created at order should be ${[ +// boGroup1.id, +// boGroup2.id, +// boDm1.id, +// boDm2.id, +// ].toString()} but was ${boConvosOrderCreated +// .map((group: any) => group.id) +// .toString()}` +// ) + +// assert( +// boConvosOrderLastMessage.map((group: any) => group.id).toString() === +// [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), +// `Conversation last message order should be ${[ +// boDm1.id, +// boGroup2.id, +// boDm2.id, +// boGroup1.id, +// ].toString()} but was ${boConvosOrderLastMessage +// .map((group: any) => group.id) +// .toString()}` +// ) + +// const messages = await boConvosOrderLastMessage[0].messages() +// assert( +// messages[0].content() === 'dm message', +// `last message 1 should be dm message ${messages[0].content()}` +// ) +// // assert( +// // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', +// // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` +// // ) +// assert( +// boGroupsLimit.length === 1, +// `List length should be 1 but was ${boGroupsLimit.length}` +// ) +// assert( +// boGroupsLimit[0].id === boGroup1.id, +// `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` +// ) + +// return true +// }) + +// test('can list groups', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const boGroup = await boClient.conversations.newGroup([alixClient.address]) +// await boClient.conversations.newGroup([ +// caroClient.address, +// alixClient.address, +// ]) +// const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) +// await boClient.conversations.findOrCreateDm(alixClient.address) + +// const boConversations = await boClient.conversations.list() +// await alixClient.conversations.sync() +// const alixConversations = await alixClient.conversations.list() + +// assert( +// boConversations.length === 4, +// `bo conversation lengths should be 4 but was ${boConversations.length}` +// ) + +// assert( +// alixConversations.length === 3, +// `alix conversation lengths should be 3 but was ${alixConversations.length}` +// ) + +// if ( +// boConversations[0].topic !== boGroup.topic || +// boConversations[0].version !== ConversationVersion.GROUP || +// boConversations[2].version !== ConversationVersion.DM || +// boConversations[2].createdAt !== boDm.createdAt +// ) { +// throw Error('Listed containers should match streamed containers') +// } + +// return true +// }) + +// test('can list conversation messages', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const boGroup = await boClient.conversations.newGroup([alixClient.address]) +// const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) +// const boGroupConversation = await boClient.conversations.findConversation( +// boGroup.id +// ) +// const boDmConversation = await boClient.conversations.findConversation( +// boDm.id +// ) + +// await boGroupConversation?.send('hello') +// await boGroupConversation?.send('hello') +// await boDmConversation?.send('hello') +// await boDmConversation?.send('hello') + +// const boGroupMessages = await boGroupConversation?.messages() +// const boDmMessages = await boDmConversation?.messages() + +// assert( +// boGroupMessages?.length === 3, +// `bo conversation lengths should be 4 but was ${boGroupMessages?.length}` +// ) + +// assert( +// boDmMessages?.length === 3, +// `alix conversation lengths should be 3 but was ${boDmMessages?.length}` +// ) + +// return true +// }) + +// test('can stream both conversations and messages at same time', async () => { +// const [alix, bo] = await createClients(2) + +// let conversationCallbacks = 0 +// let messageCallbacks = 0 +// await bo.conversations.stream(async () => { +// conversationCallbacks++ +// }) + +// await bo.conversations.streamAllMessages(async () => { +// messageCallbacks++ +// }) + +// const group = await alix.conversations.newGroup([bo.address]) +// const dm = await alix.conversations.findOrCreateDm(bo.address) +// await delayToPropogate() +// await group.send('hello') +// await dm.send('hello') +// await delayToPropogate() + +// assert( +// conversationCallbacks === 2, +// 'conversation stream should have received 2 conversation' +// ) +// assert( +// messageCallbacks === 2, +// 'message stream should have received 2 message' +// ) + +// return true +// }) + +// test('can stream conversation messages', async () => { +// const [alixClient, boClient] = await createClients(2) + +// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) +// const alixGroupConversation = await alixClient.conversations.findConversation( +// alixGroup.id +// ) +// const alixDmConversation = await alixClient.conversations.findConversation( +// alixDm.id +// ) + +// let dmMessageCallbacks = 0 +// let conversationMessageCallbacks = 0 +// await alixGroupConversation?.streamMessages(async () => { +// conversationMessageCallbacks++ +// }) + +// await alixDmConversation?.streamMessages(async () => { +// dmMessageCallbacks++ +// }) + +// await alixGroupConversation?.send({ text: `first message` }) +// await alixDmConversation?.send({ text: `first message` }) + +// assert( +// conversationMessageCallbacks === 1, +// 'conversation stream should have received 1 conversation' +// ) +// assert( +// dmMessageCallbacks === 1, +// 'message stream should have received 1 message' +// ) + +// return true +// }) + +// test('can stream all groups and conversations', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const containers: Conversation[] = [] +// await alixClient.conversations.stream( +// async (conversation: Conversation) => { +// containers.push(conversation) +// } +// ) + +// await boClient.conversations.newGroup([alixClient.address]) +// await delayToPropogate() +// if ((containers.length as number) !== 1) { +// throw Error( +// 'Unexpected num conversations (should be 1): ' + containers.length +// ) +// } + +// await boClient.conversations.findOrCreateDm(alixClient.address) +// await delayToPropogate() +// if ((containers.length as number) !== 2) { +// throw Error( +// 'Unexpected num conversations (should be 2): ' + containers.length +// ) +// } + +// await alixClient.conversations.findOrCreateDm(caroClient.address) +// await delayToPropogate() +// if (containers.length !== 3) { +// throw Error( +// 'Expected conversations length 3 but it is: ' + containers.length +// ) +// } + +// alixClient.conversations.cancelStream() +// await delayToPropogate() + +// await caroClient.conversations.newGroup([alixClient.address]) +// await delayToPropogate() +// if ((containers.length as number) !== 3) { +// throw Error( +// 'Unexpected num conversations (should be 3): ' + containers.length +// ) +// } + +// return true +// }) + +// test('can streamAll from multiple clients', async () => { +// const [alix, bo, caro] = await createClients(3) + +// // Setup stream alls +// const allBoConversations: any[] = [] +// const allAliConversations: any[] = [] + +// await bo.conversations.stream(async (conversation) => { +// allBoConversations.push(conversation) +// }) +// await alix.conversations.stream(async (conversation) => { +// allAliConversations.push(conversation) +// }) + +// // Start Caro starts a new conversation. +// await caro.conversations.newConversation(alix.address) +// await delayToPropogate() +// if (allBoConversations.length !== 0) { +// throw Error( +// 'Unexpected all conversations count for Bo ' + +// allBoConversations.length + +// ' and Alix had ' + +// allAliConversations.length +// ) +// } +// if (allAliConversations.length !== 1) { +// throw Error( +// 'Unexpected all conversations count ' + allAliConversations.length +// ) +// } +// return true +// }) + +// test('can streamAll from multiple clients - swapped orderring', async () => { +// const [alix, bo, caro] = await createClients(3) + +// // Setup stream alls +// const allBoConversations: any[] = [] +// const allAliConversations: any[] = [] + +// await alix.conversations.stream(async (conversation) => { +// allAliConversations.push(conversation) +// }) + +// await bo.conversations.stream(async (conversation) => { +// allBoConversations.push(conversation) +// }) + +// // Start Caro starts a new conversation. +// await caro.conversations.newConversation(alix.address) +// await delayToPropogate() +// if (allBoConversations.length !== 0) { +// throw Error( +// 'Unexpected all conversations count for Bo ' + +// allBoConversations.length + +// ' and Alix had ' + +// allAliConversations.length +// ) +// } +// if (allAliConversations.length !== 1) { +// throw Error( +// 'Unexpected all conversations count ' + allAliConversations.length +// ) +// } +// return true +// }) + +// test('can streamAllMessages from multiple clients', async () => { +// const [alix, bo, caro] = await createClients(3) + +// // Setup stream +// const allBoMessages: any[] = [] +// const allAliMessages: any[] = [] + +// await bo.conversations.streamAllMessages(async (conversation) => { +// allBoMessages.push(conversation) +// }) +// await alix.conversations.streamAllMessages(async (conversation) => { +// allAliMessages.push(conversation) +// }) + +// // Start Caro starts a new conversation. +// const caroConversation = await caro.conversations.newConversation( +// alix.address +// ) +// await caroConversation.send({ text: `Message` }) +// await delayToPropogate() +// if (allBoMessages.length !== 0) { +// throw Error('Unexpected all messages count for Bo ' + allBoMessages.length) +// } + +// if (allAliMessages.length !== 1) { +// throw Error( +// 'Unexpected all conversations count for Ali ' + allAliMessages.length +// ) +// } + +// return true +// }) + +// test('can streamAllMessages from multiple clients - swapped', async () => { +// const [alix, bo, caro] = await createClients(3) + +// // Setup stream +// const allBoMessages: any[] = [] +// const allAliMessages: any[] = [] +// const caroGroup = await caro.conversations.newGroup([alix.address]) + +// await alix.conversations.streamAllMessages(async (conversation) => { +// allAliMessages.push(conversation) +// }) +// await bo.conversations.streamAllMessages(async (conversation) => { +// allBoMessages.push(conversation) +// }) + +// // Start Caro starts a new conversation. +// const caroConvo = await caro.conversations.newConversation(alix.address) +// await delayToPropogate() +// await caroConvo.send({ text: `Message` }) +// await caroGroup.send({ text: `Message` }) +// await delayToPropogate() +// if (allBoMessages.length !== 0) { +// throw Error( +// 'Unexpected all conversations count for Bo ' + allBoMessages.length +// ) +// } + +// if (allAliMessages.length !== 2) { +// throw Error( +// 'Unexpected all conversations count for Ali ' + allAliMessages.length +// ) +// } + +// return true +// }) diff --git a/src/index.ts b/src/index.ts index 1fae6317b..89772ae92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { ConsentState, } from './lib/ConsentListEntry' import { + ContentCodec, DecryptedLocalAttachment, EncryptedLocalAttachment, } from './lib/ContentCodec' @@ -30,6 +31,7 @@ import { DefaultContentTypes } from './lib/types/DefaultContentType' import { MessageId, MessageOrder } from './lib/types/MessagesOptions' import { PermissionPolicySet } from './lib/types/PermissionPolicySet' import { getAddress } from './utils/address' +import { EncodedContent } from '@xmtp/proto/ts/dist/types/message_contents/content.pb' export * from './context' export * from './hooks' @@ -415,6 +417,28 @@ export async function findDmByAddress< return new Dm(client, dm) } +export async function sendWithContentType( + inboxId: InboxId, + conversationId: ConversationId, + content: T, + codec: ContentCodec +): Promise { + if ('contentKey' in codec) { + const contentJson = JSON.stringify(content) + return await XMTPModule.sendMessage(inboxId, conversationId, contentJson) + } else { + const encodedContent = codec.encode(content) + encodedContent.fallback = codec.fallback(content) + const encodedContentData = EncodedContent.encode(encodedContent).finish() + + return await XMTPModule.sendEncodedContent( + inboxId, + conversationId, + Array.from(encodedContentData) + ) + } +} + export async function sendMessage( inboxId: InboxId, conversationId: ConversationId, diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index 0b8db1a18..b03930c10 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,5 +1,5 @@ import { ConsentState } from './ConsentListEntry' -import { ConversationSendPayload, MessageId, MessagesOptions } from './types' +import { ConversationSendPayload, MessageId, MessagesOptions, SendOptions } from './types' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' import { DecodedMessage, Member, Dm, Group } from '../index' @@ -19,7 +19,8 @@ export interface ConversationBase { lastMessage?: DecodedMessage send( - content: ConversationSendPayload + content: ConversationSendPayload, + opts?: SendOptions ): Promise sync() messages(opts?: MessagesOptions): Promise[]> diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index f78daf860..faa8344fb 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -9,6 +9,7 @@ import { EventTypes } from './types/EventTypes' import { MessageId, MessagesOptions } from './types/MessagesOptions' import * as XMTP from '../index' import { ConversationId, ConversationTopic } from '../index' +import { SendOptions } from './types/SendOptions' export interface DmParams { id: ConversationId @@ -58,12 +59,12 @@ export class Dm * @throws {Error} Throws an error if there is an issue with sending the message. */ async send( - content: ConversationSendPayload + content: ConversationSendPayload, + opts?: SendOptions ): Promise { - // TODO: Enable other content types - // if (opts && opts.contentType) { - // return await this._sendWithJSCodec(content, opts.contentType) - // } + if (opts && opts.contentType) { + return await this._sendWithJSCodec(content, opts.contentType) + } try { if (typeof content === 'string') { @@ -77,6 +78,27 @@ export class Dm } } + private async _sendWithJSCodec( + content: T, + contentType: XMTP.ContentTypeId + ): Promise { + const codec = + this.client.codecRegistry[ + `${contentType.authorityId}/${contentType.typeId}:${contentType.versionMajor}.${contentType.versionMinor}` + ] + + if (!codec) { + throw new Error(`no codec found for: ${contentType}`) + } + + return await XMTP.sendWithContentType( + this.client.inboxId, + this.id, + content, + codec + ) + } + /** * Prepare a dm message to be sent. * diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 1e74f5dc7..44763e335 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -10,6 +10,7 @@ import { MessageId, MessagesOptions } from './types/MessagesOptions' import { PermissionPolicySet } from './types/PermissionPolicySet' import * as XMTP from '../index' import { Address, ConversationId, ConversationTopic } from '../index' +import { SendOptions } from './types/SendOptions' export type PermissionUpdateOption = 'allow' | 'deny' | 'admin' | 'super_admin' @@ -86,12 +87,12 @@ export class Group< * @throws {Error} Throws an error if there is an issue with sending the message. */ async send( - content: ConversationSendPayload + content: ConversationSendPayload, + opts?: SendOptions ): Promise { - // TODO: Enable other content types - // if (opts && opts.contentType) { - // return await this._sendWithJSCodec(content, opts.contentType) - // } + if (opts && opts.contentType) { + return await this._sendWithJSCodec(content, opts.contentType) + } try { if (typeof content === 'string') { @@ -105,6 +106,27 @@ export class Group< } } + private async _sendWithJSCodec( + content: T, + contentType: XMTP.ContentTypeId + ): Promise { + const codec = + this.client.codecRegistry[ + `${contentType.authorityId}/${contentType.typeId}:${contentType.versionMajor}.${contentType.versionMinor}` + ] + + if (!codec) { + throw new Error(`no codec found for: ${contentType}`) + } + + return await XMTP.sendWithContentType( + this.client.inboxId, + this.id, + content, + codec + ) + } + /** * Prepare a group message to be sent. * From 9834035dfb64547bb918d558e287577dc36933c8 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 8 Dec 2024 19:30:59 -0800 Subject: [PATCH 2/8] bring back the tests --- example/src/tests/conversationTests.ts | 1424 ++++++++++++++---------- src/index.ts | 4 +- 2 files changed, 808 insertions(+), 620 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 508e62ad4..2f8288ac7 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,4 +1,6 @@ +import { content } from '@xmtp/proto' import { Wallet } from 'ethers' +import ReactNativeBlobUtil from 'react-native-blob-util' import RNFS from 'react-native-fs' import { Test, assert, createClients, delayToPropogate } from './test-utils' @@ -8,6 +10,7 @@ import { Conversation, ConversationId, ConversationVersion, + JSContentCodec, } from '../../../src/index' export const conversationTests: Test[] = [] @@ -19,685 +22,868 @@ function test(name: string, perform: () => Promise) { }) } -test('can find a conversations by id', async () => { - const [alixClient, boClient] = await createClients(2) - const alixGroup = await alixClient.conversations.newGroup([boClient.address]) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) +type EncodedContent = content.EncodedContent +type ContentTypeId = content.ContentTypeId - await boClient.conversations.sync() - const boGroup = await boClient.conversations.findConversation(alixGroup.id) - const boDm = await boClient.conversations.findConversation(alixDm.id) - const boDm2 = await boClient.conversations.findConversation( - 'GARBAGE' as ConversationId - ) - - assert(boDm2 === undefined, `bodm2 should be undefined`) - - assert( - boGroup?.id === alixGroup.id, - `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` - ) - - assert( - boDm?.id === alixDm.id, - `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` - ) - - return true -}) - -test('can find a conversation by topic', async () => { - const [alixClient, boClient] = await createClients(2) - const alixGroup = await alixClient.conversations.newGroup([boClient.address]) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - - await boClient.conversations.sync() - const boGroup = await boClient.conversations.findConversationByTopic( - alixGroup.topic - ) - const boDm = await boClient.conversations.findConversationByTopic( - alixDm.topic - ) - - assert( - boGroup?.id === alixGroup.id, - `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` - ) - - assert( - boDm?.id === alixDm.id, - `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` - ) - - return true -}) - -test('can find a dm by inbox id', async () => { - const [alixClient, boClient] = await createClients(2) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - - await boClient.conversations.sync() - const boDm = await boClient.conversations.findDmByInboxId(alixClient.inboxId) - - assert( - boDm?.id === alixDm.id, - `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` - ) +const { fs } = ReactNativeBlobUtil - return true -}) - -test('can find a dm by address', async () => { - const [alixClient, boClient] = await createClients(2) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - - await boClient.conversations.sync() - const boDm = await boClient.conversations.findDmByAddress(alixClient.address) - - assert( - boDm?.id === alixDm.id, - `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` - ) - - return true -}) - -test('can filter conversations by consent', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) - const otherGroup = await alixClient.conversations.newGroup([boClient.address]) - const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) - await caroClient.conversations.findOrCreateDm(boClient.address) - await boClient.conversations.sync() - const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) - const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) - - const boConvos = await boClient.conversations.list() - const boConvosFilteredAllowed = await boClient.conversations.list( - {}, - undefined, - undefined, - 'allowed' - ) - const boConvosFilteredUnknown = await boClient.conversations.list( - {}, - undefined, - undefined, - 'unknown' - ) - - assert( - boConvos.length === 4, - `Conversation length should be 4 but was ${boConvos.length}` - ) - - assert( - boConvosFilteredAllowed - .map((conversation: any) => conversation.id) - .toString() === [boGroup1.id, boDm1.id].toString(), - `Conversation allowed should be ${[ - boGroup1.id, - boDm1.id, - ].toString()} but was ${boConvosFilteredAllowed - .map((convo: any) => convo.id) - .toString()}` - ) - - assert( - boConvosFilteredUnknown - .map((conversation: any) => conversation.id) - .toString() === [boGroup2?.id, boDm2?.id].toString(), - `Conversation unknown filter should be ${[ - boGroup2?.id, - boDm2?.id, - ].toString()} but was ${boConvosFilteredUnknown - .map((convo: any) => convo.id) - .toString()}` - ) - - return true -}) - -test('can list conversations with params', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) - const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) - const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) - const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) - - await boGroup1.send({ text: `first message` }) - await boGroup1.send({ text: `second message` }) - await boGroup1.send({ text: `third message` }) - await boDm2.send({ text: `third message` }) - await boGroup2.send({ text: `first message` }) - await boDm1.send({ text: `dm message` }) - // Order should be [Dm1, Group2, Dm2, Group1] - - await boClient.conversations.syncAllConversations() - const boConvosOrderCreated = await boClient.conversations.list() - const boConvosOrderLastMessage = await boClient.conversations.list( - { lastMessage: true }, - 'lastMessage' - ) - const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) - - assert( - boConvosOrderCreated.map((group: any) => group.id).toString() === - [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), - `Conversation created at order should be ${[ - boGroup1.id, - boGroup2.id, - boDm1.id, - boDm2.id, - ].toString()} but was ${boConvosOrderCreated - .map((group: any) => group.id) - .toString()}` - ) - - assert( - boConvosOrderLastMessage.map((group: any) => group.id).toString() === - [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), - `Conversation last message order should be ${[ - boDm1.id, - boGroup2.id, - boDm2.id, - boGroup1.id, - ].toString()} but was ${boConvosOrderLastMessage - .map((group: any) => group.id) - .toString()}` - ) - - const messages = await boConvosOrderLastMessage[0].messages() - assert( - messages[0].content() === 'dm message', - `last message 1 should be dm message ${messages[0].content()}` - ) - // assert( - // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', - // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` - // ) - assert( - boGroupsLimit.length === 1, - `List length should be 1 but was ${boGroupsLimit.length}` - ) - assert( - boGroupsLimit[0].id === boGroup1.id, - `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` - ) - - return true -}) - -test('can list groups', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const boGroup = await boClient.conversations.newGroup([alixClient.address]) - await boClient.conversations.newGroup([ - caroClient.address, - alixClient.address, - ]) - const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) - await boClient.conversations.findOrCreateDm(alixClient.address) - - const boConversations = await boClient.conversations.list() - await alixClient.conversations.sync() - const alixConversations = await alixClient.conversations.list() +const ContentTypeNumber: ContentTypeId = { + authorityId: 'org', + typeId: 'number', + versionMajor: 1, + versionMinor: 0, +} - assert( - boConversations.length === 4, - `bo conversation lengths should be 4 but was ${boConversations.length}` - ) +const ContentTypeNumberWithUndefinedFallback: ContentTypeId = { + authorityId: 'org', + typeId: 'number_undefined_fallback', + versionMajor: 1, + versionMinor: 0, +} - assert( - alixConversations.length === 3, - `alix conversation lengths should be 3 but was ${alixConversations.length}` - ) +const ContentTypeNumberWithEmptyFallback: ContentTypeId = { + authorityId: 'org', + typeId: 'number_empty_fallback', + versionMajor: 1, + versionMinor: 0, +} - if ( - boConversations[0].topic !== boGroup.topic || - boConversations[0].version !== ConversationVersion.GROUP || - boConversations[2].version !== ConversationVersion.DM || - boConversations[2].createdAt !== boDm.createdAt - ) { - throw Error('Listed containers should match streamed containers') +export type NumberRef = { + topNumber: { + bottomNumber: number } +} - return true -}) - -test('can list conversation messages', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const boGroup = await boClient.conversations.newGroup([alixClient.address]) - const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) - const boGroupConversation = await boClient.conversations.findConversation( - boGroup.id - ) - const boDmConversation = await boClient.conversations.findConversation( - boDm.id - ) - - await boGroupConversation?.send('hello') - await boGroupConversation?.send('hello') - await boDmConversation?.send('hello') - await boDmConversation?.send('hello') - - const boGroupMessages = await boGroupConversation?.messages() - const boDmMessages = await boDmConversation?.messages() - - assert( - boGroupMessages?.length === 3, - `bo conversation lengths should be 3 but was ${boGroupMessages?.length}` - ) - - assert( - boDmMessages?.length === 2, - `alix conversation lengths should be 2 but was ${boDmMessages?.length}` - ) - - return true -}) - -test('can stream both conversations and messages at same time', async () => { - const [alix, bo] = await createClients(2) - - let conversationCallbacks = 0 - let messageCallbacks = 0 - await bo.conversations.stream(async () => { - conversationCallbacks++ - }) - - await bo.conversations.streamAllMessages(async () => { - messageCallbacks++ - }) - - const group = await alix.conversations.newGroup([bo.address]) - const dm = await alix.conversations.findOrCreateDm(bo.address) - await delayToPropogate() - await group.send('hello') - await dm.send('hello') - await delayToPropogate() - - assert( - conversationCallbacks === 2, - 'conversation stream should have received 2 conversation' - ) - assert( - messageCallbacks === 2, - 'message stream should have received 2 message' - ) - - return true -}) - -test('can stream conversation messages', async () => { - const [alixClient, boClient] = await createClients(2) - - const alixGroup = await alixClient.conversations.newGroup([boClient.address]) - const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - const alixGroupConversation = await alixClient.conversations.findConversation( - alixGroup.id - ) - const alixDmConversation = await alixClient.conversations.findConversation( - alixDm.id - ) - - let dmMessageCallbacks = 0 - let conversationMessageCallbacks = 0 - await alixGroupConversation?.streamMessages(async () => { - conversationMessageCallbacks++ - }) - - await alixDmConversation?.streamMessages(async () => { - dmMessageCallbacks++ - }) - - await alixGroupConversation?.send({ text: `first message` }) - await alixDmConversation?.send({ text: `first message` }) - - assert( - conversationMessageCallbacks === 1, - 'conversation stream should have received 1 conversation' - ) - assert( - dmMessageCallbacks === 1, - 'message stream should have received 1 message' - ) - - return true -}) - -test('can stream all groups and conversations', async () => { - const [alixClient, boClient, caroClient] = await createClients(3) - - const containers: Conversation[] = [] - await alixClient.conversations.stream( - async (conversation: Conversation) => { - containers.push(conversation) +class NumberCodec implements JSContentCodec { + contentType = ContentTypeNumber + + // a completely absurd way of encoding number values + encode(content: NumberRef): EncodedContent { + return { + type: ContentTypeNumber, + parameters: { + test: 'test', + }, + content: new TextEncoder().encode(JSON.stringify(content)), } - ) - - await boClient.conversations.newGroup([alixClient.address]) - await delayToPropogate() - if ((containers.length as number) !== 1) { - throw Error( - 'Unexpected num conversations (should be 1): ' + containers.length - ) - } - - await boClient.conversations.findOrCreateDm(alixClient.address) - await delayToPropogate() - if ((containers.length as number) !== 2) { - throw Error( - 'Unexpected num conversations (should be 2): ' + containers.length - ) - } - - await alixClient.conversations.findOrCreateDm(caroClient.address) - await delayToPropogate() - if (containers.length !== 3) { - throw Error( - 'Expected conversations length 3 but it is: ' + containers.length - ) - } - - alixClient.conversations.cancelStream() - await delayToPropogate() - - await caroClient.conversations.newGroup([alixClient.address]) - await delayToPropogate() - if ((containers.length as number) !== 3) { - throw Error( - 'Unexpected num conversations (should be 3): ' + containers.length - ) - } - - return true -}) - -test('can streamAll from multiple clients', async () => { - const [alix, bo, caro] = await createClients(3) - - // Setup stream alls - const allBoConversations: any[] = [] - const allAliConversations: any[] = [] - - await bo.conversations.stream(async (conversation) => { - allBoConversations.push(conversation) - }) - await alix.conversations.stream(async (conversation) => { - allAliConversations.push(conversation) - }) - - // Start Caro starts a new conversation. - await caro.conversations.newConversation(alix.address) - await delayToPropogate() - if (allBoConversations.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + - allBoConversations.length + - ' and Alix had ' + - allAliConversations.length - ) - } - if (allAliConversations.length !== 1) { - throw Error( - 'Unexpected all conversations count ' + allAliConversations.length - ) - } - return true -}) - -test('can streamAll from multiple clients - swapped orderring', async () => { - const [alix, bo, caro] = await createClients(3) - - // Setup stream alls - const allBoConversations: any[] = [] - const allAliConversations: any[] = [] - - await alix.conversations.stream(async (conversation) => { - allAliConversations.push(conversation) - }) - - await bo.conversations.stream(async (conversation) => { - allBoConversations.push(conversation) - }) - - // Start Caro starts a new conversation. - await caro.conversations.newConversation(alix.address) - await delayToPropogate() - if (allBoConversations.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + - allBoConversations.length + - ' and Alix had ' + - allAliConversations.length - ) - } - if (allAliConversations.length !== 1) { - throw Error( - 'Unexpected all conversations count ' + allAliConversations.length - ) } - return true -}) -test('can streamAllMessages from multiple clients', async () => { - const [alix, bo, caro] = await createClients(3) - - // Setup stream - const allBoMessages: any[] = [] - const allAliMessages: any[] = [] - - await bo.conversations.streamAllMessages(async (conversation) => { - allBoMessages.push(conversation) - }) - await alix.conversations.streamAllMessages(async (conversation) => { - allAliMessages.push(conversation) - }) - - // Start Caro starts a new conversation. - const caroConversation = await caro.conversations.newConversation( - alix.address - ) - await caroConversation.send({ text: `Message` }) - await delayToPropogate() - if (allBoMessages.length !== 0) { - throw Error('Unexpected all messages count for Bo ' + allBoMessages.length) + decode(encodedContent: EncodedContent): NumberRef { + if (encodedContent.parameters.test !== 'test') { + throw new Error(`parameters should parse ${encodedContent.parameters}`) + } + const contentReceived = JSON.parse( + new TextDecoder().decode(encodedContent.content) + ) as NumberRef + return contentReceived } - if (allAliMessages.length !== 1) { - throw Error( - 'Unexpected all conversations count for Ali ' + allAliMessages.length - ) + fallback(content: NumberRef): string | undefined { + return 'a billion' } +} - return true -}) - -test('can streamAllMessages from multiple clients - swapped', async () => { - const [alix, bo, caro] = await createClients(3) - - // Setup stream - const allBoMessages: any[] = [] - const allAliMessages: any[] = [] - const caroGroup = await caro.conversations.newGroup([alix.address]) - - await alix.conversations.streamAllMessages(async (conversation) => { - allAliMessages.push(conversation) - }) - await bo.conversations.streamAllMessages(async (conversation) => { - allBoMessages.push(conversation) - }) - - // Start Caro starts a new conversation. - const caroConvo = await caro.conversations.newConversation(alix.address) - await delayToPropogate() - await caroConvo.send({ text: `Message` }) - await caroGroup.send({ text: `Message` }) - await delayToPropogate() - if (allBoMessages.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + allBoMessages.length - ) +class NumberCodecUndefinedFallback extends NumberCodec { + contentType = ContentTypeNumberWithUndefinedFallback + fallback(content: NumberRef): string | undefined { + return undefined } +} - if (allAliMessages.length !== 2) { - throw Error( - 'Unexpected all conversations count for Ali ' + allAliMessages.length - ) +class NumberCodecEmptyFallback extends NumberCodec { + contentType = ContentTypeNumberWithEmptyFallback + fallback(content: NumberRef): string | undefined { + return '' } +} - return true -}) - -test('can sync consent', async () => { - const [bo] = await createClients(1) +test('register and use custom content types', async () => { const keyBytes = new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, ]) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` - const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` - const directoryExists = await RNFS.exists(dbDirPath) - if (!directoryExists) { - await RNFS.mkdir(dbDirPath) - } - const directoryExists2 = await RNFS.exists(dbDirPath2) - if (!directoryExists2) { - await RNFS.mkdir(dbDirPath2) - } - const alixWallet = Wallet.createRandom() - - const alix = await Client.create(alixWallet, { + const bob = await Client.createRandom({ env: 'local', - appVersion: 'Testing/0.0.0', + codecs: [new NumberCodec()], dbEncryptionKey: keyBytes, - dbDirectory: dbDirPath, }) - - // Create DM conversation - const dm = await alix.conversations.findOrCreateDm(bo.address) - await dm.updateConsent('denied') - const consentState = await dm.consentState() - assert(consentState === 'denied', `Expected 'denied', got ${consentState}`) - - await bo.conversations.sync() - const boDm = await bo.conversations.findConversation(dm.id) - - const alix2 = await Client.create(alixWallet, { + const alice = await Client.createRandom({ env: 'local', - appVersion: 'Testing/0.0.0', + codecs: [new NumberCodec()], dbEncryptionKey: keyBytes, - dbDirectory: dbDirPath2, }) - const state = await alix2.inboxState(true) - assert( - state.installations.length === 2, - `Expected 2 installations, got ${state.installations.length}` - ) + bob.register(new NumberCodec()) + alice.register(new NumberCodec()) - // Sync conversations - await bo.conversations.sync() - if (boDm) await boDm.sync() - await alix2.preferences.syncConsent() - await alix.conversations.syncAllConversations() - await delayToPropogate(2000) - await alix2.conversations.syncAllConversations() - await delayToPropogate(2000) - - const dm2 = await alix2.conversations.findConversation(dm.id) - const consentState2 = await dm2?.consentState() - assert(consentState2 === 'denied', `Expected 'denied', got ${consentState2}`) - - await alix2.preferences.setConsentState( - new ConsentRecord(dm2!.id, 'conversation_id', 'allowed') + await delayToPropogate() + + const bobConvo = await bob.conversations.newConversation(alice.address) + await delayToPropogate() + const aliceConvo = await alice.conversations.newConversation(bob.address) + + await bobConvo.send( + { topNumber: { bottomNumber: 12 } }, + { contentType: ContentTypeNumber } ) - const convoState = await alix2.preferences.conversationConsentState(dm2!.id) - assert(convoState === 'allowed', `Expected 'allowed', got ${convoState}`) + const messages = await aliceConvo.messages() + assert(messages.length === 1, 'did not get messages') + + const message = messages[0] + const messageContent = message.content() - const updatedConsentState = await dm2?.consentState() assert( - updatedConsentState === 'allowed', - `Expected 'allowed', got ${updatedConsentState}` + typeof messageContent === 'object' && + 'topNumber' in messageContent && + messageContent.topNumber.bottomNumber === 12, + 'did not get content properly: ' + JSON.stringify(messageContent) ) return true }) -test('can stream consent', async () => { - const [bo] = await createClients(1) +test('handle fallback types appropriately', async () => { const keyBytes = new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, ]) - const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` - const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` - - // Ensure the directories exist - if (!(await RNFS.exists(dbDirPath))) { - await RNFS.mkdir(dbDirPath) - } - if (!(await RNFS.exists(dbDirPath2))) { - await RNFS.mkdir(dbDirPath2) - } - - const alixWallet = Wallet.createRandom() - - const alix = await Client.create(alixWallet, { + const bob = await Client.createRandom({ env: 'local', - appVersion: 'Testing/0.0.0', + codecs: [ + new NumberCodecEmptyFallback(), + new NumberCodecUndefinedFallback(), + ], dbEncryptionKey: keyBytes, - dbDirectory: dbDirPath, }) - - const alixGroup = await alix.conversations.newGroup([bo.address]) - - const alix2 = await Client.create(alixWallet, { + const alice = await Client.createRandom({ env: 'local', - appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, - dbDirectory: dbDirPath2, }) + bob.register(new NumberCodecEmptyFallback()) + bob.register(new NumberCodecUndefinedFallback()) + const bobConvo = await bob.conversations.newConversation(alice.address) + const aliceConvo = await alice.conversations.newConversation(bob.address) - await alixGroup.send('Hello') - await alix.conversations.syncAllConversations() - await alix2.conversations.syncAllConversations() - - const alix2Group = await alix2.conversations.findConversation(alixGroup.id) - await delayToPropogate() + await bobConvo.send(12, { contentType: ContentTypeNumberWithEmptyFallback }) - const consent = [] - await alix.preferences.streamConsent(async (entry: ConsentRecord) => { - consent.push(entry) + await bobConvo.send(12, { + contentType: ContentTypeNumberWithUndefinedFallback, }) - await delayToPropogate() + const messages = await aliceConvo.messages() + assert(messages.length === 2, 'did not get messages') - await alix2Group!.updateConsent('denied') - const dm = await alix2.conversations.newConversation(bo.address) - await dm!.updateConsent('denied') + const messageUndefinedFallback = messages[0] + const messageWithDefinedFallback = messages[1] - await delayToPropogate(3000) - await alix.conversations.syncAllConversations() - await alix2.conversations.syncAllConversations() + let message1Content = undefined + try { + message1Content = messageUndefinedFallback.content() + } catch { + message1Content = messageUndefinedFallback.fallback + } assert( - consent.length === 4, - `Expected 4 consent records, got ${consent.length}` + message1Content === undefined, + 'did not get content properly when empty fallback: ' + + JSON.stringify(message1Content) ) - const updatedConsentState = await alixGroup.consentState() + + let message2Content = undefined + try { + message2Content = messageWithDefinedFallback.content() + } catch { + message2Content = messageWithDefinedFallback.fallback + } + assert( - updatedConsentState === 'denied', - `Expected 'denied', got ${updatedConsentState}` + message2Content === '', + 'did not get content properly: ' + JSON.stringify(message2Content) ) - alix.preferences.cancelStreamConsent() - return true }) + +// test('can find a conversations by id', async () => { +// const [alixClient, boClient] = await createClients(2) +// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + +// await boClient.conversations.sync() +// const boGroup = await boClient.conversations.findConversation(alixGroup.id) +// const boDm = await boClient.conversations.findConversation(alixDm.id) +// const boDm2 = await boClient.conversations.findConversation( +// 'GARBAGE' as ConversationId +// ) + +// assert(boDm2 === undefined, `bodm2 should be undefined`) + +// assert( +// boGroup?.id === alixGroup.id, +// `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` +// ) + +// assert( +// boDm?.id === alixDm.id, +// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` +// ) + +// return true +// }) + +// test('can find a conversation by topic', async () => { +// const [alixClient, boClient] = await createClients(2) +// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + +// await boClient.conversations.sync() +// const boGroup = await boClient.conversations.findConversationByTopic( +// alixGroup.topic +// ) +// const boDm = await boClient.conversations.findConversationByTopic( +// alixDm.topic +// ) + +// assert( +// boGroup?.id === alixGroup.id, +// `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` +// ) + +// assert( +// boDm?.id === alixDm.id, +// `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` +// ) + +// return true +// }) + +// test('can find a dm by inbox id', async () => { +// const [alixClient, boClient] = await createClients(2) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + +// await boClient.conversations.sync() +// const boDm = await boClient.conversations.findDmByInboxId(alixClient.inboxId) + +// assert( +// boDm?.id === alixDm.id, +// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` +// ) + +// return true +// }) + +// test('can find a dm by address', async () => { +// const [alixClient, boClient] = await createClients(2) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + +// await boClient.conversations.sync() +// const boDm = await boClient.conversations.findDmByAddress(alixClient.address) + +// assert( +// boDm?.id === alixDm.id, +// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` +// ) + +// return true +// }) + +// test('can filter conversations by consent', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) +// const otherGroup = await alixClient.conversations.newGroup([boClient.address]) +// const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) +// await caroClient.conversations.findOrCreateDm(boClient.address) +// await boClient.conversations.sync() +// const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) +// const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) + +// const boConvos = await boClient.conversations.list() +// const boConvosFilteredAllowed = await boClient.conversations.list( +// {}, +// undefined, +// undefined, +// 'allowed' +// ) +// const boConvosFilteredUnknown = await boClient.conversations.list( +// {}, +// undefined, +// undefined, +// 'unknown' +// ) + +// assert( +// boConvos.length === 4, +// `Conversation length should be 4 but was ${boConvos.length}` +// ) + +// assert( +// boConvosFilteredAllowed +// .map((conversation: any) => conversation.id) +// .toString() === [boGroup1.id, boDm1.id].toString(), +// `Conversation allowed should be ${[ +// boGroup1.id, +// boDm1.id, +// ].toString()} but was ${boConvosFilteredAllowed +// .map((convo: any) => convo.id) +// .toString()}` +// ) + +// assert( +// boConvosFilteredUnknown +// .map((conversation: any) => conversation.id) +// .toString() === [boGroup2?.id, boDm2?.id].toString(), +// `Conversation unknown filter should be ${[ +// boGroup2?.id, +// boDm2?.id, +// ].toString()} but was ${boConvosFilteredUnknown +// .map((convo: any) => convo.id) +// .toString()}` +// ) + +// return true +// }) + +// test('can list conversations with params', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) +// const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) +// const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) +// const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) + +// await boGroup1.send({ text: `first message` }) +// await boGroup1.send({ text: `second message` }) +// await boGroup1.send({ text: `third message` }) +// await boDm2.send({ text: `third message` }) +// await boGroup2.send({ text: `first message` }) +// await boDm1.send({ text: `dm message` }) +// // Order should be [Dm1, Group2, Dm2, Group1] + +// await boClient.conversations.syncAllConversations() +// const boConvosOrderCreated = await boClient.conversations.list() +// const boConvosOrderLastMessage = await boClient.conversations.list( +// { lastMessage: true }, +// 'lastMessage' +// ) +// const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) + +// assert( +// boConvosOrderCreated.map((group: any) => group.id).toString() === +// [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), +// `Conversation created at order should be ${[ +// boGroup1.id, +// boGroup2.id, +// boDm1.id, +// boDm2.id, +// ].toString()} but was ${boConvosOrderCreated +// .map((group: any) => group.id) +// .toString()}` +// ) + +// assert( +// boConvosOrderLastMessage.map((group: any) => group.id).toString() === +// [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), +// `Conversation last message order should be ${[ +// boDm1.id, +// boGroup2.id, +// boDm2.id, +// boGroup1.id, +// ].toString()} but was ${boConvosOrderLastMessage +// .map((group: any) => group.id) +// .toString()}` +// ) + +// const messages = await boConvosOrderLastMessage[0].messages() +// assert( +// messages[0].content() === 'dm message', +// `last message 1 should be dm message ${messages[0].content()}` +// ) +// // assert( +// // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', +// // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` +// // ) +// assert( +// boGroupsLimit.length === 1, +// `List length should be 1 but was ${boGroupsLimit.length}` +// ) +// assert( +// boGroupsLimit[0].id === boGroup1.id, +// `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` +// ) + +// return true +// }) + +// test('can list groups', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const boGroup = await boClient.conversations.newGroup([alixClient.address]) +// await boClient.conversations.newGroup([ +// caroClient.address, +// alixClient.address, +// ]) +// const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) +// await boClient.conversations.findOrCreateDm(alixClient.address) + +// const boConversations = await boClient.conversations.list() +// await alixClient.conversations.sync() +// const alixConversations = await alixClient.conversations.list() + +// assert( +// boConversations.length === 4, +// `bo conversation lengths should be 4 but was ${boConversations.length}` +// ) + +// assert( +// alixConversations.length === 3, +// `alix conversation lengths should be 3 but was ${alixConversations.length}` +// ) + +// if ( +// boConversations[0].topic !== boGroup.topic || +// boConversations[0].version !== ConversationVersion.GROUP || +// boConversations[2].version !== ConversationVersion.DM || +// boConversations[2].createdAt !== boDm.createdAt +// ) { +// throw Error('Listed containers should match streamed containers') +// } + +// return true +// }) + +// test('can list conversation messages', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const boGroup = await boClient.conversations.newGroup([alixClient.address]) +// const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) +// const boGroupConversation = await boClient.conversations.findConversation( +// boGroup.id +// ) +// const boDmConversation = await boClient.conversations.findConversation( +// boDm.id +// ) + +// await boGroupConversation?.send('hello') +// await boGroupConversation?.send('hello') +// await boDmConversation?.send('hello') +// await boDmConversation?.send('hello') + +// const boGroupMessages = await boGroupConversation?.messages() +// const boDmMessages = await boDmConversation?.messages() + +// assert( +// boGroupMessages?.length === 3, +// `bo conversation lengths should be 3 but was ${boGroupMessages?.length}` +// ) + +// assert( +// boDmMessages?.length === 2, +// `alix conversation lengths should be 2 but was ${boDmMessages?.length}` +// ) + +// return true +// }) + +// test('can stream both conversations and messages at same time', async () => { +// const [alix, bo] = await createClients(2) + +// let conversationCallbacks = 0 +// let messageCallbacks = 0 +// await bo.conversations.stream(async () => { +// conversationCallbacks++ +// }) + +// await bo.conversations.streamAllMessages(async () => { +// messageCallbacks++ +// }) + +// const group = await alix.conversations.newGroup([bo.address]) +// const dm = await alix.conversations.findOrCreateDm(bo.address) +// await delayToPropogate() +// await group.send('hello') +// await dm.send('hello') +// await delayToPropogate() + +// assert( +// conversationCallbacks === 2, +// 'conversation stream should have received 2 conversation' +// ) +// assert( +// messageCallbacks === 2, +// 'message stream should have received 2 message' +// ) + +// return true +// }) + +// test('can stream conversation messages', async () => { +// const [alixClient, boClient] = await createClients(2) + +// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) +// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) +// const alixGroupConversation = await alixClient.conversations.findConversation( +// alixGroup.id +// ) +// const alixDmConversation = await alixClient.conversations.findConversation( +// alixDm.id +// ) + +// let dmMessageCallbacks = 0 +// let conversationMessageCallbacks = 0 +// await alixGroupConversation?.streamMessages(async () => { +// conversationMessageCallbacks++ +// }) + +// await alixDmConversation?.streamMessages(async () => { +// dmMessageCallbacks++ +// }) + +// await alixGroupConversation?.send({ text: `first message` }) +// await alixDmConversation?.send({ text: `first message` }) + +// assert( +// conversationMessageCallbacks === 1, +// 'conversation stream should have received 1 conversation' +// ) +// assert( +// dmMessageCallbacks === 1, +// 'message stream should have received 1 message' +// ) + +// return true +// }) + +// test('can stream all groups and conversations', async () => { +// const [alixClient, boClient, caroClient] = await createClients(3) + +// const containers: Conversation[] = [] +// await alixClient.conversations.stream( +// async (conversation: Conversation) => { +// containers.push(conversation) +// } +// ) + +// await boClient.conversations.newGroup([alixClient.address]) +// await delayToPropogate() +// if ((containers.length as number) !== 1) { +// throw Error( +// 'Unexpected num conversations (should be 1): ' + containers.length +// ) +// } + +// await boClient.conversations.findOrCreateDm(alixClient.address) +// await delayToPropogate() +// if ((containers.length as number) !== 2) { +// throw Error( +// 'Unexpected num conversations (should be 2): ' + containers.length +// ) +// } + +// await alixClient.conversations.findOrCreateDm(caroClient.address) +// await delayToPropogate() +// if (containers.length !== 3) { +// throw Error( +// 'Expected conversations length 3 but it is: ' + containers.length +// ) +// } + +// alixClient.conversations.cancelStream() +// await delayToPropogate() + +// await caroClient.conversations.newGroup([alixClient.address]) +// await delayToPropogate() +// if ((containers.length as number) !== 3) { +// throw Error( +// 'Unexpected num conversations (should be 3): ' + containers.length +// ) +// } + +// return true +// }) + +// test('can streamAll from multiple clients', async () => { +// const [alix, bo, caro] = await createClients(3) + +// // Setup stream alls +// const allBoConversations: any[] = [] +// const allAliConversations: any[] = [] + +// await bo.conversations.stream(async (conversation) => { +// allBoConversations.push(conversation) +// }) +// await alix.conversations.stream(async (conversation) => { +// allAliConversations.push(conversation) +// }) + +// // Start Caro starts a new conversation. +// await caro.conversations.newConversation(alix.address) +// await delayToPropogate() +// if (allBoConversations.length !== 0) { +// throw Error( +// 'Unexpected all conversations count for Bo ' + +// allBoConversations.length + +// ' and Alix had ' + +// allAliConversations.length +// ) +// } +// if (allAliConversations.length !== 1) { +// throw Error( +// 'Unexpected all conversations count ' + allAliConversations.length +// ) +// } +// return true +// }) + +// test('can streamAll from multiple clients - swapped orderring', async () => { +// const [alix, bo, caro] = await createClients(3) + +// // Setup stream alls +// const allBoConversations: any[] = [] +// const allAliConversations: any[] = [] + +// await alix.conversations.stream(async (conversation) => { +// allAliConversations.push(conversation) +// }) + +// await bo.conversations.stream(async (conversation) => { +// allBoConversations.push(conversation) +// }) + +// // Start Caro starts a new conversation. +// await caro.conversations.newConversation(alix.address) +// await delayToPropogate() +// if (allBoConversations.length !== 0) { +// throw Error( +// 'Unexpected all conversations count for Bo ' + +// allBoConversations.length + +// ' and Alix had ' + +// allAliConversations.length +// ) +// } +// if (allAliConversations.length !== 1) { +// throw Error( +// 'Unexpected all conversations count ' + allAliConversations.length +// ) +// } +// return true +// }) + +// test('can streamAllMessages from multiple clients', async () => { +// const [alix, bo, caro] = await createClients(3) + +// // Setup stream +// const allBoMessages: any[] = [] +// const allAliMessages: any[] = [] + +// await bo.conversations.streamAllMessages(async (conversation) => { +// allBoMessages.push(conversation) +// }) +// await alix.conversations.streamAllMessages(async (conversation) => { +// allAliMessages.push(conversation) +// }) + +// // Start Caro starts a new conversation. +// const caroConversation = await caro.conversations.newConversation( +// alix.address +// ) +// await caroConversation.send({ text: `Message` }) +// await delayToPropogate() +// if (allBoMessages.length !== 0) { +// throw Error('Unexpected all messages count for Bo ' + allBoMessages.length) +// } + +// if (allAliMessages.length !== 1) { +// throw Error( +// 'Unexpected all conversations count for Ali ' + allAliMessages.length +// ) +// } + +// return true +// }) + +// test('can streamAllMessages from multiple clients - swapped', async () => { +// const [alix, bo, caro] = await createClients(3) + +// // Setup stream +// const allBoMessages: any[] = [] +// const allAliMessages: any[] = [] +// const caroGroup = await caro.conversations.newGroup([alix.address]) + +// await alix.conversations.streamAllMessages(async (conversation) => { +// allAliMessages.push(conversation) +// }) +// await bo.conversations.streamAllMessages(async (conversation) => { +// allBoMessages.push(conversation) +// }) + +// // Start Caro starts a new conversation. +// const caroConvo = await caro.conversations.newConversation(alix.address) +// await delayToPropogate() +// await caroConvo.send({ text: `Message` }) +// await caroGroup.send({ text: `Message` }) +// await delayToPropogate() +// if (allBoMessages.length !== 0) { +// throw Error( +// 'Unexpected all conversations count for Bo ' + allBoMessages.length +// ) +// } + +// if (allAliMessages.length !== 2) { +// throw Error( +// 'Unexpected all conversations count for Ali ' + allAliMessages.length +// ) +// } + +// return true +// }) + +// test('can sync consent', async () => { +// const [bo] = await createClients(1) +// const keyBytes = new Uint8Array([ +// 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, +// 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, +// ]) +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` +// const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` +// const directoryExists = await RNFS.exists(dbDirPath) +// if (!directoryExists) { +// await RNFS.mkdir(dbDirPath) +// } +// const directoryExists2 = await RNFS.exists(dbDirPath2) +// if (!directoryExists2) { +// await RNFS.mkdir(dbDirPath2) +// } +// const alixWallet = Wallet.createRandom() + +// const alix = await Client.create(alixWallet, { +// env: 'local', +// appVersion: 'Testing/0.0.0', +// dbEncryptionKey: keyBytes, +// dbDirectory: dbDirPath, +// }) + +// // Create DM conversation +// const dm = await alix.conversations.findOrCreateDm(bo.address) +// await dm.updateConsent('denied') +// const consentState = await dm.consentState() +// assert(consentState === 'denied', `Expected 'denied', got ${consentState}`) + +// await bo.conversations.sync() +// const boDm = await bo.conversations.findConversation(dm.id) + +// const alix2 = await Client.create(alixWallet, { +// env: 'local', +// appVersion: 'Testing/0.0.0', +// dbEncryptionKey: keyBytes, +// dbDirectory: dbDirPath2, +// }) + +// const state = await alix2.inboxState(true) +// assert( +// state.installations.length === 2, +// `Expected 2 installations, got ${state.installations.length}` +// ) + +// // Sync conversations +// await bo.conversations.sync() +// if (boDm) await boDm.sync() +// await alix2.preferences.syncConsent() +// await alix.conversations.syncAllConversations() +// await delayToPropogate(2000) +// await alix2.conversations.syncAllConversations() +// await delayToPropogate(2000) + +// const dm2 = await alix2.conversations.findConversation(dm.id) +// const consentState2 = await dm2?.consentState() +// assert(consentState2 === 'denied', `Expected 'denied', got ${consentState2}`) + +// await alix2.preferences.setConsentState( +// new ConsentRecord(dm2!.id, 'conversation_id', 'allowed') +// ) + +// const convoState = await alix2.preferences.conversationConsentState(dm2!.id) +// assert(convoState === 'allowed', `Expected 'allowed', got ${convoState}`) + +// const updatedConsentState = await dm2?.consentState() +// assert( +// updatedConsentState === 'allowed', +// `Expected 'allowed', got ${updatedConsentState}` +// ) + +// return true +// }) + +// test('can stream consent', async () => { +// const [bo] = await createClients(1) +// const keyBytes = new Uint8Array([ +// 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, +// 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, +// ]) +// const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` +// const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` + +// // Ensure the directories exist +// if (!(await RNFS.exists(dbDirPath))) { +// await RNFS.mkdir(dbDirPath) +// } +// if (!(await RNFS.exists(dbDirPath2))) { +// await RNFS.mkdir(dbDirPath2) +// } + +// const alixWallet = Wallet.createRandom() + +// const alix = await Client.create(alixWallet, { +// env: 'local', +// appVersion: 'Testing/0.0.0', +// dbEncryptionKey: keyBytes, +// dbDirectory: dbDirPath, +// }) + +// const alixGroup = await alix.conversations.newGroup([bo.address]) + +// const alix2 = await Client.create(alixWallet, { +// env: 'local', +// appVersion: 'Testing/0.0.0', +// dbEncryptionKey: keyBytes, +// dbDirectory: dbDirPath2, +// }) + +// await alixGroup.send('Hello') +// await alix.conversations.syncAllConversations() +// await alix2.conversations.syncAllConversations() + +// const alix2Group = await alix2.conversations.findConversation(alixGroup.id) +// await delayToPropogate() + +// const consent = [] +// await alix.preferences.streamConsent(async (entry: ConsentRecord) => { +// consent.push(entry) +// }) + +// await delayToPropogate() + +// await alix2Group!.updateConsent('denied') +// const dm = await alix2.conversations.newConversation(bo.address) +// await dm!.updateConsent('denied') + +// await delayToPropogate(3000) +// await alix.conversations.syncAllConversations() +// await alix2.conversations.syncAllConversations() + +// assert( +// consent.length === 4, +// `Expected 4 consent records, got ${consent.length}` +// ) +// const updatedConsentState = await alixGroup.consentState() +// assert( +// updatedConsentState === 'denied', +// `Expected 'denied', got ${updatedConsentState}` +// ) + +// alix.preferences.cancelStreamConsent() + +// return true +// }) diff --git a/src/index.ts b/src/index.ts index 005d59d3c..bbedeac8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { content } from '@xmtp/proto' import { EventEmitter, NativeModulesProxy } from 'expo-modules-core' import { Client } from '.' @@ -28,7 +29,6 @@ import { DefaultContentTypes } from './lib/types/DefaultContentType' import { MessageId, MessageOrder } from './lib/types/MessagesOptions' import { PermissionPolicySet } from './lib/types/PermissionPolicySet' import { getAddress } from './utils/address' -import { EncodedContent } from '@xmtp/proto/ts/dist/types/message_contents/content.pb' export * from './context' export * from './hooks' @@ -41,6 +41,8 @@ export { StaticAttachmentCodec } from './lib/NativeCodecs/StaticAttachmentCodec' export { TextCodec } from './lib/NativeCodecs/TextCodec' export * from './lib/Signer' +const EncodedContent = content.EncodedContent + export function address(): string { return XMTPModule.address() } From 86d6983d273938d8e2582d804361a42fad9c26d8 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 8 Dec 2024 19:58:15 -0800 Subject: [PATCH 3/8] do the android side --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 20 ++++++++++++++++ ios/XMTPModule.swift | 23 +++++++++++++++++++ src/index.ts | 4 ++-- src/lib/Dm.ts | 2 +- src/lib/Group.ts | 2 +- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 8974d6bdc..d41863b3b 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -655,6 +655,26 @@ class XMTPModule : Module() { } } + AsyncFunction("sendEncodedContent") Coroutine { installationId: String, conversationId: String, encodedContentData: List -> + withContext(Dispatchers.IO) { + logV("sendEncodedContent") + val client = clients[installationId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + ?: throw XMTPException("no conversation found for $conversationId") + val encodedContentDataBytes = + encodedContentData.foldIndexed(ByteArray(encodedContentData.size)) { i, a, v -> + a.apply { + set( + i, + v.toByte() + ) + } + } + val encodedContent = EncodedContent.parseFrom(encodedContentDataBytes) + conversation.send(encodedContent) + } + } + AsyncFunction("sendMessage") Coroutine { installationId: String, id: String, contentJson: String -> withContext(Dispatchers.IO) { logV("sendMessage") diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 5a51e5546..ab45c8353 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -721,6 +721,29 @@ public class XMTPModule: Module { return nil } } + + AsyncFunction("sendEncodedContent") { + ( + installationId: String, conversationId: String, + encodedContentData: [UInt8] + ) -> String in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + guard + let conversation = try client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "no conversation found for \(conversationId)") + } + let encodedContent = try EncodedContent( + serializedBytes: Data(encodedContentData)) + + return try await conversation.send(encodedContent: encodedContent) + } AsyncFunction("sendMessage") { (installationId: String, id: String, contentJson: String) -> String diff --git a/src/index.ts b/src/index.ts index bbedeac8f..7b8a6f22b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -535,7 +535,7 @@ export async function findDmByAddress< } export async function sendWithContentType( - inboxId: InboxId, + installationId: InboxId, conversationId: ConversationId, content: T, codec: ContentCodec @@ -549,7 +549,7 @@ export async function sendWithContentType( const encodedContentData = EncodedContent.encode(encodedContent).finish() return await XMTPModule.sendEncodedContent( - inboxId, + installationId, conversationId, Array.from(encodedContentData) ) diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index 3e44a22f2..395103843 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -97,7 +97,7 @@ export class Dm } return await XMTP.sendWithContentType( - this.client.inboxId, + this.client.installationId, this.id, content, codec diff --git a/src/lib/Group.ts b/src/lib/Group.ts index f83d1d0b2..3534a418c 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -125,7 +125,7 @@ export class Group< } return await XMTP.sendWithContentType( - this.client.inboxId, + this.client.installationId, this.id, content, codec From 1dca0c8d18f25a1986b4c7033694153248a5b427 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 8 Dec 2024 20:17:06 -0800 Subject: [PATCH 4/8] tests still failing --- example/src/tests/conversationTests.ts | 1364 ++++++++++++------------ src/index.ts | 6 +- src/lib/Dm.ts | 2 +- src/lib/Group.ts | 2 +- 4 files changed, 689 insertions(+), 685 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 2f8288ac7..1515ae0a3 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -205,685 +205,685 @@ test('handle fallback types appropriately', async () => { return true }) -// test('can find a conversations by id', async () => { -// const [alixClient, boClient] = await createClients(2) -// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - -// await boClient.conversations.sync() -// const boGroup = await boClient.conversations.findConversation(alixGroup.id) -// const boDm = await boClient.conversations.findConversation(alixDm.id) -// const boDm2 = await boClient.conversations.findConversation( -// 'GARBAGE' as ConversationId -// ) - -// assert(boDm2 === undefined, `bodm2 should be undefined`) - -// assert( -// boGroup?.id === alixGroup.id, -// `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` -// ) - -// assert( -// boDm?.id === alixDm.id, -// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` -// ) - -// return true -// }) - -// test('can find a conversation by topic', async () => { -// const [alixClient, boClient] = await createClients(2) -// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - -// await boClient.conversations.sync() -// const boGroup = await boClient.conversations.findConversationByTopic( -// alixGroup.topic -// ) -// const boDm = await boClient.conversations.findConversationByTopic( -// alixDm.topic -// ) - -// assert( -// boGroup?.id === alixGroup.id, -// `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` -// ) - -// assert( -// boDm?.id === alixDm.id, -// `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` -// ) - -// return true -// }) - -// test('can find a dm by inbox id', async () => { -// const [alixClient, boClient] = await createClients(2) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - -// await boClient.conversations.sync() -// const boDm = await boClient.conversations.findDmByInboxId(alixClient.inboxId) - -// assert( -// boDm?.id === alixDm.id, -// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` -// ) - -// return true -// }) - -// test('can find a dm by address', async () => { -// const [alixClient, boClient] = await createClients(2) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - -// await boClient.conversations.sync() -// const boDm = await boClient.conversations.findDmByAddress(alixClient.address) - -// assert( -// boDm?.id === alixDm.id, -// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` -// ) - -// return true -// }) - -// test('can filter conversations by consent', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) -// const otherGroup = await alixClient.conversations.newGroup([boClient.address]) -// const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) -// await caroClient.conversations.findOrCreateDm(boClient.address) -// await boClient.conversations.sync() -// const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) -// const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) - -// const boConvos = await boClient.conversations.list() -// const boConvosFilteredAllowed = await boClient.conversations.list( -// {}, -// undefined, -// undefined, -// 'allowed' -// ) -// const boConvosFilteredUnknown = await boClient.conversations.list( -// {}, -// undefined, -// undefined, -// 'unknown' -// ) - -// assert( -// boConvos.length === 4, -// `Conversation length should be 4 but was ${boConvos.length}` -// ) - -// assert( -// boConvosFilteredAllowed -// .map((conversation: any) => conversation.id) -// .toString() === [boGroup1.id, boDm1.id].toString(), -// `Conversation allowed should be ${[ -// boGroup1.id, -// boDm1.id, -// ].toString()} but was ${boConvosFilteredAllowed -// .map((convo: any) => convo.id) -// .toString()}` -// ) - -// assert( -// boConvosFilteredUnknown -// .map((conversation: any) => conversation.id) -// .toString() === [boGroup2?.id, boDm2?.id].toString(), -// `Conversation unknown filter should be ${[ -// boGroup2?.id, -// boDm2?.id, -// ].toString()} but was ${boConvosFilteredUnknown -// .map((convo: any) => convo.id) -// .toString()}` -// ) - -// return true -// }) - -// test('can list conversations with params', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) -// const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) -// const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) -// const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) - -// await boGroup1.send({ text: `first message` }) -// await boGroup1.send({ text: `second message` }) -// await boGroup1.send({ text: `third message` }) -// await boDm2.send({ text: `third message` }) -// await boGroup2.send({ text: `first message` }) -// await boDm1.send({ text: `dm message` }) -// // Order should be [Dm1, Group2, Dm2, Group1] - -// await boClient.conversations.syncAllConversations() -// const boConvosOrderCreated = await boClient.conversations.list() -// const boConvosOrderLastMessage = await boClient.conversations.list( -// { lastMessage: true }, -// 'lastMessage' -// ) -// const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) - -// assert( -// boConvosOrderCreated.map((group: any) => group.id).toString() === -// [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), -// `Conversation created at order should be ${[ -// boGroup1.id, -// boGroup2.id, -// boDm1.id, -// boDm2.id, -// ].toString()} but was ${boConvosOrderCreated -// .map((group: any) => group.id) -// .toString()}` -// ) - -// assert( -// boConvosOrderLastMessage.map((group: any) => group.id).toString() === -// [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), -// `Conversation last message order should be ${[ -// boDm1.id, -// boGroup2.id, -// boDm2.id, -// boGroup1.id, -// ].toString()} but was ${boConvosOrderLastMessage -// .map((group: any) => group.id) -// .toString()}` -// ) - -// const messages = await boConvosOrderLastMessage[0].messages() -// assert( -// messages[0].content() === 'dm message', -// `last message 1 should be dm message ${messages[0].content()}` -// ) -// // assert( -// // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', -// // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` -// // ) -// assert( -// boGroupsLimit.length === 1, -// `List length should be 1 but was ${boGroupsLimit.length}` -// ) -// assert( -// boGroupsLimit[0].id === boGroup1.id, -// `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` -// ) - -// return true -// }) - -// test('can list groups', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const boGroup = await boClient.conversations.newGroup([alixClient.address]) -// await boClient.conversations.newGroup([ -// caroClient.address, -// alixClient.address, -// ]) -// const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) -// await boClient.conversations.findOrCreateDm(alixClient.address) - -// const boConversations = await boClient.conversations.list() -// await alixClient.conversations.sync() -// const alixConversations = await alixClient.conversations.list() - -// assert( -// boConversations.length === 4, -// `bo conversation lengths should be 4 but was ${boConversations.length}` -// ) - -// assert( -// alixConversations.length === 3, -// `alix conversation lengths should be 3 but was ${alixConversations.length}` -// ) - -// if ( -// boConversations[0].topic !== boGroup.topic || -// boConversations[0].version !== ConversationVersion.GROUP || -// boConversations[2].version !== ConversationVersion.DM || -// boConversations[2].createdAt !== boDm.createdAt -// ) { -// throw Error('Listed containers should match streamed containers') -// } - -// return true -// }) - -// test('can list conversation messages', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const boGroup = await boClient.conversations.newGroup([alixClient.address]) -// const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) -// const boGroupConversation = await boClient.conversations.findConversation( -// boGroup.id -// ) -// const boDmConversation = await boClient.conversations.findConversation( -// boDm.id -// ) - -// await boGroupConversation?.send('hello') -// await boGroupConversation?.send('hello') -// await boDmConversation?.send('hello') -// await boDmConversation?.send('hello') - -// const boGroupMessages = await boGroupConversation?.messages() -// const boDmMessages = await boDmConversation?.messages() - -// assert( -// boGroupMessages?.length === 3, -// `bo conversation lengths should be 3 but was ${boGroupMessages?.length}` -// ) - -// assert( -// boDmMessages?.length === 2, -// `alix conversation lengths should be 2 but was ${boDmMessages?.length}` -// ) - -// return true -// }) - -// test('can stream both conversations and messages at same time', async () => { -// const [alix, bo] = await createClients(2) - -// let conversationCallbacks = 0 -// let messageCallbacks = 0 -// await bo.conversations.stream(async () => { -// conversationCallbacks++ -// }) - -// await bo.conversations.streamAllMessages(async () => { -// messageCallbacks++ -// }) - -// const group = await alix.conversations.newGroup([bo.address]) -// const dm = await alix.conversations.findOrCreateDm(bo.address) -// await delayToPropogate() -// await group.send('hello') -// await dm.send('hello') -// await delayToPropogate() - -// assert( -// conversationCallbacks === 2, -// 'conversation stream should have received 2 conversation' -// ) -// assert( -// messageCallbacks === 2, -// 'message stream should have received 2 message' -// ) - -// return true -// }) - -// test('can stream conversation messages', async () => { -// const [alixClient, boClient] = await createClients(2) - -// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) -// const alixGroupConversation = await alixClient.conversations.findConversation( -// alixGroup.id -// ) -// const alixDmConversation = await alixClient.conversations.findConversation( -// alixDm.id -// ) - -// let dmMessageCallbacks = 0 -// let conversationMessageCallbacks = 0 -// await alixGroupConversation?.streamMessages(async () => { -// conversationMessageCallbacks++ -// }) - -// await alixDmConversation?.streamMessages(async () => { -// dmMessageCallbacks++ -// }) - -// await alixGroupConversation?.send({ text: `first message` }) -// await alixDmConversation?.send({ text: `first message` }) - -// assert( -// conversationMessageCallbacks === 1, -// 'conversation stream should have received 1 conversation' -// ) -// assert( -// dmMessageCallbacks === 1, -// 'message stream should have received 1 message' -// ) - -// return true -// }) - -// test('can stream all groups and conversations', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const containers: Conversation[] = [] -// await alixClient.conversations.stream( -// async (conversation: Conversation) => { -// containers.push(conversation) -// } -// ) - -// await boClient.conversations.newGroup([alixClient.address]) -// await delayToPropogate() -// if ((containers.length as number) !== 1) { -// throw Error( -// 'Unexpected num conversations (should be 1): ' + containers.length -// ) -// } - -// await boClient.conversations.findOrCreateDm(alixClient.address) -// await delayToPropogate() -// if ((containers.length as number) !== 2) { -// throw Error( -// 'Unexpected num conversations (should be 2): ' + containers.length -// ) -// } - -// await alixClient.conversations.findOrCreateDm(caroClient.address) -// await delayToPropogate() -// if (containers.length !== 3) { -// throw Error( -// 'Expected conversations length 3 but it is: ' + containers.length -// ) -// } - -// alixClient.conversations.cancelStream() -// await delayToPropogate() - -// await caroClient.conversations.newGroup([alixClient.address]) -// await delayToPropogate() -// if ((containers.length as number) !== 3) { -// throw Error( -// 'Unexpected num conversations (should be 3): ' + containers.length -// ) -// } - -// return true -// }) - -// test('can streamAll from multiple clients', async () => { -// const [alix, bo, caro] = await createClients(3) - -// // Setup stream alls -// const allBoConversations: any[] = [] -// const allAliConversations: any[] = [] - -// await bo.conversations.stream(async (conversation) => { -// allBoConversations.push(conversation) -// }) -// await alix.conversations.stream(async (conversation) => { -// allAliConversations.push(conversation) -// }) - -// // Start Caro starts a new conversation. -// await caro.conversations.newConversation(alix.address) -// await delayToPropogate() -// if (allBoConversations.length !== 0) { -// throw Error( -// 'Unexpected all conversations count for Bo ' + -// allBoConversations.length + -// ' and Alix had ' + -// allAliConversations.length -// ) -// } -// if (allAliConversations.length !== 1) { -// throw Error( -// 'Unexpected all conversations count ' + allAliConversations.length -// ) -// } -// return true -// }) - -// test('can streamAll from multiple clients - swapped orderring', async () => { -// const [alix, bo, caro] = await createClients(3) - -// // Setup stream alls -// const allBoConversations: any[] = [] -// const allAliConversations: any[] = [] - -// await alix.conversations.stream(async (conversation) => { -// allAliConversations.push(conversation) -// }) - -// await bo.conversations.stream(async (conversation) => { -// allBoConversations.push(conversation) -// }) - -// // Start Caro starts a new conversation. -// await caro.conversations.newConversation(alix.address) -// await delayToPropogate() -// if (allBoConversations.length !== 0) { -// throw Error( -// 'Unexpected all conversations count for Bo ' + -// allBoConversations.length + -// ' and Alix had ' + -// allAliConversations.length -// ) -// } -// if (allAliConversations.length !== 1) { -// throw Error( -// 'Unexpected all conversations count ' + allAliConversations.length -// ) -// } -// return true -// }) - -// test('can streamAllMessages from multiple clients', async () => { -// const [alix, bo, caro] = await createClients(3) - -// // Setup stream -// const allBoMessages: any[] = [] -// const allAliMessages: any[] = [] - -// await bo.conversations.streamAllMessages(async (conversation) => { -// allBoMessages.push(conversation) -// }) -// await alix.conversations.streamAllMessages(async (conversation) => { -// allAliMessages.push(conversation) -// }) - -// // Start Caro starts a new conversation. -// const caroConversation = await caro.conversations.newConversation( -// alix.address -// ) -// await caroConversation.send({ text: `Message` }) -// await delayToPropogate() -// if (allBoMessages.length !== 0) { -// throw Error('Unexpected all messages count for Bo ' + allBoMessages.length) -// } - -// if (allAliMessages.length !== 1) { -// throw Error( -// 'Unexpected all conversations count for Ali ' + allAliMessages.length -// ) -// } - -// return true -// }) - -// test('can streamAllMessages from multiple clients - swapped', async () => { -// const [alix, bo, caro] = await createClients(3) - -// // Setup stream -// const allBoMessages: any[] = [] -// const allAliMessages: any[] = [] -// const caroGroup = await caro.conversations.newGroup([alix.address]) - -// await alix.conversations.streamAllMessages(async (conversation) => { -// allAliMessages.push(conversation) -// }) -// await bo.conversations.streamAllMessages(async (conversation) => { -// allBoMessages.push(conversation) -// }) - -// // Start Caro starts a new conversation. -// const caroConvo = await caro.conversations.newConversation(alix.address) -// await delayToPropogate() -// await caroConvo.send({ text: `Message` }) -// await caroGroup.send({ text: `Message` }) -// await delayToPropogate() -// if (allBoMessages.length !== 0) { -// throw Error( -// 'Unexpected all conversations count for Bo ' + allBoMessages.length -// ) -// } - -// if (allAliMessages.length !== 2) { -// throw Error( -// 'Unexpected all conversations count for Ali ' + allAliMessages.length -// ) -// } - -// return true -// }) - -// test('can sync consent', async () => { -// const [bo] = await createClients(1) -// const keyBytes = new Uint8Array([ -// 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, -// 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, -// ]) -// // eslint-disable-next-line @typescript-eslint/no-unused-vars -// const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` -// const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` -// const directoryExists = await RNFS.exists(dbDirPath) -// if (!directoryExists) { -// await RNFS.mkdir(dbDirPath) -// } -// const directoryExists2 = await RNFS.exists(dbDirPath2) -// if (!directoryExists2) { -// await RNFS.mkdir(dbDirPath2) -// } -// const alixWallet = Wallet.createRandom() - -// const alix = await Client.create(alixWallet, { -// env: 'local', -// appVersion: 'Testing/0.0.0', -// dbEncryptionKey: keyBytes, -// dbDirectory: dbDirPath, -// }) - -// // Create DM conversation -// const dm = await alix.conversations.findOrCreateDm(bo.address) -// await dm.updateConsent('denied') -// const consentState = await dm.consentState() -// assert(consentState === 'denied', `Expected 'denied', got ${consentState}`) - -// await bo.conversations.sync() -// const boDm = await bo.conversations.findConversation(dm.id) - -// const alix2 = await Client.create(alixWallet, { -// env: 'local', -// appVersion: 'Testing/0.0.0', -// dbEncryptionKey: keyBytes, -// dbDirectory: dbDirPath2, -// }) - -// const state = await alix2.inboxState(true) -// assert( -// state.installations.length === 2, -// `Expected 2 installations, got ${state.installations.length}` -// ) - -// // Sync conversations -// await bo.conversations.sync() -// if (boDm) await boDm.sync() -// await alix2.preferences.syncConsent() -// await alix.conversations.syncAllConversations() -// await delayToPropogate(2000) -// await alix2.conversations.syncAllConversations() -// await delayToPropogate(2000) - -// const dm2 = await alix2.conversations.findConversation(dm.id) -// const consentState2 = await dm2?.consentState() -// assert(consentState2 === 'denied', `Expected 'denied', got ${consentState2}`) - -// await alix2.preferences.setConsentState( -// new ConsentRecord(dm2!.id, 'conversation_id', 'allowed') -// ) - -// const convoState = await alix2.preferences.conversationConsentState(dm2!.id) -// assert(convoState === 'allowed', `Expected 'allowed', got ${convoState}`) - -// const updatedConsentState = await dm2?.consentState() -// assert( -// updatedConsentState === 'allowed', -// `Expected 'allowed', got ${updatedConsentState}` -// ) - -// return true -// }) - -// test('can stream consent', async () => { -// const [bo] = await createClients(1) -// const keyBytes = new Uint8Array([ -// 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, -// 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, -// ]) -// const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` -// const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` - -// // Ensure the directories exist -// if (!(await RNFS.exists(dbDirPath))) { -// await RNFS.mkdir(dbDirPath) -// } -// if (!(await RNFS.exists(dbDirPath2))) { -// await RNFS.mkdir(dbDirPath2) -// } - -// const alixWallet = Wallet.createRandom() - -// const alix = await Client.create(alixWallet, { -// env: 'local', -// appVersion: 'Testing/0.0.0', -// dbEncryptionKey: keyBytes, -// dbDirectory: dbDirPath, -// }) - -// const alixGroup = await alix.conversations.newGroup([bo.address]) - -// const alix2 = await Client.create(alixWallet, { -// env: 'local', -// appVersion: 'Testing/0.0.0', -// dbEncryptionKey: keyBytes, -// dbDirectory: dbDirPath2, -// }) - -// await alixGroup.send('Hello') -// await alix.conversations.syncAllConversations() -// await alix2.conversations.syncAllConversations() - -// const alix2Group = await alix2.conversations.findConversation(alixGroup.id) -// await delayToPropogate() - -// const consent = [] -// await alix.preferences.streamConsent(async (entry: ConsentRecord) => { -// consent.push(entry) -// }) - -// await delayToPropogate() - -// await alix2Group!.updateConsent('denied') -// const dm = await alix2.conversations.newConversation(bo.address) -// await dm!.updateConsent('denied') - -// await delayToPropogate(3000) -// await alix.conversations.syncAllConversations() -// await alix2.conversations.syncAllConversations() - -// assert( -// consent.length === 4, -// `Expected 4 consent records, got ${consent.length}` -// ) -// const updatedConsentState = await alixGroup.consentState() -// assert( -// updatedConsentState === 'denied', -// `Expected 'denied', got ${updatedConsentState}` -// ) - -// alix.preferences.cancelStreamConsent() - -// return true -// }) +test('can find a conversations by id', async () => { + const [alixClient, boClient] = await createClients(2) + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.sync() + const boGroup = await boClient.conversations.findConversation(alixGroup.id) + const boDm = await boClient.conversations.findConversation(alixDm.id) + const boDm2 = await boClient.conversations.findConversation( + 'GARBAGE' as ConversationId + ) + + assert(boDm2 === undefined, `bodm2 should be undefined`) + + assert( + boGroup?.id === alixGroup.id, + `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` + ) + + assert( + boDm?.id === alixDm.id, + `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` + ) + + return true +}) + +test('can find a conversation by topic', async () => { + const [alixClient, boClient] = await createClients(2) + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.sync() + const boGroup = await boClient.conversations.findConversationByTopic( + alixGroup.topic + ) + const boDm = await boClient.conversations.findConversationByTopic( + alixDm.topic + ) + + assert( + boGroup?.id === alixGroup.id, + `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` + ) + + assert( + boDm?.id === alixDm.id, + `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` + ) + + return true +}) + +test('can find a dm by inbox id', async () => { + const [alixClient, boClient] = await createClients(2) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.sync() + const boDm = await boClient.conversations.findDmByInboxId(alixClient.inboxId) + + assert( + boDm?.id === alixDm.id, + `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` + ) + + return true +}) + +test('can find a dm by address', async () => { + const [alixClient, boClient] = await createClients(2) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.sync() + const boDm = await boClient.conversations.findDmByAddress(alixClient.address) + + assert( + boDm?.id === alixDm.id, + `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` + ) + + return true +}) + +test('can filter conversations by consent', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) + const otherGroup = await alixClient.conversations.newGroup([boClient.address]) + const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) + await caroClient.conversations.findOrCreateDm(boClient.address) + await boClient.conversations.sync() + const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) + + const boConvos = await boClient.conversations.list() + const boConvosFilteredAllowed = await boClient.conversations.list( + {}, + undefined, + undefined, + 'allowed' + ) + const boConvosFilteredUnknown = await boClient.conversations.list( + {}, + undefined, + undefined, + 'unknown' + ) + + assert( + boConvos.length === 4, + `Conversation length should be 4 but was ${boConvos.length}` + ) + + assert( + boConvosFilteredAllowed + .map((conversation: any) => conversation.id) + .toString() === [boGroup1.id, boDm1.id].toString(), + `Conversation allowed should be ${[ + boGroup1.id, + boDm1.id, + ].toString()} but was ${boConvosFilteredAllowed + .map((convo: any) => convo.id) + .toString()}` + ) + + assert( + boConvosFilteredUnknown + .map((conversation: any) => conversation.id) + .toString() === [boGroup2?.id, boDm2?.id].toString(), + `Conversation unknown filter should be ${[ + boGroup2?.id, + boDm2?.id, + ].toString()} but was ${boConvosFilteredUnknown + .map((convo: any) => convo.id) + .toString()}` + ) + + return true +}) + +test('can list conversations with params', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) + const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) + const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) + const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) + + await boGroup1.send({ text: `first message` }) + await boGroup1.send({ text: `second message` }) + await boGroup1.send({ text: `third message` }) + await boDm2.send({ text: `third message` }) + await boGroup2.send({ text: `first message` }) + await boDm1.send({ text: `dm message` }) + // Order should be [Dm1, Group2, Dm2, Group1] + + await boClient.conversations.syncAllConversations() + const boConvosOrderCreated = await boClient.conversations.list() + const boConvosOrderLastMessage = await boClient.conversations.list( + { lastMessage: true }, + 'lastMessage' + ) + const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) + + assert( + boConvosOrderCreated.map((group: any) => group.id).toString() === + [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), + `Conversation created at order should be ${[ + boGroup1.id, + boGroup2.id, + boDm1.id, + boDm2.id, + ].toString()} but was ${boConvosOrderCreated + .map((group: any) => group.id) + .toString()}` + ) + + assert( + boConvosOrderLastMessage.map((group: any) => group.id).toString() === + [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), + `Conversation last message order should be ${[ + boDm1.id, + boGroup2.id, + boDm2.id, + boGroup1.id, + ].toString()} but was ${boConvosOrderLastMessage + .map((group: any) => group.id) + .toString()}` + ) + + const messages = await boConvosOrderLastMessage[0].messages() + assert( + messages[0].content() === 'dm message', + `last message 1 should be dm message ${messages[0].content()}` + ) + // assert( + // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', + // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` + // ) + assert( + boGroupsLimit.length === 1, + `List length should be 1 but was ${boGroupsLimit.length}` + ) + assert( + boGroupsLimit[0].id === boGroup1.id, + `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` + ) + + return true +}) + +test('can list groups', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup = await boClient.conversations.newGroup([alixClient.address]) + await boClient.conversations.newGroup([ + caroClient.address, + alixClient.address, + ]) + const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) + await boClient.conversations.findOrCreateDm(alixClient.address) + + const boConversations = await boClient.conversations.list() + await alixClient.conversations.sync() + const alixConversations = await alixClient.conversations.list() + + assert( + boConversations.length === 4, + `bo conversation lengths should be 4 but was ${boConversations.length}` + ) + + assert( + alixConversations.length === 3, + `alix conversation lengths should be 3 but was ${alixConversations.length}` + ) + + if ( + boConversations[0].topic !== boGroup.topic || + boConversations[0].version !== ConversationVersion.GROUP || + boConversations[2].version !== ConversationVersion.DM || + boConversations[2].createdAt !== boDm.createdAt + ) { + throw Error('Listed containers should match streamed containers') + } + + return true +}) + +test('can list conversation messages', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup = await boClient.conversations.newGroup([alixClient.address]) + const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) + const boGroupConversation = await boClient.conversations.findConversation( + boGroup.id + ) + const boDmConversation = await boClient.conversations.findConversation( + boDm.id + ) + + await boGroupConversation?.send('hello') + await boGroupConversation?.send('hello') + await boDmConversation?.send('hello') + await boDmConversation?.send('hello') + + const boGroupMessages = await boGroupConversation?.messages() + const boDmMessages = await boDmConversation?.messages() + + assert( + boGroupMessages?.length === 3, + `bo conversation lengths should be 3 but was ${boGroupMessages?.length}` + ) + + assert( + boDmMessages?.length === 2, + `alix conversation lengths should be 2 but was ${boDmMessages?.length}` + ) + + return true +}) + +test('can stream both conversations and messages at same time', async () => { + const [alix, bo] = await createClients(2) + + let conversationCallbacks = 0 + let messageCallbacks = 0 + await bo.conversations.stream(async () => { + conversationCallbacks++ + }) + + await bo.conversations.streamAllMessages(async () => { + messageCallbacks++ + }) + + const group = await alix.conversations.newGroup([bo.address]) + const dm = await alix.conversations.findOrCreateDm(bo.address) + await delayToPropogate() + await group.send('hello') + await dm.send('hello') + await delayToPropogate() + + assert( + conversationCallbacks === 2, + 'conversation stream should have received 2 conversation' + ) + assert( + messageCallbacks === 2, + 'message stream should have received 2 message' + ) + + return true +}) + +test('can stream conversation messages', async () => { + const [alixClient, boClient] = await createClients(2) + + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + const alixGroupConversation = await alixClient.conversations.findConversation( + alixGroup.id + ) + const alixDmConversation = await alixClient.conversations.findConversation( + alixDm.id + ) + + let dmMessageCallbacks = 0 + let conversationMessageCallbacks = 0 + await alixGroupConversation?.streamMessages(async () => { + conversationMessageCallbacks++ + }) + + await alixDmConversation?.streamMessages(async () => { + dmMessageCallbacks++ + }) + + await alixGroupConversation?.send({ text: `first message` }) + await alixDmConversation?.send({ text: `first message` }) + + assert( + conversationMessageCallbacks === 1, + 'conversation stream should have received 1 conversation' + ) + assert( + dmMessageCallbacks === 1, + 'message stream should have received 1 message' + ) + + return true +}) + +test('can stream all groups and conversations', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const containers: Conversation[] = [] + await alixClient.conversations.stream( + async (conversation: Conversation) => { + containers.push(conversation) + } + ) + + await boClient.conversations.newGroup([alixClient.address]) + await delayToPropogate() + if ((containers.length as number) !== 1) { + throw Error( + 'Unexpected num conversations (should be 1): ' + containers.length + ) + } + + await boClient.conversations.findOrCreateDm(alixClient.address) + await delayToPropogate() + if ((containers.length as number) !== 2) { + throw Error( + 'Unexpected num conversations (should be 2): ' + containers.length + ) + } + + await alixClient.conversations.findOrCreateDm(caroClient.address) + await delayToPropogate() + if (containers.length !== 3) { + throw Error( + 'Expected conversations length 3 but it is: ' + containers.length + ) + } + + alixClient.conversations.cancelStream() + await delayToPropogate() + + await caroClient.conversations.newGroup([alixClient.address]) + await delayToPropogate() + if ((containers.length as number) !== 3) { + throw Error( + 'Unexpected num conversations (should be 3): ' + containers.length + ) + } + + return true +}) + +test('can streamAll from multiple clients', async () => { + const [alix, bo, caro] = await createClients(3) + + // Setup stream alls + const allBoConversations: any[] = [] + const allAliConversations: any[] = [] + + await bo.conversations.stream(async (conversation) => { + allBoConversations.push(conversation) + }) + await alix.conversations.stream(async (conversation) => { + allAliConversations.push(conversation) + }) + + // Start Caro starts a new conversation. + await caro.conversations.newConversation(alix.address) + await delayToPropogate() + if (allBoConversations.length !== 0) { + throw Error( + 'Unexpected all conversations count for Bo ' + + allBoConversations.length + + ' and Alix had ' + + allAliConversations.length + ) + } + if (allAliConversations.length !== 1) { + throw Error( + 'Unexpected all conversations count ' + allAliConversations.length + ) + } + return true +}) + +test('can streamAll from multiple clients - swapped orderring', async () => { + const [alix, bo, caro] = await createClients(3) + + // Setup stream alls + const allBoConversations: any[] = [] + const allAliConversations: any[] = [] + + await alix.conversations.stream(async (conversation) => { + allAliConversations.push(conversation) + }) + + await bo.conversations.stream(async (conversation) => { + allBoConversations.push(conversation) + }) + + // Start Caro starts a new conversation. + await caro.conversations.newConversation(alix.address) + await delayToPropogate() + if (allBoConversations.length !== 0) { + throw Error( + 'Unexpected all conversations count for Bo ' + + allBoConversations.length + + ' and Alix had ' + + allAliConversations.length + ) + } + if (allAliConversations.length !== 1) { + throw Error( + 'Unexpected all conversations count ' + allAliConversations.length + ) + } + return true +}) + +test('can streamAllMessages from multiple clients', async () => { + const [alix, bo, caro] = await createClients(3) + + // Setup stream + const allBoMessages: any[] = [] + const allAliMessages: any[] = [] + + await bo.conversations.streamAllMessages(async (conversation) => { + allBoMessages.push(conversation) + }) + await alix.conversations.streamAllMessages(async (conversation) => { + allAliMessages.push(conversation) + }) + + // Start Caro starts a new conversation. + const caroConversation = await caro.conversations.newConversation( + alix.address + ) + await caroConversation.send({ text: `Message` }) + await delayToPropogate() + if (allBoMessages.length !== 0) { + throw Error('Unexpected all messages count for Bo ' + allBoMessages.length) + } + + if (allAliMessages.length !== 1) { + throw Error( + 'Unexpected all conversations count for Ali ' + allAliMessages.length + ) + } + + return true +}) + +test('can streamAllMessages from multiple clients - swapped', async () => { + const [alix, bo, caro] = await createClients(3) + + // Setup stream + const allBoMessages: any[] = [] + const allAliMessages: any[] = [] + const caroGroup = await caro.conversations.newGroup([alix.address]) + + await alix.conversations.streamAllMessages(async (conversation) => { + allAliMessages.push(conversation) + }) + await bo.conversations.streamAllMessages(async (conversation) => { + allBoMessages.push(conversation) + }) + + // Start Caro starts a new conversation. + const caroConvo = await caro.conversations.newConversation(alix.address) + await delayToPropogate() + await caroConvo.send({ text: `Message` }) + await caroGroup.send({ text: `Message` }) + await delayToPropogate() + if (allBoMessages.length !== 0) { + throw Error( + 'Unexpected all conversations count for Bo ' + allBoMessages.length + ) + } + + if (allAliMessages.length !== 2) { + throw Error( + 'Unexpected all conversations count for Ali ' + allAliMessages.length + ) + } + + return true +}) + +test('can sync consent', async () => { + const [bo] = await createClients(1) + const keyBytes = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` + const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` + const directoryExists = await RNFS.exists(dbDirPath) + if (!directoryExists) { + await RNFS.mkdir(dbDirPath) + } + const directoryExists2 = await RNFS.exists(dbDirPath2) + if (!directoryExists2) { + await RNFS.mkdir(dbDirPath2) + } + const alixWallet = Wallet.createRandom() + + const alix = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, + }) + + // Create DM conversation + const dm = await alix.conversations.findOrCreateDm(bo.address) + await dm.updateConsent('denied') + const consentState = await dm.consentState() + assert(consentState === 'denied', `Expected 'denied', got ${consentState}`) + + await bo.conversations.sync() + const boDm = await bo.conversations.findConversation(dm.id) + + const alix2 = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath2, + }) + + const state = await alix2.inboxState(true) + assert( + state.installations.length === 2, + `Expected 2 installations, got ${state.installations.length}` + ) + + // Sync conversations + await bo.conversations.sync() + if (boDm) await boDm.sync() + await alix2.preferences.syncConsent() + await alix.conversations.syncAllConversations() + await delayToPropogate(2000) + await alix2.conversations.syncAllConversations() + await delayToPropogate(2000) + + const dm2 = await alix2.conversations.findConversation(dm.id) + const consentState2 = await dm2?.consentState() + assert(consentState2 === 'denied', `Expected 'denied', got ${consentState2}`) + + await alix2.preferences.setConsentState( + new ConsentRecord(dm2!.id, 'conversation_id', 'allowed') + ) + + const convoState = await alix2.preferences.conversationConsentState(dm2!.id) + assert(convoState === 'allowed', `Expected 'allowed', got ${convoState}`) + + const updatedConsentState = await dm2?.consentState() + assert( + updatedConsentState === 'allowed', + `Expected 'allowed', got ${updatedConsentState}` + ) + + return true +}) + +test('can stream consent', async () => { + const [bo] = await createClients(1) + const keyBytes = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` + const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` + + // Ensure the directories exist + if (!(await RNFS.exists(dbDirPath))) { + await RNFS.mkdir(dbDirPath) + } + if (!(await RNFS.exists(dbDirPath2))) { + await RNFS.mkdir(dbDirPath2) + } + + const alixWallet = Wallet.createRandom() + + const alix = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, + }) + + const alixGroup = await alix.conversations.newGroup([bo.address]) + + const alix2 = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath2, + }) + + await alixGroup.send('Hello') + await alix.conversations.syncAllConversations() + await alix2.conversations.syncAllConversations() + + const alix2Group = await alix2.conversations.findConversation(alixGroup.id) + await delayToPropogate() + + const consent = [] + await alix.preferences.streamConsent(async (entry: ConsentRecord) => { + consent.push(entry) + }) + + await delayToPropogate() + + await alix2Group!.updateConsent('denied') + const dm = await alix2.conversations.newConversation(bo.address) + await dm!.updateConsent('denied') + + await delayToPropogate(3000) + await alix.conversations.syncAllConversations() + await alix2.conversations.syncAllConversations() + + assert( + consent.length === 4, + `Expected 4 consent records, got ${consent.length}` + ) + const updatedConsentState = await alixGroup.consentState() + assert( + updatedConsentState === 'denied', + `Expected 'denied', got ${updatedConsentState}` + ) + + alix.preferences.cancelStreamConsent() + + return true +}) diff --git a/src/index.ts b/src/index.ts index 7b8a6f22b..8ce6af618 100644 --- a/src/index.ts +++ b/src/index.ts @@ -542,7 +542,11 @@ export async function sendWithContentType( ): Promise { if ('contentKey' in codec) { const contentJson = JSON.stringify(content) - return await XMTPModule.sendMessage(inboxId, conversationId, contentJson) + return await XMTPModule.sendMessage( + installationId, + conversationId, + contentJson + ) } else { const encodedContent = codec.encode(content) encodedContent.fallback = codec.fallback(content) diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index 395103843..6b1bfaf92 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -8,9 +8,9 @@ import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { MessageId, MessagesOptions } from './types/MessagesOptions' +import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' import { ConversationId, ConversationTopic } from '../index' -import { SendOptions } from './types/SendOptions' export interface DmParams { id: ConversationId diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 3534a418c..08445f9b0 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -9,9 +9,9 @@ import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { MessageId, MessagesOptions } from './types/MessagesOptions' import { PermissionPolicySet } from './types/PermissionPolicySet' +import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' import { Address, ConversationId, ConversationTopic } from '../index' -import { SendOptions } from './types/SendOptions' export type PermissionUpdateOption = 'allow' | 'deny' | 'admin' | 'super_admin' From 97a056ee64d2fbd1587324b9aab4a5c7a834d5c7 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 12 Dec 2024 11:41:43 -0800 Subject: [PATCH 5/8] update xmtp android to 3.0.14 --- android/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 0dee991a1..50f26041e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:3.0.13" + implementation "org.xmtp:android:3.0.14" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" @@ -109,8 +109,8 @@ dependencies { // implementation 'io.grpc:grpc-okhttp:1.62.2' // implementation 'io.grpc:grpc-protobuf-lite:1.62.2' // implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0' - // implementation 'org.web3j:crypto:5.0.0' + // implementation 'org.web3j:crypto:4.9.4' // implementation "net.java.dev.jna:jna:5.14.0@aar" // api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3' - // api 'org.xmtp:proto-kotlin:3.62.1' + // api 'org.xmtp:proto-kotlin:3.71.0' } From 8542036bdc86b00df3690c746ee5805224e38fd4 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 12 Dec 2024 11:59:11 -0800 Subject: [PATCH 6/8] update xmtp ios to 3.0.15 --- example/ios/Podfile.lock | 8 ++++---- ios/XMTPReactNative.podspec | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c4ec2128e..b7927a00e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -448,7 +448,7 @@ PODS: - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.28.2) - - XMTP (3.0.14): + - XMTP (3.0.15): - Connect-Swift (= 1.0.0) - CryptoSwift (= 1.8.3) - CSecp256k1 (~> 0.2) @@ -459,7 +459,7 @@ PODS: - ExpoModulesCore - MessagePacker - SQLCipher (= 4.5.7) - - XMTP (= 3.0.14) + - XMTP (= 3.0.15) - Yoga (1.14.0) DEPENDENCIES: @@ -762,8 +762,8 @@ SPEC CHECKSUMS: RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d - XMTP: 3b586fa3703640bb5fec8a64daba9e157d9e5fdc - XMTPReactNative: f3e1cbf80b7278b817bd42982703a95a9250497d + XMTP: 8b0c84096edf74642c5780e4fca9ebbc848fdcf2 + XMTPReactNative: fa98630d85a3947eccfde6062916bcf3de9c32e2 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 4756572ce..f3e0d9e29 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency "MessagePacker" - s.dependency "XMTP", "= 3.0.14" + s.dependency "XMTP", "= 3.0.15" s.dependency 'CSecp256k1', '~> 0.2' s.dependency "SQLCipher", "= 4.5.7" end From 605304cc8440f41db4588ce5bfc9b6fe317cae29 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 12 Dec 2024 12:00:54 -0800 Subject: [PATCH 7/8] fix content type tests, use v3 syncing --- example/src/tests/conversationTests.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 1515ae0a3..716c94482 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -120,14 +120,15 @@ test('register and use custom content types', async () => { const bobConvo = await bob.conversations.newConversation(alice.address) await delayToPropogate() - const aliceConvo = await alice.conversations.newConversation(bob.address) - await bobConvo.send( { topNumber: { bottomNumber: 12 } }, { contentType: ContentTypeNumber } ) - const messages = await aliceConvo.messages() + await alice.conversations.syncAllConversations() + const aliceConvo = await alice.conversations.findConversation(bobConvo.id) + + const messages = await aliceConvo!.messages() assert(messages.length === 1, 'did not get messages') const message = messages[0] @@ -163,15 +164,19 @@ test('handle fallback types appropriately', async () => { bob.register(new NumberCodecEmptyFallback()) bob.register(new NumberCodecUndefinedFallback()) const bobConvo = await bob.conversations.newConversation(alice.address) - const aliceConvo = await alice.conversations.newConversation(bob.address) + // @ts-ignore await bobConvo.send(12, { contentType: ContentTypeNumberWithEmptyFallback }) + // @ts-ignore await bobConvo.send(12, { contentType: ContentTypeNumberWithUndefinedFallback, }) - const messages = await aliceConvo.messages() + await alice.conversations.syncAllConversations() + const aliceConvo = await alice.conversations.findConversation(bobConvo.id) + + const messages = await aliceConvo!.messages() assert(messages.length === 2, 'did not get messages') const messageUndefinedFallback = messages[0] From cacd7edf52c9f422d78f9fa01572fdd730dfe1f1 Mon Sep 17 00:00:00 2001 From: Cameron Voell <1103838+cameronvoell@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:05:51 -0800 Subject: [PATCH 8/8] Create add-custom-content-type-ability.md --- .changeset/add-custom-content-type-ability.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add-custom-content-type-ability.md diff --git a/.changeset/add-custom-content-type-ability.md b/.changeset/add-custom-content-type-ability.md new file mode 100644 index 000000000..e4c8e3e7e --- /dev/null +++ b/.changeset/add-custom-content-type-ability.md @@ -0,0 +1,5 @@ +--- +"@xmtp/react-native-sdk": patch +--- + +Add back custom content types.