Skip to content

Commit

Permalink
Allow custom content types to be used
Browse files Browse the repository at this point in the history
Note: This solution is only suitable for very basic content types.
For things like remote attachments, a native component will still
be necessary for performance.
  • Loading branch information
nakajima committed Nov 27, 2023
1 parent 039c854 commit 75bb183
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 264 deletions.
18 changes: 9 additions & 9 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ PODS:
- libevent (2.1.12)
- Logging (1.0.0)
- MessagePacker (0.4.7)
- MMKV (1.3.1):
- MMKVCore (~> 1.3.1)
- MMKVCore (1.3.1)
- MMKV (1.3.2):
- MMKVCore (~> 1.3.2)
- MMKVCore (1.3.2)
- RCT-Folly (2021.07.22.00):
- boost
- DoubleConversion
Expand Down Expand Up @@ -415,15 +415,15 @@ PODS:
- GenericJSON (~> 2.0)
- Logging (~> 1.0.0)
- secp256k1.swift (~> 0.1)
- XMTP (0.6.8-alpha0):
- XMTP (0.6.12-alpha0):
- Connect-Swift (= 0.3.0)
- GzipSwift
- web3.swift
- XMTPRust (= 0.3.6-beta0)
- XMTPReactNative (0.1.0):
- ExpoModulesCore
- MessagePacker
- XMTP (= 0.6.8-alpha0)
- XMTP (= 0.6.12-alpha0)
- XMTPRust (0.3.6-beta0)
- Yoga (1.14.0)

Expand Down Expand Up @@ -640,8 +640,8 @@ SPEC CHECKSUMS:
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26
MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02
MMKV: 5a07930c70c70b86cd87761a42c8f3836fb681d7
MMKVCore: e50135dbd33235b6ab390635991bab437ab873c0
MMKV: f21593c0af4b3f2a0ceb8f820f28bb639ea22bb7
MMKVCore: 31b4cb83f8266467eef20a35b6d78e409a11060d
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: 8af6a32dfc2b65ec82193c2dee6e1011ff22ac2a
RCTTypeSafety: bee9dd161c175896c680d47ef1d9eaacf2b587f4
Expand Down Expand Up @@ -680,8 +680,8 @@ SPEC CHECKSUMS:
secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634
SwiftProtobuf: b02b5075dcf60c9f5f403000b3b0c202a11b6ae1
web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959
XMTP: f13ad726c53e4c02eca780841a18b5fbd3546a49
XMTPReactNative: 37ad9f55adc7bd7dbeaa51a1410ccce3e0cbc87d
XMTP: a1c4aaae8e0b05c95223a75c055dc120962760fb
XMTPReactNative: c1cb2c303284405b3a09c0fd9b21972d03762b22
XMTPRust: 3c958736a4f4ee798e425b5644551f1c948da4b0
Yoga: 065f0b74dba4832d6e328238de46eb72c5de9556

Expand Down
135 changes: 82 additions & 53 deletions example/src/tests.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
import ReactNativeBlobUtil from "react-native-blob-util";
import * as XMTP from "../../src/index";
import { content } from "@xmtp/proto";

type EncodedContent = content.EncodedContent;
type ContentTypeId = content.ContentTypeId;

const { fs } = ReactNativeBlobUtil;

const ContentTypeNumber = {
authorityId: "xmtp.org",
typeId: "number",
versionMajor: 1,
versionMinor: 0,
};

class NumberCodec implements XMTP.JSContentCodec<number> {
contentType = ContentTypeNumber;

// a completely absurd way of encoding number values
encode(content: number): EncodedContent {
return {
type: ContentTypeNumber,
parameters: {
number: JSON.stringify(content),
},
content: new Uint8Array(),
};
}

decode(encodedContent: EncodedContent): number {
return JSON.parse(encodedContent.parameters.number) as number;
}

fallback(content: number): string | undefined {
return "a billion";
}
}

export type Test = {
name: string;
run: () => Promise<boolean>;
};

export const tests: Test[] = [];

function assert(condition: boolean, msg: string) {
if (!condition) {
throw new Error(msg);
}
}

