Skip to content

Commit

Permalink
Merge branch 'main' into cv/update-libxmtp-fork-fix-intent-filter
Browse files Browse the repository at this point in the history
  • Loading branch information
cameronvoell committed Dec 17, 2024
2 parents 0a8a6db + 7fbe833 commit cea628b
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 13 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
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
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
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 cea628b

Please sign in to comment.