From 7ca505e80db3dd7d5f86af94fa4b22b2f213b34e Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Tue, 13 Feb 2024 08:04:05 -0800 Subject: [PATCH 1/8] Added conv and group implement common interface. stream all --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 29 ++++++++ .../wrappers/GroupWrapper.kt | 3 +- .../wrappers/IConversationWrapper.kt | 47 ++++++++++++ example/src/tests.ts | 71 +++++++++++++++++++ src/index.ts | 4 ++ src/lib/Conversation.ts | 22 +++--- src/lib/Conversations.ts | 52 ++++++++++++++ src/lib/Group.ts | 16 ++++- src/lib/IConversation.ts | 21 ++++++ 9 files changed, 254 insertions(+), 11 deletions(-) create mode 100644 android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/IConversationWrapper.kt create mode 100644 src/lib/IConversation.ts diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index b514698b9..8deaab58d 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -19,6 +19,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper +import expo.modules.xmtpreactnativesdk.wrappers.IConversationWrapper import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -150,6 +151,7 @@ class XMTPModule : Module() { "sign", "authed", "conversation", + "IConversation", "group", "message", "preEnableIdentityCallback", @@ -667,6 +669,11 @@ class XMTPModule : Module() { subscribeToGroups(clientAddress = clientAddress) } + Function("subscribeToAll") { clientAddress: String -> + logV("subscribeToAll") + subscribeToAll(clientAddress = clientAddress) + } + Function("subscribeToAllMessages") { clientAddress: String -> logV("subscribeToAllMessages") subscribeToAllMessages(clientAddress = clientAddress) @@ -890,6 +897,28 @@ class XMTPModule : Module() { } } + private fun subscribeToAll(clientAddress: String) { + val client = clients[clientAddress] ?: throw XMTPException("No client") + + subscriptions[getConversationsKey(clientAddress)]?.cancel() + subscriptions[getConversationsKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { + try { + client.conversations.streamAll().collect { conversation -> + sendEvent( + "IConversation", + mapOf( + "clientAddress" to clientAddress, + "iConversation" to IConversationWrapper.encodeToObj(client, conversation) + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in subscription to groups + conversations: $e") + subscriptions[getConversationsKey(clientAddress)]?.cancel() + } + } + } + private fun subscribeToAllMessages(clientAddress: String) { val client = clients[clientAddress] ?: throw XMTPException("No client") diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt index 8c3c6da3f..59a400ba5 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -15,7 +15,8 @@ class GroupWrapper { "id" to id, "createdAt" to group.createdAt.time, "peerAddresses" to group.memberAddresses(), - + "version" to "group", + "topic" to id ) } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/IConversationWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/IConversationWrapper.kt new file mode 100644 index 000000000..88caab55a --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/IConversationWrapper.kt @@ -0,0 +1,47 @@ +package expo.modules.xmtpreactnativesdk.wrappers + +import android.util.Base64 +import com.google.gson.GsonBuilder +import org.xmtp.android.library.Client +import org.xmtp.android.library.Conversation + +class IConversationWrapper { + + companion object { + fun encodeToObj(client: Client, conversation: Conversation): Map { + when (conversation.version) { + Conversation.Version.GROUP -> { + return mapOf( + "clientAddress" to client.address, + "id" to conversation.topic, + "createdAt" to conversation.createdAt.time, + "peerAddresses" to conversation.peerAddresses, + "version" to "group", + "topic" to conversation.topic + ) + } + else -> { + val context = when (conversation.version) { + Conversation.Version.V2 -> mapOf( + "conversationID" to (conversation.conversationId ?: ""), + // TODO: expose the context/metadata explicitly in xmtp-android + "metadata" to conversation.toTopicData().invitation.context.metadataMap, + ) + + else -> mapOf() + } + return mapOf( + "clientAddress" to client.address, + "createdAt" to conversation.createdAt.time, + "context" to context, + "topic" to conversation.topic, + "peerAddress" to conversation.peerAddress, + "version" to if (conversation.version == Conversation.Version.V1) "v1" else "v2", + "conversationID" to (conversation.conversationId ?: ""), + "keyMaterial" to Base64.encodeToString(conversation.keyMaterial, Base64.NO_WRAP) + ) + } + } + } + } +} diff --git a/example/src/tests.ts b/example/src/tests.ts index e4d9bb667..58f747d14 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -2,6 +2,7 @@ import { content } from '@xmtp/proto' import ReactNativeBlobUtil from 'react-native-blob-util' import { TextEncoder, TextDecoder } from 'text-encoding' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' +import { IConversation } from 'xmtp-react-native-sdk/lib/IConversation' import { Query, @@ -568,6 +569,76 @@ test('can stream groups', async () => { return true }) +test('can stream groups and conversations', async () => { + // Create three MLS enabled Clients + const aliceClient = await Client.createRandom({ + env: 'local', + enableAlphaMls: true, + }) + const bobClient = await Client.createRandom({ + env: 'local', + enableAlphaMls: true, + }) + const camClient = await Client.createRandom({ + env: 'local', + enableAlphaMls: true, + }) + + // Start streaming groups + const groups: IConversation[] = [] + const cancelStreamAll = await aliceClient.conversations.streamAll( + async (iConversation: IConversation) => { + groups.push(iConversation) + } + ) + + // Cam creates a group with Alice, so stream callback is fired + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const camGroup = await camClient.conversations.newGroup([aliceClient.address]) + await delayToPropogate() + if ((groups.length as number) !== 1) { + throw Error('Unexpected num groups (should be 1): ' + groups.length) + } + + // Bob creates a v2 Conversation with Alice so a stream callback is fired + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const bobConversation = await bobClient.conversations.newConversation( + aliceClient.address + ) + await delayToPropogate() + if ((groups.length as number) !== 2) { + throw Error('Unexpected num groups (should be 2): ' + groups.length) + } + + // * Note Alice creating a v2 Conversation does trigger alice conversations + // stream. + + // Alice creates a V2 Conversationgroup + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const aliceConversation = await aliceClient.conversations.newConversation( + camClient.address + ) + await delayToPropogate() + if (groups.length !== 3) { + throw Error('Expected group length 3 but it is: ' + groups.length) + } + + cancelStreamAll() + await delayToPropogate() + + // Creating a group should no longer trigger stream groups + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const camSecond = await camClient.conversations.newGroup([ + aliceClient.address, + ]) + await delayToPropogate() + if ((groups.length as number) !== 3) { + throw Error('Unexpected num groups (should be 3): ' + groups.length) + } + + return true +}) + test('can pass a custom filter date and receive message objects with expected dates', async () => { try { const bob = await Client.createRandom({ env: 'local' }) diff --git a/src/index.ts b/src/index.ts index 1a092e44c..bcf2d1583 100644 --- a/src/index.ts +++ b/src/index.ts @@ -424,6 +424,10 @@ export function subscribeToConversations(clientAddress: string) { return XMTPModule.subscribeToConversations(clientAddress) } +export function subscribeToAll(clientAddress: string) { + return XMTPModule.subscribeToAll(clientAddress) +} + export function subscribeToGroups(clientAddress: string) { return XMTPModule.subscribeToGroups(clientAddress) } diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index 436ad8185..db4b313bb 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,4 +1,8 @@ -import { ContentTypeId } from './types/ContentCodec' +import { + ConversationVersion, + IConversation, + SendOptions, +} from './IConversation' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' @@ -8,17 +12,15 @@ import { PreparedLocalMessage, } from '../index' -export type SendOptions = { - contentType?: ContentTypeId -} - -export class Conversation { +export class Conversation + implements IConversation +{ client: XMTP.Client createdAt: number context?: ConversationContext topic: string peerAddress: string - version: string + version: ConversationVersion conversationID?: string | undefined /** * Base64 encoded key material for the conversation. @@ -32,7 +34,7 @@ export class Conversation { context?: ConversationContext topic: string peerAddress: string - version: string + version: ConversationVersion conversationID?: string | undefined keyMaterial?: string | undefined } @@ -301,4 +303,8 @@ export class Conversation { XMTP.unsubscribeFromMessages(this.client.address, this.topic) } } + + isGroup(): boolean { + return false + } } diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 57d9fae6a..8eeb4b1f1 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -2,6 +2,7 @@ import { Client } from './Client' import { Conversation } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Group } from './Group' +import { ConversationVersion, IConversation } from './IConversation' import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' import { ContentCodec } from '../index' @@ -160,6 +161,57 @@ export default class Conversations< ) } + /** + * Sets up a real-time stream to listen for new conversations and groups being started. + * + * This method subscribes to conversations in real-time and listens for incoming conversation and group events. + * When a new conversation is detected, the provided callback function is invoked with the details of the conversation. + * @param {Function} callback - A callback function that will be invoked with the new Conversation when a conversation is started. + * @returns {Promise} A Promise that resolves when the stream is set up. + * @warning This stream will continue infinitely. To end the stream, you can call the function returned by this streamAll. + */ + async streamAll( + callback: (conversation: IConversation) => Promise + ) { + XMTPModule.subscribeToAll(this.client.address) + const subscription = XMTPModule.emitter.addListener( + 'IConversation', + async ({ + clientAddress, + iConversation, + }: { + clientAddress: string + iConversation: IConversation + }) => { + if (this.known[iConversation.topic]) { + return + } + + this.known[iConversation.topic] = true + console.log( + 'Version on emitter call: ' + + JSON.stringify({ clientAddress, iConversation }) + ) + if (iConversation.version === ConversationVersion.GROUP) { + return await callback( + new Group(this.client, iConversation as Group) + ) + } else { + return await callback( + new Conversation( + this.client, + iConversation as Conversation + ) + ) + } + } + ) + return () => { + subscription.remove() + this.cancelStream() + } + } + /** * Listen for new messages in all conversations. * diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 842cdb2f2..02253329a 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -1,16 +1,23 @@ -import { SendOptions } from './Conversation' import { DecodedMessage } from './DecodedMessage' +import { + SendOptions, + ConversationVersion, + IConversation, +} from './IConversation' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' export class Group< ContentTypes extends DefaultContentTypes = DefaultContentTypes, -> { +> implements IConversation +{ client: XMTP.Client id: string createdAt: number peerAddresses: string[] + version = ConversationVersion.GROUP + topic: string constructor( client: XMTP.Client, @@ -24,6 +31,7 @@ export class Group< this.id = params.id this.createdAt = params.createdAt this.peerAddresses = params.peerAddresses + this.topic = params.id } get clientAddress(): string { @@ -126,4 +134,8 @@ export class Group< async removeMembers(addresses: string[]): Promise { return XMTP.removeGroupMembers(this.client.address, this.id, addresses) } + + isGroup(): boolean { + return true + } } diff --git a/src/lib/IConversation.ts b/src/lib/IConversation.ts new file mode 100644 index 000000000..18f470329 --- /dev/null +++ b/src/lib/IConversation.ts @@ -0,0 +1,21 @@ +import { ContentTypeId } from './types/ContentCodec' +import { DefaultContentTypes } from './types/DefaultContentType' +import * as XMTP from '../index' + +export type SendOptions = { + contentType?: ContentTypeId +} + +export enum ConversationVersion { + V1 = 'v1', + V2 = 'v2', + GROUP = 'group', +} + +export interface IConversation { + client: XMTP.Client + createdAt: number + version: ConversationVersion + topic: string + isGroup(): boolean +} From a660d978c9c85f4ebd54c8883b8d0f253f61cc67 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Tue, 13 Feb 2024 10:20:33 -0800 Subject: [PATCH 2/8] Update interface name to ConversationContainer --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 8 +++---- ...per.kt => ConversationContainerWrapper.kt} | 2 +- example/src/tests.ts | 11 ++++++---- src/lib/Conversation.ts | 6 ++--- ...nversation.ts => ConversationContainer.ts} | 2 +- src/lib/Conversations.ts | 22 +++++++++---------- src/lib/Group.ts | 6 ++--- 7 files changed, 30 insertions(+), 27 deletions(-) rename android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/{IConversationWrapper.kt => ConversationContainerWrapper.kt} (98%) rename src/lib/{IConversation.ts => ConversationContainer.ts} (83%) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 8deaab58d..86a7aef1c 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -19,7 +19,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper -import expo.modules.xmtpreactnativesdk.wrappers.IConversationWrapper +import expo.modules.xmtpreactnativesdk.wrappers.ConversationContainerWrapper import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -151,7 +151,7 @@ class XMTPModule : Module() { "sign", "authed", "conversation", - "IConversation", + "conversationContainer", "group", "message", "preEnableIdentityCallback", @@ -905,10 +905,10 @@ class XMTPModule : Module() { try { client.conversations.streamAll().collect { conversation -> sendEvent( - "IConversation", + "conversationContainer", mapOf( "clientAddress" to clientAddress, - "iConversation" to IConversationWrapper.encodeToObj(client, conversation) + "conversationContainer" to ConversationContainerWrapper.encodeToObj(client, conversation) ) ) } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/IConversationWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt similarity index 98% rename from android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/IConversationWrapper.kt rename to android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt index 88caab55a..2c60c6215 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/IConversationWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt @@ -5,7 +5,7 @@ import com.google.gson.GsonBuilder import org.xmtp.android.library.Client import org.xmtp.android.library.Conversation -class IConversationWrapper { +class ConversationContainerWrapper { companion object { fun encodeToObj(client: Client, conversation: Conversation): Map { diff --git a/example/src/tests.ts b/example/src/tests.ts index 58f747d14..faea86be6 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -2,7 +2,7 @@ import { content } from '@xmtp/proto' import ReactNativeBlobUtil from 'react-native-blob-util' import { TextEncoder, TextDecoder } from 'text-encoding' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' -import { IConversation } from 'xmtp-react-native-sdk/lib/IConversation' +import { ConversationContainer } from 'xmtp-react-native-sdk/lib/ConversationContainer' import { Query, @@ -585,10 +585,10 @@ test('can stream groups and conversations', async () => { }) // Start streaming groups - const groups: IConversation[] = [] + const groups: ConversationContainer[] = [] const cancelStreamAll = await aliceClient.conversations.streamAll( - async (iConversation: IConversation) => { - groups.push(iConversation) + async (conversationContainer: ConversationContainer) => { + groups.push(conversationContainer) } ) @@ -1352,7 +1352,10 @@ test('register and use custom content types', async () => { bob.register(new NumberCodec()) alice.register(new NumberCodec()) + delayToPropogate() + const bobConvo = await bob.conversations.newConversation(alice.address) + delayToPropogate() const aliceConvo = await alice.conversations.newConversation(bob.address) await bobConvo.send( diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index db4b313bb..97fbe51a6 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,8 +1,8 @@ import { ConversationVersion, - IConversation, + ConversationContainer, SendOptions, -} from './IConversation' +} from './ConversationContainer' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' @@ -13,7 +13,7 @@ import { } from '../index' export class Conversation - implements IConversation + implements ConversationContainer { client: XMTP.Client createdAt: number diff --git a/src/lib/IConversation.ts b/src/lib/ConversationContainer.ts similarity index 83% rename from src/lib/IConversation.ts rename to src/lib/ConversationContainer.ts index 18f470329..61db20ce0 100644 --- a/src/lib/IConversation.ts +++ b/src/lib/ConversationContainer.ts @@ -12,7 +12,7 @@ export enum ConversationVersion { GROUP = 'group', } -export interface IConversation { +export interface ConversationContainer { client: XMTP.Client createdAt: number version: ConversationVersion diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 8eeb4b1f1..cb7f5e3ba 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -2,7 +2,7 @@ import { Client } from './Client' import { Conversation } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Group } from './Group' -import { ConversationVersion, IConversation } from './IConversation' +import { ConversationVersion, ConversationContainer } from './ConversationContainer' import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' import { ContentCodec } from '../index' @@ -171,36 +171,36 @@ export default class Conversations< * @warning This stream will continue infinitely. To end the stream, you can call the function returned by this streamAll. */ async streamAll( - callback: (conversation: IConversation) => Promise + callback: (conversation: ConversationContainer) => Promise ) { XMTPModule.subscribeToAll(this.client.address) const subscription = XMTPModule.emitter.addListener( - 'IConversation', + 'conversationContainer', async ({ clientAddress, - iConversation, + conversationContainer, }: { clientAddress: string - iConversation: IConversation + conversationContainer: ConversationContainer }) => { - if (this.known[iConversation.topic]) { + if (this.known[conversationContainer.topic]) { return } - this.known[iConversation.topic] = true + this.known[conversationContainer.topic] = true console.log( 'Version on emitter call: ' + - JSON.stringify({ clientAddress, iConversation }) + JSON.stringify({ clientAddress, conversationContainer }) ) - if (iConversation.version === ConversationVersion.GROUP) { + if (conversationContainer.version === ConversationVersion.GROUP) { return await callback( - new Group(this.client, iConversation as Group) + new Group(this.client, conversationContainer as Group) ) } else { return await callback( new Conversation( this.client, - iConversation as Conversation + conversationContainer as Conversation ) ) } diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 02253329a..391690fe9 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -2,15 +2,15 @@ import { DecodedMessage } from './DecodedMessage' import { SendOptions, ConversationVersion, - IConversation, -} from './IConversation' + ConversationContainer, +} from './ConversationContainer' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' export class Group< ContentTypes extends DefaultContentTypes = DefaultContentTypes, -> implements IConversation +> implements ConversationContainer { client: XMTP.Client id: string From 31f397e916ad972491b023c4f81ebfb97ff57d2b Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Tue, 13 Feb 2024 10:47:58 -0800 Subject: [PATCH 3/8] lint --- example/src/tests.ts | 2 +- src/lib/ConversationContainer.ts | 4 +++- src/lib/Conversations.ts | 9 +++++++-- src/lib/Group.ts | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/example/src/tests.ts b/example/src/tests.ts index faea86be6..643d5544b 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -1,8 +1,8 @@ import { content } from '@xmtp/proto' import ReactNativeBlobUtil from 'react-native-blob-util' import { TextEncoder, TextDecoder } from 'text-encoding' -import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' import { ConversationContainer } from 'xmtp-react-native-sdk/lib/ConversationContainer' +import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' import { Query, diff --git a/src/lib/ConversationContainer.ts b/src/lib/ConversationContainer.ts index 61db20ce0..7db80bac1 100644 --- a/src/lib/ConversationContainer.ts +++ b/src/lib/ConversationContainer.ts @@ -12,7 +12,9 @@ export enum ConversationVersion { GROUP = 'group', } -export interface ConversationContainer { +export interface ConversationContainer< + ContentTypes extends DefaultContentTypes, +> { client: XMTP.Client createdAt: number version: ConversationVersion diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index cb7f5e3ba..95e11124b 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -1,8 +1,11 @@ import { Client } from './Client' import { Conversation } from './Conversation' +import { + ConversationVersion, + ConversationContainer, +} from './ConversationContainer' import { DecodedMessage } from './DecodedMessage' import { Group } from './Group' -import { ConversationVersion, ConversationContainer } from './ConversationContainer' import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' import { ContentCodec } from '../index' @@ -171,7 +174,9 @@ export default class Conversations< * @warning This stream will continue infinitely. To end the stream, you can call the function returned by this streamAll. */ async streamAll( - callback: (conversation: ConversationContainer) => Promise + callback: ( + conversation: ConversationContainer + ) => Promise ) { XMTPModule.subscribeToAll(this.client.address) const subscription = XMTPModule.emitter.addListener( diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 391690fe9..b9be36abb 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -1,9 +1,9 @@ -import { DecodedMessage } from './DecodedMessage' import { SendOptions, ConversationVersion, ConversationContainer, } from './ConversationContainer' +import { DecodedMessage } from './DecodedMessage' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' From c9bbb4524737f0bd65615c9d9b59c3e60d4572d6 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 14 Feb 2024 07:38:04 -0800 Subject: [PATCH 4/8] Added converting to Group and Conversation to test --- example/src/tests.ts | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/example/src/tests.ts b/example/src/tests.ts index 993f65976..473b8c259 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -14,6 +14,7 @@ import { RemoteAttachmentContent, Group, } from '../../src/index' +import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' type EncodedContent = content.EncodedContent type ContentTypeId = content.ContentTypeId @@ -579,7 +580,7 @@ test('can stream groups', async () => { return true }) -test('can stream groups and conversations', async () => { +test('can stream all groups and conversations', async () => { // Create three MLS enabled Clients const aliceClient = await Client.createRandom({ env: 'local', @@ -594,20 +595,25 @@ test('can stream groups and conversations', async () => { enableAlphaMls: true, }) - // Start streaming groups - const groups: ConversationContainer[] = [] + // Start streaming groups and conversations + const containers: ConversationContainer[] = [] const cancelStreamAll = await aliceClient.conversations.streamAll( async (conversationContainer: ConversationContainer) => { - groups.push(conversationContainer) + containers.push(conversationContainer) } ) - // Cam creates a group with Alice, so stream callback is fired + // Bob creates a group with Alice, so stream callback is fired // eslint-disable-next-line @typescript-eslint/no-unused-vars - const camGroup = await camClient.conversations.newGroup([aliceClient.address]) + const bobGroup = await bobClient.conversations.newGroup([aliceClient.address]) await delayToPropogate() - if ((groups.length as number) !== 1) { - throw Error('Unexpected num groups (should be 1): ' + groups.length) + if ((containers.length as number) !== 1) { + throw Error('Unexpected num groups (should be 1): ' + containers.length) + } + if (containers[0].isGroup()) { + (containers[0] as Group).sync() + } else { + throw Error('Unexpected first ConversationContainer should be a group') } // Bob creates a v2 Conversation with Alice so a stream callback is fired @@ -616,8 +622,12 @@ test('can stream groups and conversations', async () => { aliceClient.address ) await delayToPropogate() - if ((groups.length as number) !== 2) { - throw Error('Unexpected num groups (should be 2): ' + groups.length) + if ((containers.length as number) !== 2) { + throw Error('Unexpected num groups (should be 2): ' + containers.length) + } + + if(bobConversation.conversationID != (containers[1] as Conversation).conversationID) { + throw Error('Conversation from streamed all should match conversationID with created conversation') } // * Note Alice creating a v2 Conversation does trigger alice conversations @@ -629,8 +639,8 @@ test('can stream groups and conversations', async () => { camClient.address ) await delayToPropogate() - if (groups.length !== 3) { - throw Error('Expected group length 3 but it is: ' + groups.length) + if (containers.length !== 3) { + throw Error('Expected group length 3 but it is: ' + containers.length) } cancelStreamAll() @@ -638,12 +648,12 @@ test('can stream groups and conversations', async () => { // Creating a group should no longer trigger stream groups // eslint-disable-next-line @typescript-eslint/no-unused-vars - const camSecond = await camClient.conversations.newGroup([ + const camConversation = await camClient.conversations.newGroup([ aliceClient.address, ]) await delayToPropogate() - if ((groups.length as number) !== 3) { - throw Error('Unexpected num groups (should be 3): ' + groups.length) + if ((containers.length as number) !== 3) { + throw Error('Unexpected num groups (should be 3): ' + containers.length) } return true From fb87e3b9c8a337c241dd2f92143172c56d2a3587 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 14 Feb 2024 09:01:09 -0800 Subject: [PATCH 5/8] Convert conv to group for wrapper re-use --- .../wrappers/ConversationContainerWrapper.kt | 33 +++---------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt index 2c60c6215..4207cdf6f 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt @@ -1,7 +1,6 @@ package expo.modules.xmtpreactnativesdk.wrappers import android.util.Base64 -import com.google.gson.GsonBuilder import org.xmtp.android.library.Client import org.xmtp.android.library.Conversation @@ -11,35 +10,13 @@ class ConversationContainerWrapper { fun encodeToObj(client: Client, conversation: Conversation): Map { when (conversation.version) { Conversation.Version.GROUP -> { - return mapOf( - "clientAddress" to client.address, - "id" to conversation.topic, - "createdAt" to conversation.createdAt.time, - "peerAddresses" to conversation.peerAddresses, - "version" to "group", - "topic" to conversation.topic - ) + val group = (conversation as Conversation.Group).group + return GroupWrapper.encodeToObj(client, group, Base64.encodeToString(group.id, + Base64.NO_WRAP + )) } else -> { - val context = when (conversation.version) { - Conversation.Version.V2 -> mapOf( - "conversationID" to (conversation.conversationId ?: ""), - // TODO: expose the context/metadata explicitly in xmtp-android - "metadata" to conversation.toTopicData().invitation.context.metadataMap, - ) - - else -> mapOf() - } - return mapOf( - "clientAddress" to client.address, - "createdAt" to conversation.createdAt.time, - "context" to context, - "topic" to conversation.topic, - "peerAddress" to conversation.peerAddress, - "version" to if (conversation.version == Conversation.Version.V1) "v1" else "v2", - "conversationID" to (conversation.conversationId ?: ""), - "keyMaterial" to Base64.encodeToString(conversation.keyMaterial, Base64.NO_WRAP) - ) + return ConversationWrapper.encodeToObj(client, conversation) } } } From 4816fd40c2771906cf3f0a9b3bac4a0de9dc3c85 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 14 Feb 2024 10:15:30 -0800 Subject: [PATCH 6/8] Removed isGroups. Version is GROUP or DIRECT --- .../wrappers/ConversationWrapper.kt | 2 +- .../xmtpreactnativesdk/wrappers/GroupWrapper.kt | 2 +- example/src/tests.ts | 8 +++++--- ios/Wrappers/ConversationWrapper.swift | 2 +- src/index.ts | 1 + src/lib/Conversation.ts | 10 ++-------- src/lib/ConversationContainer.ts | 11 ++--------- src/lib/Group.ts | 6 +----- 8 files changed, 14 insertions(+), 28 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt index 97d88beed..dc97f211f 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt @@ -24,7 +24,7 @@ class ConversationWrapper { "context" to context, "topic" to conversation.topic, "peerAddress" to conversation.peerAddress, - "version" to if (conversation.version == Conversation.Version.V1) "v1" else "v2", + "version" to "DIRECT", "conversationID" to (conversation.conversationId ?: ""), "keyMaterial" to Base64.encodeToString(conversation.keyMaterial, Base64.NO_WRAP) ) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt index 59a400ba5..e34bd7d5f 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -15,7 +15,7 @@ class GroupWrapper { "id" to id, "createdAt" to group.createdAt.time, "peerAddresses" to group.memberAddresses(), - "version" to "group", + "version" to "GROUP", "topic" to id ) } diff --git a/example/src/tests.ts b/example/src/tests.ts index 473b8c259..688e18362 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -1,7 +1,6 @@ import { content } from '@xmtp/proto' import ReactNativeBlobUtil from 'react-native-blob-util' import { TextEncoder, TextDecoder } from 'text-encoding' -import { ConversationContainer } from 'xmtp-react-native-sdk/lib/ConversationContainer' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' import { @@ -13,6 +12,8 @@ import { RemoteAttachmentCodec, RemoteAttachmentContent, Group, + ConversationContainer, + ConversationVersion, } from '../../src/index' import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' @@ -610,9 +611,10 @@ test('can stream all groups and conversations', async () => { if ((containers.length as number) !== 1) { throw Error('Unexpected num groups (should be 1): ' + containers.length) } - if (containers[0].isGroup()) { + if (containers[0].version === ConversationVersion.GROUP) { (containers[0] as Group).sync() } else { + console.log(JSON.stringify(containers[0] as Group)) throw Error('Unexpected first ConversationContainer should be a group') } @@ -626,7 +628,7 @@ test('can stream all groups and conversations', async () => { throw Error('Unexpected num groups (should be 2): ' + containers.length) } - if(bobConversation.conversationID != (containers[1] as Conversation).conversationID) { + if(containers[1].version === ConversationVersion.DIRECT && bobConversation.conversationID != (containers[1] as Conversation).conversationID) { throw Error('Conversation from streamed all should match conversationID with created conversation') } diff --git a/ios/Wrappers/ConversationWrapper.swift b/ios/Wrappers/ConversationWrapper.swift index b3a63587e..5918a33f4 100644 --- a/ios/Wrappers/ConversationWrapper.swift +++ b/ios/Wrappers/ConversationWrapper.swift @@ -23,7 +23,7 @@ struct ConversationWrapper { "createdAt": UInt64(conversation.createdAt.timeIntervalSince1970 * 1000), "context": context, "peerAddress": conversation.peerAddress, - "version": conversation.version == .v1 ? "v1" : "v2", + "version": "DIRECT", "conversationID": conversation.conversationID ?? "", "keyMaterial": conversation.keyMaterial?.base64EncodedString() ?? "" ] diff --git a/src/index.ts b/src/index.ts index 5d90179f1..eae856e61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -560,6 +560,7 @@ export * from './XMTP.types' export { Client } from './lib/Client' export * from './lib/ContentCodec' export { Conversation } from './lib/Conversation' +export { ConversationContainer, ConversationVersion } from './lib/ConversationContainer' export { Query } from './lib/Query' export { XMTPPush } from './lib/XMTPPush' export { ConsentListEntry, DecodedMessage } diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index 97fbe51a6..7feb300ee 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,7 +1,6 @@ import { ConversationVersion, ConversationContainer, - SendOptions, } from './ConversationContainer' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' @@ -11,6 +10,7 @@ import { DecodedMessage, PreparedLocalMessage, } from '../index' +import { SendOptions } from './types/SendOptions' export class Conversation implements ConversationContainer @@ -20,7 +20,7 @@ export class Conversation context?: ConversationContext topic: string peerAddress: string - version: ConversationVersion + version = ConversationVersion.DIRECT conversationID?: string | undefined /** * Base64 encoded key material for the conversation. @@ -34,7 +34,6 @@ export class Conversation context?: ConversationContext topic: string peerAddress: string - version: ConversationVersion conversationID?: string | undefined keyMaterial?: string | undefined } @@ -44,7 +43,6 @@ export class Conversation this.context = params.context this.topic = params.topic this.peerAddress = params.peerAddress - this.version = params.version this.conversationID = params.conversationID this.keyMaterial = params.keyMaterial } @@ -303,8 +301,4 @@ export class Conversation XMTP.unsubscribeFromMessages(this.client.address, this.topic) } } - - isGroup(): boolean { - return false - } } diff --git a/src/lib/ConversationContainer.ts b/src/lib/ConversationContainer.ts index 7db80bac1..e957244ef 100644 --- a/src/lib/ConversationContainer.ts +++ b/src/lib/ConversationContainer.ts @@ -1,15 +1,9 @@ -import { ContentTypeId } from './types/ContentCodec' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' -export type SendOptions = { - contentType?: ContentTypeId -} - export enum ConversationVersion { - V1 = 'v1', - V2 = 'v2', - GROUP = 'group', + DIRECT = "DIRECT", + GROUP = "GROUP" } export interface ConversationContainer< @@ -19,5 +13,4 @@ export interface ConversationContainer< createdAt: number version: ConversationVersion topic: string - isGroup(): boolean } diff --git a/src/lib/Group.ts b/src/lib/Group.ts index db60d71db..ff159d55b 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -1,5 +1,4 @@ import { - SendOptions, ConversationVersion, ConversationContainer, } from './ConversationContainer' @@ -7,6 +6,7 @@ import { DecodedMessage } from './DecodedMessage' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' +import { SendOptions } from './types/SendOptions' export class Group< ContentTypes extends DefaultContentTypes = DefaultContentTypes, @@ -134,10 +134,6 @@ export class Group< async removeMembers(addresses: string[]): Promise { return XMTP.removeGroupMembers(this.client.address, this.id, addresses) } - - isGroup(): boolean { - return true - } isActive(): boolean { return XMTP.isGroupActive(this.client.address, this.id) From bbfe753ec638bed7a555ddc42c5a3b47d03c36b1 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 14 Feb 2024 10:16:35 -0800 Subject: [PATCH 7/8] feat: streamAll method and ConversationContainer interface --- example/src/tests.ts | 14 ++++++++++---- src/index.ts | 5 ++++- src/lib/Conversation.ts | 2 +- src/lib/ConversationContainer.ts | 4 ++-- src/lib/Group.ts | 4 ++-- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/example/src/tests.ts b/example/src/tests.ts index 688e18362..2fb107031 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -2,6 +2,7 @@ import { content } from '@xmtp/proto' import ReactNativeBlobUtil from 'react-native-blob-util' import { TextEncoder, TextDecoder } from 'text-encoding' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' +import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' import { Query, @@ -15,7 +16,6 @@ import { ConversationContainer, ConversationVersion, } from '../../src/index' -import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' type EncodedContent = content.EncodedContent type ContentTypeId = content.ContentTypeId @@ -612,7 +612,7 @@ test('can stream all groups and conversations', async () => { throw Error('Unexpected num groups (should be 1): ' + containers.length) } if (containers[0].version === ConversationVersion.GROUP) { - (containers[0] as Group).sync() + ;(containers[0] as Group).sync() } else { console.log(JSON.stringify(containers[0] as Group)) throw Error('Unexpected first ConversationContainer should be a group') @@ -628,8 +628,14 @@ test('can stream all groups and conversations', async () => { throw Error('Unexpected num groups (should be 2): ' + containers.length) } - if(containers[1].version === ConversationVersion.DIRECT && bobConversation.conversationID != (containers[1] as Conversation).conversationID) { - throw Error('Conversation from streamed all should match conversationID with created conversation') + if ( + containers[1].version === ConversationVersion.DIRECT && + bobConversation.conversationID != + (containers[1] as Conversation).conversationID + ) { + throw Error( + 'Conversation from streamed all should match conversationID with created conversation' + ) } // * Note Alice creating a v2 Conversation does trigger alice conversations diff --git a/src/index.ts b/src/index.ts index eae856e61..2d79e5085 100644 --- a/src/index.ts +++ b/src/index.ts @@ -560,7 +560,10 @@ export * from './XMTP.types' export { Client } from './lib/Client' export * from './lib/ContentCodec' export { Conversation } from './lib/Conversation' -export { ConversationContainer, ConversationVersion } from './lib/ConversationContainer' +export { + ConversationContainer, + ConversationVersion, +} from './lib/ConversationContainer' export { Query } from './lib/Query' export { XMTPPush } from './lib/XMTPPush' export { ConsentListEntry, DecodedMessage } diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index 7feb300ee..05be85158 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -4,13 +4,13 @@ import { } from './ConversationContainer' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' +import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' import { ConversationContext, DecodedMessage, PreparedLocalMessage, } from '../index' -import { SendOptions } from './types/SendOptions' export class Conversation implements ConversationContainer diff --git a/src/lib/ConversationContainer.ts b/src/lib/ConversationContainer.ts index e957244ef..82034216a 100644 --- a/src/lib/ConversationContainer.ts +++ b/src/lib/ConversationContainer.ts @@ -2,8 +2,8 @@ import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' export enum ConversationVersion { - DIRECT = "DIRECT", - GROUP = "GROUP" + DIRECT = 'DIRECT', + GROUP = 'GROUP', } export interface ConversationContainer< diff --git a/src/lib/Group.ts b/src/lib/Group.ts index ff159d55b..6d1c94a6b 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -5,8 +5,8 @@ import { import { DecodedMessage } from './DecodedMessage' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' -import * as XMTP from '../index' import { SendOptions } from './types/SendOptions' +import * as XMTP from '../index' export class Group< ContentTypes extends DefaultContentTypes = DefaultContentTypes, @@ -134,7 +134,7 @@ export class Group< async removeMembers(addresses: string[]): Promise { return XMTP.removeGroupMembers(this.client.address, this.id, addresses) } - + isActive(): boolean { return XMTP.isGroupActive(this.client.address, this.id) } From 79b2526e096078961160739f51b94ddbcc2b3dca Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 14 Feb 2024 10:24:08 -0800 Subject: [PATCH 8/8] lint errors --- example/src/tests.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example/src/tests.ts b/example/src/tests.ts index 2fb107031..3905a729f 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -2,7 +2,6 @@ import { content } from '@xmtp/proto' import ReactNativeBlobUtil from 'react-native-blob-util' import { TextEncoder, TextDecoder } from 'text-encoding' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' -import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' import { Query, @@ -630,7 +629,7 @@ test('can stream all groups and conversations', async () => { if ( containers[1].version === ConversationVersion.DIRECT && - bobConversation.conversationID != + bobConversation.conversationID !== (containers[1] as Conversation).conversationID ) { throw Error(