function delayToPropogate(): Promise<void> {
// delay 1s to avoid clobbering
return new Promise((r) => setTimeout(r, 100));
Expand Down Expand Up @@ -262,38 +302,38 @@ test("can paginate batch messages", async () => {
if (messagesLimited.length !== 2) {
throw Error("Unexpected messagesLimited count " + messagesLimited.length);
}
if (messagesLimited[0].content.text !== "Message 4") {
if (messagesLimited[0].content().text !== "Message 4") {
throw Error(
"Unexpected messagesLimited content " + messagesLimited[0].content.text
"Unexpected messagesLimited content " + messagesLimited[0].content().text
);
}
if (messagesLimited[1].content.text !== "Message 3") {
if (messagesLimited[1].content().text !== "Message 3") {
throw Error(
"Unexpected messagesLimited content " + messagesLimited[1].content.text
"Unexpected messagesLimited content " + messagesLimited[1].content().text
);
}

if (messagesBefore.length !== 1) {
throw Error("Unexpected messagesBefore count " + messagesBefore.length);
}
if (messagesBefore[0].content.text !== "Initial Message") {
if (messagesBefore[0].content().text !== "Initial Message") {
throw Error(
"Unexpected messagesBefore content " + messagesBefore[0].content.text
"Unexpected messagesBefore content " + messagesBefore[0].content().text
);
}

if (messagesAfter.length !== 5) {
throw Error("Unexpected messagesAfter count " + messagesAfter.length);
}
if (messagesAfter[0].content.text !== "Message 4") {
if (messagesAfter[0].content().text !== "Message 4") {
throw Error(
"Unexpected messagesAfter content " + messagesAfter[0].content.text
"Unexpected messagesAfter content " + messagesAfter[0].content().text
);
}

if (messagesAsc[0].content.text !== "Initial Message") {
if (messagesAsc[0].content().text !== "Initial Message") {
throw Error(
"Unexpected messagesAsc content " + messagesAsc[0].content.text
"Unexpected messagesAsc content " + messagesAsc[0].content().text
);
}

Expand Down Expand Up @@ -383,17 +423,17 @@ test("can stream messages", async () => {
throw Error("Unexpected convo messages count " + convoMessages.length);
}
for (let i = 0; i < 5; i++) {
if (allMessages[i].content.text !== `Message ${i}`) {
if (allMessages[i].content().text !== `Message ${i}`) {
throw Error(
"Unexpected all message content " + allMessages[i].content.text
"Unexpected all message content " + allMessages[i].content().text
);
}
if (allMessages[i].topic !== bobConvo.topic) {
throw Error("Unexpected all message topic " + allMessages[i].topic);
}
if (convoMessages[i].content.text !== `Message ${i}`) {
if (convoMessages[i].content().text !== `Message ${i}`) {
throw Error(
"Unexpected convo message content " + convoMessages[i].content.text
"Unexpected convo message content " + convoMessages[i].content().text
);
}
if (convoMessages[i].topic !== bobConvo.topic) {
Expand Down Expand Up @@ -451,10 +491,10 @@ test("remote attachments should work", async () => {
if (message.contentTypeId !== "xmtp.org/remoteStaticAttachment:1.0") {
throw new Error("Expected correctly formatted typeId");
}
if (!message.content.remoteAttachment) {
if (!message.content().remoteAttachment) {
throw new Error("Expected remoteAttachment");
}
if (message.content.remoteAttachment.url !== "https://example.com/123") {
if (message.content().remoteAttachment.url !== "https://example.com/123") {
throw new Error("Expected url to match");
}

Expand All @@ -471,7 +511,7 @@ test("remote attachments should work", async () => {
// Now we can decrypt the downloaded file using the message metadata.
const attached = await alice.decryptAttachment({
encryptedLocalFileUri: downloadedFileUri,
metadata: message.content.remoteAttachment,
metadata: message.content().remoteAttachment,
});
if (attached.mimeType !== "text/plain") {
throw new Error("Expected mimeType to match");
Expand Down Expand Up @@ -633,39 +673,28 @@ test("canManagePreferences", async () => {
return true;
});

// test("register and use custom content types", async () => {
// const bob = await XMTP.Client.createRandom({ env: "local" });
// const alice = await XMTP.Client.createRandom({ env: "local" });
// const bobConvo = await bob.conversations.newConversation(alice.address);
// const aliceConvo = await alice.conversations.newConversation(bob.address);

// const reaction: Reaction = {
// reference: "abcdefg",
// action: "added",
// content: "coolcool",
// schema: "custom",
// };

// await bobConvo.send(reaction, {
// contentType: XMTP.ContentTypeReaction,
// });

// function assert(condition: boolean, msg: string) {
// if (!condition) {
// throw new Error(msg);
// }
// }

// const messages = await aliceConvo.messages();
// assert(messages.length !== 1, "did not get messages");

// const message = messages[0];
// const messageReaction: XMTP.Reaction = message.content;

// assert(
// messageReaction.reference === "abcdefg",
// "did not set reference properly"
// );

// return true;
// });
test("register and use custom content types", async () => {
const bob = await XMTP.Client.createRandom({ env: "local" });
const alice = await XMTP.Client.createRandom({ env: "local" });

bob.register(new NumberCodec());
alice.register(new NumberCodec());

const bobConvo = await bob.conversations.newConversation(alice.address);
const aliceConvo = await alice.conversations.newConversation(bob.address);

await bobConvo.sendWithJSCodec(12, ContentTypeNumber, bob);

const messages = await aliceConvo.messages();
assert(messages.length === 1, "did not get messages");

const message = messages[0];
const messageContent: number = message.content();

assert(
messageContent === 12,
"did not get content properly: " + JSON.stringify(messageContent)
);

return true;
});
23 changes: 14 additions & 9 deletions ios/Wrappers/DecodedMessageWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import XMTP
// Wrapper around XMTP.DecodedMessage to allow passing these objects back
// into react native.
struct DecodedMessageWrapper {
static func encodeToObj(_ model: XMTP.DecodedMessage) throws -> [String: Any] {
return try [
static func encodeToObj(_ model: XMTP.DecryptedMessage, client: Client) throws -> [String: Any] {
return [
"id": model.id,
"topic": model.topic,
"contentTypeId": model.encodedContent.type.description,
"content": ContentJson.fromEncoded(model.encodedContent, client: model.client).toJsonMap() as Any,
"content": try ContentJson.fromEncoded(model.encodedContent, client: client).toJsonMap() as Any,
"senderAddress": model.senderAddress,
"sent": UInt64(model.sent.timeIntervalSince1970 * 1000),
"fallback": model.fallbackContent,
"sent": UInt64(model.sentAt.timeIntervalSince1970 * 1000),
"fallback": model.encodedContent.fallback,
]
}

static func encode(_ model: XMTP.DecodedMessage) throws -> String {
let obj = try encodeToObj(model)
static func encode(_ model: XMTP.DecryptedMessage, client: Client) throws -> String {
let obj = try encodeToObj(model, client: client)
return try obj.toJson()
}
}
Expand All @@ -32,6 +32,7 @@ extension ContentTypeID {
struct ContentJson {
var type: ContentTypeID
var content: Any
var encodedContent: EncodedContent?

static var codecs: [any ContentCodec] = [
TextCodec(),
Expand All @@ -53,7 +54,7 @@ struct ContentJson {
}

static func fromEncoded(_ encoded: XMTP.EncodedContent, client: Client) throws -> ContentJson {
return try ContentJson(type: encoded.type, content: encoded.decoded(with: client))
return try ContentJson(type: encoded.type, content: encoded.decoded(with: client), encodedContent: encoded)
}

static func fromJsonObj(_ obj: [String: Any]) throws -> ContentJson {
Expand Down Expand Up @@ -158,7 +159,11 @@ struct ContentJson {
case ContentTypeReadReceipt.id where content is XMTP.ReadReceipt:
return ["readReceipt": ""]
default:
return ["unknown": ["contentTypeId": type.description]]
if let encodedContent, let encodedContentJSON = try? encodedContent.jsonString() {
return ["encoded": encodedContentJSON]
} else {
return ["unknown": ["contentTypeId": type.description]]
}
}
}
}
Expand Down
Loading

0 comments on commit 75bb183

Please sign in to comment.