Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom content types for preparing a message #568

Merged
merged 5 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -25,7 +25,7 @@
type EncodedContent = content.EncodedContent
type ContentTypeId = content.ContentTypeId

const { fs } = ReactNativeBlobUtil

Check warning on line 28 in example/src/tests/conversationTests.ts

View workflow job for this annotation

GitHub Actions / lint

'fs' is assigned a value but never used

const ContentTypeNumber: ContentTypeId = {
authorityId: 'org',
Expand Down Expand Up @@ -144,6 +144,54 @@
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
Loading