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/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index c531c9016..6b49dfebc 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -443,6 +443,39 @@ 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, + ) + 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/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/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/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 95568074f..890947eca 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.7.3-alpha0" + s.dependency "XMTP", "= 0.7.4-alpha0" end 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 }