From 3c105cd26c73da53d1a07db756369ef27a569fa6 Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Tue, 2 Jan 2024 15:27:28 -0700 Subject: [PATCH 1/3] Add Support for Custom Content Types when preparing messages Added logic in Conversation to handle content type in prepare method Updated bridged module to handle encrypted messages Requires iOS Sdk Change: https://github.com/xmtp/xmtp-ios/pull/212 Requires Android Sdk Change: --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 34 ++++++++++++++++++ example/src/tests.ts | 36 +++++++++++++++++++ ios/XMTPModule.swift | 24 +++++++++++++ src/index.ts | 20 +++++++++++ src/lib/Conversation.ts | 29 ++++++++++++++- 5 files changed, 142 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index c531c9016..cb43fa028 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -443,6 +443,40 @@ class XMTPModule : Module() { ).toJson() } + AsyncFunction("prepareEncodedMessage") { clientAddress: String, conversationTopic: String, encodedContentData: List -> + logV("prepareEncodedMessage") + val conversation = + findConversation( + clientAddress = clientAddress, + topic = conversationTopic + ) + ?: throw XMTPException("no conversation found for $conversationTopic") + + val encodedContentDataBytes = + encodedContentData.foldIndexed(ByteArray(encodedContentData.size)) { i, a, v -> + a.apply { + set( + i, + v.toByte() + ) + } + } + val encodedContent = EncodedContent.parseFrom(encodedContentDataBytes) + + val prepared = conversation.prepareMessage( + encodedContent = encodedContent, + options = SendOptions(contentType = sending.type) + ) + val preparedAtMillis = prepared.envelopes[0].timestampNs / 1_000_000 + val preparedFile = File.createTempFile(prepared.messageId, null) + preparedFile.writeBytes(prepared.toSerializedData()) + PreparedLocalMessage( + messageId = prepared.messageId, + preparedFileUri = preparedFile.toURI().toString(), + preparedAt = preparedAtMillis, + ).toJson() + } + AsyncFunction("sendPreparedMessage") { clientAddress: String, preparedLocalMessageJson: String -> logV("sendPreparedMessage") val client = clients[clientAddress] ?: throw XMTPException("No client") diff --git a/example/src/tests.ts b/example/src/tests.ts index 69567bf48..101fc38af 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -756,6 +756,42 @@ test('register and use custom content types', async () => { return true }) +test('register and use custom content types when preparing message', async () => { + const bob = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + }) + const alice = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + }) + + bob.register(new NumberCodec()) + alice.register(new NumberCodec()) + + const bobConvo = await bob.conversations.newConversation(alice.address) + const aliceConvo = await alice.conversations.newConversation(bob.address) + + const prepped = await bobConvo.prepareMessage(12, { + contentType: ContentTypeNumber, + }) + + await bobConvo.sendPreparedMessage(prepped) + + const messages = await aliceConvo.messages() + assert(messages.length === 1, 'did not get messages') + + const message = messages[0] + const messageContent = message.content() + + assert( + messageContent === 12, + 'did not get content properly: ' + JSON.stringify(messageContent) + ) + + return true +}) + test('calls preCreateIdentityCallback when supplied', async () => { let isCallbackCalled = false const preCreateIdentityCallback = () => { diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 8387ab56d..841407af3 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -373,6 +373,30 @@ public class XMTPModule: Module { ).toJson() } + AsyncFunction("prepareEncodedMessage") { ( + clientAddress: String, + conversationTopic: String, + encodedContentData: [UInt8] + ) -> String in + guard let conversation = try await findConversation(clientAddress: clientAddress, topic: conversationTopic) else { + throw Error.conversationNotFound("no conversation found for \(conversationTopic)") + } + let encodedContent = try EncodedContent(serializedData: Data(encodedContentData)) + + let prepared = try await conversation.prepareMessage( + encodedContent: encodedContent + ) + let preparedAtMillis = prepared.envelopes[0].timestampNs / 1_000_000 + let preparedData = try prepared.serializedData() + let preparedFile = FileManager.default.temporaryDirectory.appendingPathComponent(prepared.messageID) + try preparedData.write(to: preparedFile) + return try PreparedLocalMessage( + messageId: prepared.messageID, + preparedFileUri: preparedFile.absoluteString, + preparedAt: preparedAtMillis + ).toJson() + } + AsyncFunction("sendPreparedMessage") { (clientAddress: String, preparedLocalMessageJson: String) -> String in guard let client = await clientsManager.getClient(key: clientAddress) else { throw Error.noClient diff --git a/src/index.ts b/src/index.ts index 5e324ad8b..8ab24df3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -276,6 +276,26 @@ export async function prepareMessage( return JSON.parse(preparedJson) } +export async function prepareMessageWithContentType( + clientAddress: string, + conversationTopic: string, + content: any, + codec: ContentCodec +): Promise { + if ('contentKey' in codec) { + return prepareMessage(clientAddress, conversationTopic, content) + } + const encodedContent = codec.encode(content) + encodedContent.fallback = codec.fallback(content) + const encodedContentData = EncodedContent.encode(encodedContent).finish() + const preparedJson = await XMTPModule.prepareEncodedMessage( + clientAddress, + conversationTopic, + Array.from(encodedContentData) + ) + return JSON.parse(preparedJson) +} + export async function sendPreparedMessage( clientAddress: string, preparedLocalMessage: PreparedLocalMessage diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index d50a50f98..760aae7be 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -131,6 +131,27 @@ export class Conversation { } } + 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.address, + this.topic, + content, + codec, + ) + } + /** * Prepares a message to be sent, yielding a `PreparedLocalMessage` object. * @@ -146,7 +167,13 @@ export class Conversation { * @returns {Promise} A Promise that resolves to a `PreparedLocalMessage` object. * @throws {Error} Throws an error if there is an issue with preparing the message. */ - async prepareMessage(content: any): Promise { + async prepareMessage( + content: any, + opts?: SendOptions + ): Promise { + if (opts && opts.contentType) { + return await this._prepareWithJSCodec(content, opts.contentType) + } try { if (typeof content === 'string') { content = { text: content } From 74bfcf4d4ec7ff753e85ba8b5b1eee449fbc8adc Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Tue, 2 Jan 2024 19:16:54 -0700 Subject: [PATCH 2/3] fix: Fixed Support for Custom Content Types in Prepare Message Bumped versions --- android/build.gradle | 2 +- example/ios/Podfile.lock | 14 +++++++------- ios/XMTPReactNative.podspec | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 748c76a0d..e3c6c0b97 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -95,7 +95,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.7.0" + implementation "org.xmtp:android:0.7.1" 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 257d96291..1e099051c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -302,7 +302,7 @@ PODS: - React-jsinspector (0.71.14) - React-logger (0.71.14): - glog - - react-native-blob-util (0.19.4): + - react-native-blob-util (0.19.6): - React-Core - react-native-encrypted-storage (4.0.3): - React-Core @@ -411,7 +411,7 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.7.3-alpha0): + - XMTP (0.7.4-alpha0): - Connect-Swift (= 0.3.0) - GzipSwift - web3.swift @@ -419,7 +419,7 @@ PODS: - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - - XMTP (= 0.7.3-alpha0) + - XMTP (= 0.7.4-alpha0) - XMTPRust (0.3.7-beta0) - Yoga (1.14.0) @@ -646,7 +646,7 @@ SPEC CHECKSUMS: React-jsiexecutor: 94cfc1788637ceaf8841ef1f69b10cc0d62baadc React-jsinspector: 7bf923954b4e035f494b01ac16633963412660d7 React-logger: 655ff5db8bd922acfbe76a4983ffab048916343e - react-native-blob-util: 30a6c9fd067aadf9177e61a998f2c7efb670598d + react-native-blob-util: d8fa1a7f726867907a8e43163fdd8b441d4489ea react-native-encrypted-storage: db300a3f2f0aba1e818417c1c0a6be549038deb7 react-native-mmkv: e97c0c79403fb94577e5d902ab1ebd42b0715b43 react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc @@ -668,11 +668,11 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: b02b5075dcf60c9f5f403000b3b0c202a11b6ae1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: dc02c96b475e326a4a7b3d3912cc45cf3527bd0b - XMTPReactNative: 5c1111c5bd3456e75b3fa67d1ddccabb7a01df11 + XMTP: 9ba94e797211aa4f7cbed9ed2a2f4c44d32c8d06 + XMTPReactNative: 6f194a2f3ab388d2517f92feae01cff961ee88ab XMTPRust: 8848a2ba761b2c961d666632f2ad27d1082faa93 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 522d88edc2d5fac4825e60a121c24abc18983367 -COCOAPODS: 1.13.0 +COCOAPODS: 1.14.3 diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 88246e716..c87abeb86 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -25,5 +25,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.7.3-alpha0" + s.dependency "XMTP", "= 0.7.4-alpha0" end From 7b50ba04601a005d229b8a1bd7565c771fab18d3 Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Tue, 2 Jan 2024 20:26:30 -0700 Subject: [PATCH 3/3] Add Support for Custom Content Types when preparing messages Added logic in Conversation to handle content type in prepare method Updated bridged module to handle encrypted messages Requires iOS Sdk Change: https://github.com/xmtp/xmtp-ios/pull/212 Requires Android Sdk Change: --- .../src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index cb43fa028..6b49dfebc 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -465,7 +465,6 @@ class XMTPModule : Module() { val prepared = conversation.prepareMessage( encodedContent = encodedContent, - options = SendOptions(contentType = sending.type) ) val preparedAtMillis = prepared.envelopes[0].timestampNs / 1_000_000 val preparedFile = File.createTempFile(prepared.messageId, null)