From 78c98e8f05810072089031b550dda87cdab0bbe3 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Wed, 5 Jul 2023 10:45:09 -0700 Subject: [PATCH 1/8] Add Reply content type --- packages/content-type-reply/.eslintrc.cjs | 10 ++ packages/content-type-reply/LICENSE | 21 +++++ packages/content-type-reply/README.md | 93 ++++++++++++++++++ packages/content-type-reply/package.json | 94 +++++++++++++++++++ packages/content-type-reply/src/Reply.test.ts | 50 ++++++++++ packages/content-type-reply/src/Reply.ts | 52 ++++++++++ packages/content-type-reply/src/index.ts | 2 + .../content-type-reply/tsconfig.eslint.json | 5 + packages/content-type-reply/tsconfig.json | 5 + packages/content-type-reply/tsup.config.ts | 35 +++++++ packages/content-type-reply/vitest.config.ts | 8 ++ packages/content-type-reply/vitest.setup.ts | 5 + yarn.lock | 24 +++++ 13 files changed, 404 insertions(+) create mode 100644 packages/content-type-reply/.eslintrc.cjs create mode 100644 packages/content-type-reply/LICENSE create mode 100644 packages/content-type-reply/README.md create mode 100644 packages/content-type-reply/package.json create mode 100644 packages/content-type-reply/src/Reply.test.ts create mode 100644 packages/content-type-reply/src/Reply.ts create mode 100644 packages/content-type-reply/src/index.ts create mode 100644 packages/content-type-reply/tsconfig.eslint.json create mode 100644 packages/content-type-reply/tsconfig.json create mode 100644 packages/content-type-reply/tsup.config.ts create mode 100644 packages/content-type-reply/vitest.config.ts create mode 100644 packages/content-type-reply/vitest.setup.ts diff --git a/packages/content-type-reply/.eslintrc.cjs b/packages/content-type-reply/.eslintrc.cjs new file mode 100644 index 0000000..0c96fc0 --- /dev/null +++ b/packages/content-type-reply/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, + rules: { + "class-methods-use-this": "off", + }, +}; diff --git a/packages/content-type-reply/LICENSE b/packages/content-type-reply/LICENSE new file mode 100644 index 0000000..7e2c80a --- /dev/null +++ b/packages/content-type-reply/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 XMTP developers (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/content-type-reply/README.md b/packages/content-type-reply/README.md new file mode 100644 index 0000000..e9f69c2 --- /dev/null +++ b/packages/content-type-reply/README.md @@ -0,0 +1,93 @@ +# Reply content type + +This package provides an XMTP content type to support replying to a message. + +## What’s a reply? + +... + +## Why replies? + +... + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-reply + +# yarn +yarn add @xmtp/content-type-reply + +# pnpm +pnpm i @xmtp/content-type-reply +``` + +## Create a reply + +With XMTP, replys are represented as objects with the following keys: + +- `reference`: The message ID for the message that is being reacted to +- `content`: A string representation of the reply + +```tsx +const reply: Reply = { + reference: someMessageID, + content: "I concur", +}; +``` + +## Send a reply + +Now that you have a reply, you can send it: + +```tsx +await conversation.messages.send(reply, { + contentType: ContentTypeReaction, + contentFallback: `[Reply] ${client.address} replied to ${someMessage.content} with:\n\n${reply.content}`, +}); +``` + +Note that we’re using `contentFallback` to enable clients that don't support these content types to still display something. For cases where clients *do* support these types, they can use the content fallback as alt text for accessibility purposes. + +## Receive a reply + +Now that you can send a reply, you need a way to receive a reply. For example: + +```tsx +// Assume `loadLastMessage` is a thing you have +const message: DecodedMessage = await loadLastMessage(); + +if (!message.contentType.sameAs(ContentTypeReply)) { + // We do not have a reply. A topic for another blog post. + return; +} + +// We've got a reply. +const reply: Reply = message.content; +``` + +## Display the reply + +Generally, replys should be displayed alongside the original message to provide context. Ultimately, how you choose to display replies is completely up to you. + +## Developing + +Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. + +## Testing + +Before running unit tests, start the required Docker container at the root of this repository. For more info, see [Running tests](../../README.md#running-tests). + +## Useful commands + +- `yarn build`: Builds the content type +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn format`: Runs prettier format and write changes +- `yarn format:check`: Runs prettier format check +- `yarn lint`: Runs ESLint +- `yarn test:setup`: Starts a necessary docker container for testing +- `yarn test:teardown`: Stops docker container for testing +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/packages/content-type-reply/package.json b/packages/content-type-reply/package.json new file mode 100644 index 0000000..acacdda --- /dev/null +++ b/packages/content-type-reply/package.json @@ -0,0 +1,94 @@ +{ + "name": "@xmtp/content-type-reply", + "version": "1.0.0", + "description": "An XMTP content type to support replying to a message", + "author": "XMTP Labs ", + "license": "MIT", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "browser": "dist/web/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/web/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "sideEffects": false, + "repository": { + "type": "git", + "url": "git@github.com:xmtp/xmtp-js-content-types.git", + "directory": "packages/content-type-reply" + }, + "homepage": "https://github.com/xmtp/xmtp-js-content-types", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js-content-types/issues" + }, + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "publishConfig": { + "access": "public" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "scripts": { + "build:node": "tsup", + "build:web": "tsup --platform browser --target esnext", + "build": "yarn clean:build && yarn build:node && yarn build:web", + "clean:build": "rimraf dist", + "clean": "rimraf .turbo node_modules && yarn clean:build", + "dev": "tsup --watch", + "format:base": "prettier --ignore-path ../../.gitignore", + "format:check": "yarn format:base -c .", + "format": "yarn format:base -w .", + "generate:types": "tsup --dts-only", + "lint": "eslint . --ignore-path ../../.gitignore", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment jsdom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "test": "yarn test:node && yarn test:jsdom", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@xmtp/xmtp-js": "^9.1.7" + }, + "devDependencies": { + "@types/node": "^18.14.2", + "buffer": "^6.0.3", + "esbuild": "^0.18.2", + "esbuild-plugin-external-global": "^1.0.1", + "eslint": "^8.43.0", + "eslint-config-custom": "workspace:*", + "ethers": "^6.0.8", + "jsdom": "^22.1.0", + "prettier": "^2.8.8", + "rimraf": "^5.0.1", + "tsup": "^7.0.0", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vitest": "^0.32.2" + }, + "peerDependencies": { + "@xmtp/xmtp-js": "^9.1.7" + } +} diff --git a/packages/content-type-reply/src/Reply.test.ts b/packages/content-type-reply/src/Reply.test.ts new file mode 100644 index 0000000..ddf2aca --- /dev/null +++ b/packages/content-type-reply/src/Reply.test.ts @@ -0,0 +1,50 @@ +import { Wallet } from "ethers"; +import { Client } from "@xmtp/xmtp-js"; +import { ContentTypeReply, ReplyCodec } from "./Reply"; +import type { Reply } from "./Reply"; + +describe("ReplyContentType", () => { + it("has the right content type", () => { + expect(ContentTypeReply.authorityId).toBe("xmtp.org"); + expect(ContentTypeReply.typeId).toBe("reply"); + expect(ContentTypeReply.versionMajor).toBe(1); + expect(ContentTypeReply.versionMinor).toBe(0); + }); + + it("can send a reaction", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { env: "local" }); + aliceClient.registerCodec(new ReplyCodec()); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { env: "local" }); + bobClient.registerCodec(new ReplyCodec()); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const originalMessage = await conversation.send("test"); + + const reaction: Reply = { + content: "LGTM", + reference: originalMessage.id, + }; + + await conversation.send(reaction, { contentType: ContentTypeReply }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(2); + + const reactionMessage = messages[1]; + const messageContent = reactionMessage.content as Reply; + expect(messageContent.content).toBe("LGTM"); + expect(messageContent.reference).toBe(originalMessage.id); + }); +}); diff --git a/packages/content-type-reply/src/Reply.ts b/packages/content-type-reply/src/Reply.ts new file mode 100644 index 0000000..5edb2e5 --- /dev/null +++ b/packages/content-type-reply/src/Reply.ts @@ -0,0 +1,52 @@ +import { ContentTypeId } from "@xmtp/xmtp-js"; +import type { ContentCodec, EncodedContent } from "@xmtp/xmtp-js"; + +export const ContentTypeReply = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "reply", + versionMajor: 1, + versionMinor: 0, +}); + +export type Reply = { + /** + * The message ID for the message that is being replied to + */ + reference: string; + /** + * The content of the reply + */ + content: string; +}; + +export type ReplyParameters = Pick & { + encoding: "UTF-8"; +}; + +export class ReplyCodec implements ContentCodec { + get contentType(): ContentTypeId { + return ContentTypeReply; + } + + encode(content: Reply): EncodedContent { + return { + type: ContentTypeReply, + parameters: { + encoding: "UTF-8", + reference: content.reference, + }, + content: new TextEncoder().encode(content.content), + }; + } + + decode(content: EncodedContent): Reply { + const { encoding } = content.parameters; + if (encoding && encoding !== "UTF-8") { + throw new Error(`unrecognized encoding ${encoding as string}`); + } + return { + reference: content.parameters.reference, + content: new TextDecoder().decode(content.content), + }; + } +} diff --git a/packages/content-type-reply/src/index.ts b/packages/content-type-reply/src/index.ts new file mode 100644 index 0000000..acc5508 --- /dev/null +++ b/packages/content-type-reply/src/index.ts @@ -0,0 +1,2 @@ +export { ReplyCodec, ContentTypeReply } from "./Reply"; +export type { Reply, ReplyParameters } from "./Reply"; diff --git a/packages/content-type-reply/tsconfig.eslint.json b/packages/content-type-reply/tsconfig.eslint.json new file mode 100644 index 0000000..bdcd7f1 --- /dev/null +++ b/packages/content-type-reply/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/content-type-reply/tsconfig.json b/packages/content-type-reply/tsconfig.json new file mode 100644 index 0000000..5217e9f --- /dev/null +++ b/packages/content-type-reply/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["."], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/content-type-reply/tsup.config.ts b/packages/content-type-reply/tsup.config.ts new file mode 100644 index 0000000..b32be10 --- /dev/null +++ b/packages/content-type-reply/tsup.config.ts @@ -0,0 +1,35 @@ +import { defineConfig } from "tsup"; +import externalGlobal from "esbuild-plugin-external-global"; +import type { Plugin } from "esbuild"; + +export default defineConfig((options) => { + const esbuildPlugins: Plugin[] = []; + + // for the browser bundle, replace `crypto` import with an object that + // returns the browser's built-in crypto library + if (options.platform === "browser") { + esbuildPlugins.push( + externalGlobal.externalGlobalPlugin({ + crypto: "{ webcrypto: window.crypto }", + }), + ); + } + + return { + entry: options.entry ?? ["src/index.ts"], + outDir: + options.outDir ?? (options.platform === "browser" ? "dist/web" : "dist"), + splitting: false, + sourcemap: true, + treeshake: true, + clean: true, + bundle: true, + platform: options.platform ?? "node", + minify: options.platform === "browser", + dts: options.platform !== "browser", + format: + options.format ?? + (options.platform === "browser" ? ["esm"] : ["esm", "cjs"]), + esbuildPlugins, + }; +}); diff --git a/packages/content-type-reply/vitest.config.ts b/packages/content-type-reply/vitest.config.ts new file mode 100644 index 0000000..2ee9019 --- /dev/null +++ b/packages/content-type-reply/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + setupFiles: "./vitest.setup.ts", + }, +}); diff --git a/packages/content-type-reply/vitest.setup.ts b/packages/content-type-reply/vitest.setup.ts new file mode 100644 index 0000000..8989be9 --- /dev/null +++ b/packages/content-type-reply/vitest.setup.ts @@ -0,0 +1,5 @@ +import { Buffer } from "buffer"; +import { webcrypto } from "crypto"; + +globalThis.Buffer = Buffer; +globalThis.crypto = webcrypto as unknown as Crypto; diff --git a/yarn.lock b/yarn.lock index 755093f..3ebccc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1748,6 +1748,30 @@ __metadata: languageName: unknown linkType: soft +"@xmtp/content-type-reply@workspace:packages/content-type-reply": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-reply@workspace:packages/content-type-reply" + dependencies: + "@types/node": ^18.14.2 + "@xmtp/xmtp-js": ^9.1.7 + buffer: ^6.0.3 + esbuild: ^0.18.2 + esbuild-plugin-external-global: ^1.0.1 + eslint: ^8.43.0 + eslint-config-custom: "workspace:*" + ethers: ^6.0.8 + jsdom: ^22.1.0 + prettier: ^2.8.8 + rimraf: ^5.0.1 + tsup: ^7.0.0 + typescript: ^5.1.3 + vite: ^4.3.9 + vitest: ^0.32.2 + peerDependencies: + "@xmtp/xmtp-js": ^9.1.7 + languageName: unknown + linkType: soft + "@xmtp/proto@npm:^3.24.0, @xmtp/proto@npm:^3.25.0": version: 3.25.0 resolution: "@xmtp/proto@npm:3.25.0" From 2029427b5083b0e811d379c63da29fde3daa8458 Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Wed, 5 Jul 2023 11:27:56 -0700 Subject: [PATCH 2/8] docs: some tw edits --- packages/content-type-reaction/README.md | 3 ++- packages/content-type-reply/README.md | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/content-type-reaction/README.md b/packages/content-type-reaction/README.md index c28a341..23c5a85 100644 --- a/packages/content-type-reaction/README.md +++ b/packages/content-type-reaction/README.md @@ -50,7 +50,8 @@ await conversation.messages.send(reaction, { }); ``` -Note that we’re using `contentFallback` to enable clients that don't support these content types to still display something. For cases where clients *do* support these types, they can use the content fallback as alt text for accessibility purposes. +> **Note** +> Use `contentFallback` to enable clients that don't support these content types to still display some useful context. For cases where clients *do* support these types, they can use the content fallback as alt text for accessibility purposes. ## Receive a reaction diff --git a/packages/content-type-reply/README.md b/packages/content-type-reply/README.md index e9f69c2..670729c 100644 --- a/packages/content-type-reply/README.md +++ b/packages/content-type-reply/README.md @@ -1,14 +1,14 @@ # Reply content type -This package provides an XMTP content type to support replying to a message. +This package provides an XMTP content type to support direct replies to messages. ## What’s a reply? -... +A reply action is a way to respond directly to a specific message in a conversation. Instead of sending a new message, users can select and reply to a particular message. ## Why replies? -... +Providing replies in your app enables users to maintain context and clarity in their conversations. Replies can also help organize messages, making messages easier to find and reference in the future. This user experience can help make your app a great tool for collaboration. ## Install the package @@ -25,7 +25,7 @@ pnpm i @xmtp/content-type-reply ## Create a reply -With XMTP, replys are represented as objects with the following keys: +With XMTP, replies are represented as objects with the following keys: - `reference`: The message ID for the message that is being reacted to - `content`: A string representation of the reply @@ -48,7 +48,8 @@ await conversation.messages.send(reply, { }); ``` -Note that we’re using `contentFallback` to enable clients that don't support these content types to still display something. For cases where clients *do* support these types, they can use the content fallback as alt text for accessibility purposes. +> **Note** +> Use `contentFallback` to enable clients that don't support these content types to still display some useful context. For cases where clients *do* support these types, they can use the content fallback as alt text for accessibility purposes. ## Receive a reply @@ -69,7 +70,7 @@ const reply: Reply = message.content; ## Display the reply -Generally, replys should be displayed alongside the original message to provide context. Ultimately, how you choose to display replies is completely up to you. +Generally, replies should be displayed alongside the original message to provide context. Ultimately, how you choose to display replies is completely up to you. ## Developing @@ -84,10 +85,10 @@ Before running unit tests, start the required Docker container at the root of th - `yarn build`: Builds the content type - `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders - `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild -- `yarn format`: Runs prettier format and write changes -- `yarn format:check`: Runs prettier format check +- `yarn format`: Runs Prettier format and write changes +- `yarn format:check`: Runs Prettier format check - `yarn lint`: Runs ESLint -- `yarn test:setup`: Starts a necessary docker container for testing -- `yarn test:teardown`: Stops docker container for testing +- `yarn test:setup`: Starts a necessary Docker container for testing +- `yarn test:teardown`: Stops Docker container for testing - `yarn test`: Runs all unit tests - `yarn typecheck`: Runs `tsc` From 17cb0338a1295147c4de228d9e831f15014887b3 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Wed, 5 Jul 2023 11:56:27 -0700 Subject: [PATCH 3/8] Fix example code --- packages/content-type-reply/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/content-type-reply/README.md b/packages/content-type-reply/README.md index 670729c..3579fa4 100644 --- a/packages/content-type-reply/README.md +++ b/packages/content-type-reply/README.md @@ -43,7 +43,7 @@ Now that you have a reply, you can send it: ```tsx await conversation.messages.send(reply, { - contentType: ContentTypeReaction, + contentType: ContentTypeReply, contentFallback: `[Reply] ${client.address} replied to ${someMessage.content} with:\n\n${reply.content}`, }); ``` From ebe4afd0795c576f27e6864e8751b6ee8ad1df15 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Wed, 5 Jul 2023 11:58:54 -0700 Subject: [PATCH 4/8] `reaction` => `reply` --- packages/content-type-reply/src/Reply.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/content-type-reply/src/Reply.test.ts b/packages/content-type-reply/src/Reply.test.ts index ddf2aca..11f3557 100644 --- a/packages/content-type-reply/src/Reply.test.ts +++ b/packages/content-type-reply/src/Reply.test.ts @@ -11,7 +11,7 @@ describe("ReplyContentType", () => { expect(ContentTypeReply.versionMinor).toBe(0); }); - it("can send a reaction", async () => { + it("can send a reply", async () => { const aliceWallet = Wallet.createRandom(); const aliceClient = await Client.create(aliceWallet, { env: "local" }); aliceClient.registerCodec(new ReplyCodec()); @@ -28,12 +28,12 @@ describe("ReplyContentType", () => { const originalMessage = await conversation.send("test"); - const reaction: Reply = { + const reply: Reply = { content: "LGTM", reference: originalMessage.id, }; - await conversation.send(reaction, { contentType: ContentTypeReply }); + await conversation.send(reply, { contentType: ContentTypeReply }); const bobConversation = await bobClient.conversations.newConversation( aliceWallet.address, @@ -42,8 +42,8 @@ describe("ReplyContentType", () => { expect(messages.length).toBe(2); - const reactionMessage = messages[1]; - const messageContent = reactionMessage.content as Reply; + const replyMessage = messages[1]; + const messageContent = replyMessage.content as Reply; expect(messageContent.content).toBe("LGTM"); expect(messageContent.reference).toBe(originalMessage.id); }); From 850dd3643a4d8d0470c2fb8bf6f1063ee25656bf Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Wed, 5 Jul 2023 16:56:14 -0700 Subject: [PATCH 5/8] Allow for any content in replies --- packages/content-type-reply/package.json | 6 +- packages/content-type-reply/src/Reply.test.ts | 61 +++++++++++++++++- packages/content-type-reply/src/Reply.ts | 63 +++++++++++++++---- yarn.lock | 34 +++++++++- 4 files changed, 146 insertions(+), 18 deletions(-) diff --git a/packages/content-type-reply/package.json b/packages/content-type-reply/package.json index acacdda..774858f 100644 --- a/packages/content-type-reply/package.json +++ b/packages/content-type-reply/package.json @@ -70,10 +70,12 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@xmtp/xmtp-js": "^9.1.7" + "@xmtp/proto": "^3.26.0", + "@xmtp/xmtp-js": "^9.2.0" }, "devDependencies": { "@types/node": "^18.14.2", + "@xmtp/content-type-remote-attachment": "workspace:*", "buffer": "^6.0.3", "esbuild": "^0.18.2", "esbuild-plugin-external-global": "^1.0.1", @@ -89,6 +91,6 @@ "vitest": "^0.32.2" }, "peerDependencies": { - "@xmtp/xmtp-js": "^9.1.7" + "@xmtp/xmtp-js": "^9.2.0" } } diff --git a/packages/content-type-reply/src/Reply.test.ts b/packages/content-type-reply/src/Reply.test.ts index 11f3557..3043298 100644 --- a/packages/content-type-reply/src/Reply.test.ts +++ b/packages/content-type-reply/src/Reply.test.ts @@ -1,5 +1,10 @@ import { Wallet } from "ethers"; -import { Client } from "@xmtp/xmtp-js"; +import { Client, ContentTypeText } from "@xmtp/xmtp-js"; +import type { Attachment } from "@xmtp/content-type-remote-attachment"; +import { + AttachmentCodec, + ContentTypeAttachment, +} from "@xmtp/content-type-remote-attachment"; import { ContentTypeReply, ReplyCodec } from "./Reply"; import type { Reply } from "./Reply"; @@ -11,7 +16,7 @@ describe("ReplyContentType", () => { expect(ContentTypeReply.versionMinor).toBe(0); }); - it("can send a reply", async () => { + it("can send a text reply", async () => { const aliceWallet = Wallet.createRandom(); const aliceClient = await Client.create(aliceWallet, { env: "local" }); aliceClient.registerCodec(new ReplyCodec()); @@ -30,6 +35,7 @@ describe("ReplyContentType", () => { const reply: Reply = { content: "LGTM", + contentType: ContentTypeText, reference: originalMessage.id, }; @@ -47,4 +53,55 @@ describe("ReplyContentType", () => { expect(messageContent.content).toBe("LGTM"); expect(messageContent.reference).toBe(originalMessage.id); }); + + it("can send an attachment reply", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { env: "local" }); + aliceClient.registerCodec(new ReplyCodec()); + aliceClient.registerCodec(new AttachmentCodec()); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { env: "local" }); + bobClient.registerCodec(new ReplyCodec()); + bobClient.registerCodec(new AttachmentCodec()); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const originalMessage = await conversation.send("test"); + + const attachment: Attachment = { + filename: "test.png", + mimeType: "image/png", + data: Uint8Array.from([5, 4, 3, 2, 1]), + }; + + const reply: Reply = { + content: attachment, + contentType: ContentTypeAttachment, + reference: originalMessage.id, + }; + + await conversation.send(reply, { contentType: ContentTypeReply }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(2); + + const replyMessage = messages[1]; + const messageContent = replyMessage.content as Reply; + expect(ContentTypeAttachment.sameAs(messageContent.contentType)).toBe(true); + expect(messageContent.content).toEqual({ + filename: "test.png", + mimeType: "image/png", + data: Uint8Array.from([5, 4, 3, 2, 1]), + }); + expect(messageContent.reference).toBe(originalMessage.id); + }); }); diff --git a/packages/content-type-reply/src/Reply.ts b/packages/content-type-reply/src/Reply.ts index 5edb2e5..66059b4 100644 --- a/packages/content-type-reply/src/Reply.ts +++ b/packages/content-type-reply/src/Reply.ts @@ -1,5 +1,10 @@ import { ContentTypeId } from "@xmtp/xmtp-js"; -import type { ContentCodec, EncodedContent } from "@xmtp/xmtp-js"; +import type { + CodecRegistry, + ContentCodec, + EncodedContent, +} from "@xmtp/xmtp-js"; +import { composite as proto } from "@xmtp/proto"; export const ContentTypeReply = new ContentTypeId({ authorityId: "xmtp.org", @@ -16,11 +21,15 @@ export type Reply = { /** * The content of the reply */ - content: string; + content: any; + /** + * The content type of the reply + */ + contentType: ContentTypeId; }; export type ReplyParameters = Pick & { - encoding: "UTF-8"; + contentType: string; }; export class ReplyCodec implements ContentCodec { @@ -28,25 +37,57 @@ export class ReplyCodec implements ContentCodec { return ContentTypeReply; } - encode(content: Reply): EncodedContent { + encode( + content: Reply, + codecs: CodecRegistry, + ): EncodedContent { + const codec = codecs.codecFor(content.contentType); + if (!codec) { + throw new Error( + `missing codec for content type "${content.contentType.toString()}"`, + ); + } + + const bytes = proto.Composite.encode({ + parts: [ + { + part: codec.encode(content.content, codecs), + composite: undefined, + }, + ], + }).finish(); + return { type: ContentTypeReply, parameters: { - encoding: "UTF-8", + contentType: content.contentType.toString(), reference: content.reference, }, - content: new TextEncoder().encode(content.content), + content: bytes, }; } - decode(content: EncodedContent): Reply { - const { encoding } = content.parameters; - if (encoding && encoding !== "UTF-8") { - throw new Error(`unrecognized encoding ${encoding as string}`); + decode( + content: EncodedContent, + codecs: CodecRegistry, + ): Reply { + const composite = proto.Composite.decode(content.content); + const contentType = ContentTypeId.fromString( + content.parameters.contentType, + ); + + const codec = codecs.codecFor(contentType); + if (!codec) { + throw new Error( + `missing codec for content type "${content.parameters.contentType}"`, + ); } + return { reference: content.parameters.reference, - content: new TextDecoder().decode(content.content), + contentType, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + content: codec.decode(composite.parts[0].part as EncodedContent, codecs), }; } } diff --git a/yarn.lock b/yarn.lock index 3ebccc9..b811990 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,7 +1722,7 @@ __metadata: languageName: unknown linkType: soft -"@xmtp/content-type-remote-attachment@workspace:packages/content-type-remote-attachment": +"@xmtp/content-type-remote-attachment@workspace:^, @xmtp/content-type-remote-attachment@workspace:packages/content-type-remote-attachment": version: 0.0.0-use.local resolution: "@xmtp/content-type-remote-attachment@workspace:packages/content-type-remote-attachment" dependencies: @@ -1753,7 +1753,9 @@ __metadata: resolution: "@xmtp/content-type-reply@workspace:packages/content-type-reply" dependencies: "@types/node": ^18.14.2 - "@xmtp/xmtp-js": ^9.1.7 + "@xmtp/content-type-remote-attachment": "workspace:^" + "@xmtp/proto": ^3.26.0 + "@xmtp/xmtp-js": ^9.2.0 buffer: ^6.0.3 esbuild: ^0.18.2 esbuild-plugin-external-global: ^1.0.1 @@ -1768,7 +1770,7 @@ __metadata: vite: ^4.3.9 vitest: ^0.32.2 peerDependencies: - "@xmtp/xmtp-js": ^9.1.7 + "@xmtp/xmtp-js": ^9.2.0 languageName: unknown linkType: soft @@ -1784,6 +1786,18 @@ __metadata: languageName: node linkType: hard +"@xmtp/proto@npm:^3.26.0": + version: 3.26.0 + resolution: "@xmtp/proto@npm:3.26.0" + dependencies: + long: ^5.2.0 + protobufjs: ^7.0.0 + rxjs: ^7.8.0 + undici: ^5.8.1 + checksum: 2d29c5fdcaa3332577fac20a65a7ca837e937b8aab5c6bb2f20a198e5d07c9d76b534676fcc98b5375ef00f5f9f54f858d55524b3da40b8fe1db4225e1c4fdd7 + languageName: node + linkType: hard + "@xmtp/xmtp-js-content-types@workspace:.": version: 0.0.0-use.local resolution: "@xmtp/xmtp-js-content-types@workspace:." @@ -1812,6 +1826,20 @@ __metadata: languageName: node linkType: hard +"@xmtp/xmtp-js@npm:^9.2.0": + version: 9.2.0 + resolution: "@xmtp/xmtp-js@npm:9.2.0" + dependencies: + "@noble/secp256k1": ^1.5.2 + "@xmtp/proto": ^3.24.0 + async-mutex: ^0.4.0 + elliptic: ^6.5.4 + ethers: ^5.5.3 + long: ^5.2.0 + checksum: a2f1c4061bd2c5325bd0ebcc59c0b72b30995f04f4838ea5caa3901f46f616862b26f61a1b9a21cb5933d0ad6a8ca56fde86df4256678aa7a6f1309caf04d42d + languageName: node + linkType: hard + "abab@npm:^2.0.6": version: 2.0.6 resolution: "abab@npm:2.0.6" From dfc2fb645ab9191443627f5fe91678785767ace4 Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Wed, 5 Jul 2023 20:59:25 -0700 Subject: [PATCH 6/8] Change encoding --- packages/content-type-reply/src/Reply.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/content-type-reply/src/Reply.ts b/packages/content-type-reply/src/Reply.ts index 66059b4..b9ea875 100644 --- a/packages/content-type-reply/src/Reply.ts +++ b/packages/content-type-reply/src/Reply.ts @@ -4,7 +4,7 @@ import type { ContentCodec, EncodedContent, } from "@xmtp/xmtp-js"; -import { composite as proto } from "@xmtp/proto"; +import { content as proto } from "@xmtp/proto"; export const ContentTypeReply = new ContentTypeId({ authorityId: "xmtp.org", @@ -48,14 +48,8 @@ export class ReplyCodec implements ContentCodec { ); } - const bytes = proto.Composite.encode({ - parts: [ - { - part: codec.encode(content.content, codecs), - composite: undefined, - }, - ], - }).finish(); + const encodedContent = codec.encode(content.content, codecs); + const bytes = proto.EncodedContent.encode(encodedContent).finish(); return { type: ContentTypeReply, @@ -71,7 +65,7 @@ export class ReplyCodec implements ContentCodec { content: EncodedContent, codecs: CodecRegistry, ): Reply { - const composite = proto.Composite.decode(content.content); + const decodedContent = proto.EncodedContent.decode(content.content); const contentType = ContentTypeId.fromString( content.parameters.contentType, ); @@ -87,7 +81,7 @@ export class ReplyCodec implements ContentCodec { reference: content.parameters.reference, contentType, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - content: codec.decode(composite.parts[0].part as EncodedContent, codecs), + content: codec.decode(decodedContent as EncodedContent, codecs), }; } } From ba784c0d15c3e7c17872585f33e6d6c95680301f Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Thu, 6 Jul 2023 09:20:37 -0700 Subject: [PATCH 7/8] Fix yarn.lock --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index b811990..8fe531a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,7 +1722,7 @@ __metadata: languageName: unknown linkType: soft -"@xmtp/content-type-remote-attachment@workspace:^, @xmtp/content-type-remote-attachment@workspace:packages/content-type-remote-attachment": +"@xmtp/content-type-remote-attachment@workspace:*, @xmtp/content-type-remote-attachment@workspace:packages/content-type-remote-attachment": version: 0.0.0-use.local resolution: "@xmtp/content-type-remote-attachment@workspace:packages/content-type-remote-attachment" dependencies: @@ -1753,7 +1753,7 @@ __metadata: resolution: "@xmtp/content-type-reply@workspace:packages/content-type-reply" dependencies: "@types/node": ^18.14.2 - "@xmtp/content-type-remote-attachment": "workspace:^" + "@xmtp/content-type-remote-attachment": "workspace:*" "@xmtp/proto": ^3.26.0 "@xmtp/xmtp-js": ^9.2.0 buffer: ^6.0.3 From d13a9d1128c45dbfa50549b9aa4543126fad462a Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Thu, 6 Jul 2023 09:48:45 -0700 Subject: [PATCH 8/8] Fix tests --- turbo.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/turbo.json b/turbo.json index d004a82..4cfc34b 100644 --- a/turbo.json +++ b/turbo.json @@ -30,6 +30,10 @@ "test": { "outputs": [] }, + "@xmtp/content-type-reply#test": { + "dependsOn": ["@xmtp/content-type-remote-attachment#build"], + "outputs": [] + }, "typecheck": { "dependsOn": ["^generate:types"], "outputs": []