From 8f85a168b54c6c320634fda754c41b06a1bd8a61 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 16 Dec 2024 12:24:11 -0800 Subject: [PATCH 1/4] add custom content types for preparing a message --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 20 ++++++++++ src/index.ts | 19 ++++++++++ src/lib/Conversation.ts | 4 ++ src/lib/Dm.ts | 33 ++++++++++++++--- src/lib/Group.ts | 37 +++++++++++++++---- 5 files changed, 101 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index d41863b3..a707c428 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -713,6 +713,26 @@ class XMTPModule : Module() { } } + AsyncFunction("prepareEncodedMessage") Coroutine { installationId: String, conversationId: String, encodedContentData: List -> + withContext(Dispatchers.IO) { + logV("prepareEncodedMessage") + 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.prepareMessage(encodedContent = encodedContent) + } + } + AsyncFunction("findOrCreateDm") Coroutine { installationId: String, peerAddress: String -> withContext(Dispatchers.IO) { logV("findOrCreateDm") diff --git a/src/index.ts b/src/index.ts index 8ce6af61..29e87b54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -596,6 +596,25 @@ export async function prepareMessage( ) } +export async function prepareMessageWithContentType( + installationId: InstallationId, + conversationId: ConversationId, + content: any, + codec: ContentCodec +): Promise { + if ('contentKey' in codec) { + return prepareMessage(installationId, conversationId, content) + } + const encodedContent = codec.encode(content) + encodedContent.fallback = codec.fallback(content) + const encodedContentData = EncodedContent.encode(encodedContent).finish() + return await XMTPModule.prepareEncodedMessage( + installationId, + conversationId, + Array.from(encodedContentData) + ) +} + export async function findOrCreateDm< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index a230d673..a801079e 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -28,6 +28,10 @@ export interface ConversationBase { content: ConversationSendPayload, opts?: SendOptions ): Promise + prepareMessage( + content: ConversationSendPayload, + opts?: SendOptions + ): Promise sync() messages(opts?: MessagesOptions): Promise[]> streamMessages( diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index 6b1bfaf9..9619642d 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -113,11 +113,13 @@ export class Dm */ async prepareMessage< SendContentTypes extends DefaultContentTypes = ContentTypes, - >(content: ConversationSendPayload): Promise { - // TODO: Enable other content types - // if (opts && opts.contentType) { - // return await this._sendWithJSCodec(content, opts.contentType) - // } + >( + content: ConversationSendPayload, + opts?: SendOptions + ): Promise { + if (opts && opts.contentType) { + return await this._prepareWithJSCodec(content, opts.contentType) + } try { if (typeof content === 'string') { @@ -135,6 +137,27 @@ export class Dm } } + private async _prepareWithJSCodec( + 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.prepareMessageWithContentType( + this.client.installationId, + this.id, + content, + codec + ) + } + /** * Publish all prepared messages. * diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 08445f9b..82e6d017 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -84,7 +84,7 @@ export class Group< * Sends a message to the current group. * * @param {string | MessageContent} content - The content of the message. It can be either a string or a structured MessageContent object. - * @returns {Promise} A Promise that resolves to a string identifier for the sent message. + * @returns {Promise} A Promise that resolves to a string identifier for the sent message. * @throws {Error} Throws an error if there is an issue with sending the message. */ async send( @@ -136,16 +136,18 @@ export class Group< * Prepare a group message to be sent. * * @param {string | MessageContent} content - The content of the message. It can be either a string or a structured MessageContent object. - * @returns {Promise} A Promise that resolves to a string identifier for the prepared message to be sent. + * @returns {Promise} A Promise that resolves to a string identifier for the prepared message to be sent. * @throws {Error} Throws an error if there is an issue with sending the message. */ async prepareMessage< SendContentTypes extends DefaultContentTypes = ContentTypes, - >(content: ConversationSendPayload): Promise { - // TODO: Enable other content types - // if (opts && opts.contentType) { - // return await this._sendWithJSCodec(content, opts.contentType) - // } + >( + content: ConversationSendPayload, + opts?: SendOptions + ): Promise { + if (opts && opts.contentType) { + return await this._prepareWithJSCodec(content, opts.contentType) + } try { if (typeof content === 'string') { @@ -163,6 +165,27 @@ export class Group< } } + private async _prepareWithJSCodec( + 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.prepareMessageWithContentType( + this.client.installationId, + this.id, + content, + codec + ) + } + /** * Publish all prepared messages. * From 4feaa1bedd91de0e64b4065b18ead5cc55831a5b Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 16 Dec 2024 12:27:37 -0800 Subject: [PATCH 2/4] do iOS --- ios/XMTPModule.swift | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index ab45c835..34e36224 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -721,7 +721,7 @@ public class XMTPModule: Module { return nil } } - + AsyncFunction("sendEncodedContent") { ( installationId: String, conversationId: String, @@ -809,6 +809,30 @@ public class XMTPModule: Module { ) } + AsyncFunction("prepareEncodedMessage") { + ( + 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.prepareMessage( + encodedContent: encodedContent) + } + AsyncFunction("findOrCreateDm") { (installationId: String, peerAddress: String) -> String in guard From ffa3d9af3a906a8c68751c1be41da0d4e9c95b20 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 16 Dec 2024 12:39:03 -0800 Subject: [PATCH 3/4] Create fifty-drinks-type.md --- .changeset/fifty-drinks-type.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fifty-drinks-type.md diff --git a/.changeset/fifty-drinks-type.md b/.changeset/fifty-drinks-type.md new file mode 100644 index 00000000..76d5590f --- /dev/null +++ b/.changeset/fifty-drinks-type.md @@ -0,0 +1,5 @@ +--- +"@xmtp/react-native-sdk": patch +--- + +Add custom content types for preparing a message From 2036f4afec5e93e9fb5b87b61595f8ce8025b622 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 17 Dec 2024 08:29:50 -0800 Subject: [PATCH 4/4] write a test for it --- android/build.gradle | 2 +- example/ios/Podfile.lock | 10 +++--- example/src/tests/conversationTests.ts | 48 ++++++++++++++++++++++++++ ios/XMTPReactNative.podspec | 2 +- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 50f26041..5a7d0dd1 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.14" + implementation "org.xmtp:android:3.0.15" 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" diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b7927a00..1d6094c8 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -448,18 +448,18 @@ PODS: - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.28.2) - - XMTP (3.0.15): + - XMTP (3.0.16): - Connect-Swift (= 1.0.0) - CryptoSwift (= 1.8.3) - CSecp256k1 (~> 0.2) - LibXMTP (= 3.0.10) - SQLCipher (= 4.5.7) - - XMTPReactNative (3.1.1): + - XMTPReactNative (3.1.2): - CSecp256k1 (~> 0.2) - ExpoModulesCore - MessagePacker - SQLCipher (= 4.5.7) - - XMTP (= 3.0.15) + - XMTP (= 3.0.16) - Yoga (1.14.0) DEPENDENCIES: @@ -762,8 +762,8 @@ SPEC CHECKSUMS: RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d - XMTP: 8b0c84096edf74642c5780e4fca9ebbc848fdcf2 - XMTPReactNative: fa98630d85a3947eccfde6062916bcf3de9c32e2 + XMTP: ce70e4a8e71db02af15bf4a0c230f5990c619281 + XMTPReactNative: 00f79e4244439587ade2f7d65900e0dc9bd2634f Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 716c9448..711bea28 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -144,6 +144,54 @@ test('register and use custom content types', async () => { return true }) +test('register and use custom content types with prepare', 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, + }) + const alice = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + dbEncryptionKey: keyBytes, + }) + + bob.register(new NumberCodec()) + alice.register(new NumberCodec()) + + await delayToPropogate() + + const bobConvo = await bob.conversations.newConversation(alice.address) + await delayToPropogate() + await bobConvo.prepareMessage( + { topNumber: { bottomNumber: 12 } }, + { contentType: ContentTypeNumber } + ) + await bobConvo.publishPreparedMessages() + + 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] + const messageContent = message.content() + + assert( + typeof messageContent === 'object' && + 'topNumber' in messageContent && + messageContent.topNumber.bottomNumber === 12, + 'did not get content properly: ' + JSON.stringify(messageContent) + ) + + return true +}) + 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, diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index f3e0d9e2..6e267100 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.15" + s.dependency "XMTP", "= 3.0.16" s.dependency 'CSecp256k1', '~> 0.2' s.dependency "SQLCipher", "= 4.5.7" end