Skip to content

Commit

Permalink
Merge pull request #568 from xmtp/np/custom-content-types-prepare
Browse files Browse the repository at this point in the history
Add custom content types for preparing a message
  • Loading branch information
nplasterer authored Dec 17, 2024
2 parents d8d37aa + a7d6234 commit 7fbe833
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-drinks-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@xmtp/react-native-sdk": patch
---

Add custom content types for preparing a message
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,26 @@ class XMTPModule : Module() {
}
}

AsyncFunction("prepareEncodedMessage") Coroutine { installationId: String, conversationId: String, encodedContentData: List<Int> ->
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")
Expand Down
10 changes: 5 additions & 5 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions example/src/tests/conversationTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 25 additions & 1 deletion ios/XMTPModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ public class XMTPModule: Module {
return nil
}
}

AsyncFunction("sendEncodedContent") {
(
installationId: String, conversationId: String,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ios/XMTPReactNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 19 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,25 @@ export async function prepareMessage(
)
}

export async function prepareMessageWithContentType<T>(
installationId: InstallationId,
conversationId: ConversationId,
content: any,
codec: ContentCodec<T>
): Promise<MessageId> {
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,
>(
Expand Down
4 changes: 4 additions & 0 deletions src/lib/Conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface ConversationBase<ContentTypes extends DefaultContentTypes> {
content: ConversationSendPayload<SendContentTypes>,
opts?: SendOptions
): Promise<MessageId>
prepareMessage<SendContentTypes extends DefaultContentTypes = ContentTypes>(
content: ConversationSendPayload<SendContentTypes>,
opts?: SendOptions
): Promise<MessageId>
sync()
messages(opts?: MessagesOptions): Promise<DecodedMessageUnion<ContentTypes>[]>
streamMessages(
Expand Down
33 changes: 28 additions & 5 deletions src/lib/Dm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,13 @@ export class Dm<ContentTypes extends DefaultContentTypes = DefaultContentTypes>
*/
async prepareMessage<
SendContentTypes extends DefaultContentTypes = ContentTypes,
>(content: ConversationSendPayload<SendContentTypes>): Promise<string> {
// TODO: Enable other content types
// if (opts && opts.contentType) {
// return await this._sendWithJSCodec(content, opts.contentType)
// }
>(
content: ConversationSendPayload<SendContentTypes>,
opts?: SendOptions
): Promise<MessageId> {
if (opts && opts.contentType) {
return await this._prepareWithJSCodec(content, opts.contentType)
}

try {
if (typeof content === 'string') {
Expand All @@ -135,6 +137,27 @@ export class Dm<ContentTypes extends DefaultContentTypes = DefaultContentTypes>
}
}

private async _prepareWithJSCodec<T>(
content: T,
contentType: XMTP.ContentTypeId
): Promise<MessageId> {
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.
*
Expand Down
37 changes: 30 additions & 7 deletions src/lib/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} A Promise that resolves to a string identifier for the sent message.
* @returns {Promise<MessageId>} 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<SendContentTypes extends DefaultContentTypes = ContentTypes>(
Expand Down Expand Up @@ -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<string>} A Promise that resolves to a string identifier for the prepared message to be sent.
* @returns {Promise<MessageId>} 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<SendContentTypes>): Promise<string> {
// TODO: Enable other content types
// if (opts && opts.contentType) {
// return await this._sendWithJSCodec(content, opts.contentType)
// }
>(
content: ConversationSendPayload<SendContentTypes>,
opts?: SendOptions
): Promise<MessageId> {
if (opts && opts.contentType) {
return await this._prepareWithJSCodec(content, opts.contentType)
}

try {
if (typeof content === 'string') {
Expand All @@ -163,6 +165,27 @@ export class Group<
}
}

private async _prepareWithJSCodec<T>(
content: T,
contentType: XMTP.ContentTypeId
): Promise<MessageId> {
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.
*
Expand Down

0 comments on commit 7fbe833

Please sign in to comment.