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

Custom content types #155

Merged
merged 38 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
039c854
everything is broken
nakajima Nov 20, 2023
75bb183
Allow custom content types to be used
nakajima Nov 27, 2023
bf1d669
Merge branch 'main' of https://github.com/xmtp/xmtp-react-native into…
nakajima Nov 27, 2023
abcf8f4
Delete plan.md
nakajima Nov 27, 2023
47b638a
bring the android code up to the ios side of custom content types
nplasterer Nov 28, 2023
46275c0
Add eslint
nakajima Nov 28, 2023
d29a870
Merge branch 'custom-content-types' of https://github.com/xmtp/xmtp-r…
nakajima Nov 28, 2023
3daedd9
add lint ci
nakajima Nov 28, 2023
36bbc0e
fix name
nakajima Nov 28, 2023
49a270c
sync package-lock
nakajima Nov 28, 2023
22213a6
sync lockfile?
nakajima Nov 28, 2023
e3fb360
get the app building
nplasterer Nov 28, 2023
25541cc
Merge branch 'custom-content-types' of https://github.com/xmtp/xmtp-r…
nplasterer Nov 28, 2023
a927f3d
fix tests
nakajima Nov 28, 2023
dfe6759
remove unused function
nakajima Nov 28, 2023
aca99cd
wip
nakajima Nov 29, 2023
402b3f3
Improve type safety of DecodedMessage#content()
nakajima Nov 29, 2023
90c449a
Merge branch 'main' of https://github.com/xmtp/xmtp-react-native into…
nakajima Nov 30, 2023
0576065
fix lint
nakajima Nov 30, 2023
e77a713
fix up the json parsing
nplasterer Nov 30, 2023
fd1c631
a small tweak to the json
nplasterer Nov 30, 2023
caee68c
Merge pull request #156 from xmtp/np/custom-content-types-android
nplasterer Nov 30, 2023
3b69fb1
Use Conversation#send instead of new method for sending with custom c…
nakajima Nov 30, 2023
00415de
make an update to the readme
nplasterer Nov 30, 2023
bdec8d9
remove extra space
nplasterer Nov 30, 2023
bbea7f9
Update src/lib/NativeCodecs/ReadReceiptCodec.ts
nplasterer Nov 30, 2023
68152b9
Fix example app
nakajima Nov 30, 2023
bf95df0
improve this
nakajima Nov 30, 2023
db7e361
Merge branch 'custom-content-types' of https://github.com/xmtp/xmtp-r…
nakajima Nov 30, 2023
c6d8533
fix up the reaction codec
nplasterer Dec 1, 2023
997b3e4
fixes
nakajima Dec 1, 2023
e8d4246
fix up the reply codec
nplasterer Dec 1, 2023
ab149ce
Get closer on replies
nakajima Dec 1, 2023
0ced1a2
Merge branch 'custom-content-types' of https://github.com/xmtp/xmtp-r…
nakajima Dec 1, 2023
846cd7f
fix up the reply rendering
nplasterer Dec 1, 2023
d24af88
another pass on the readme
nplasterer Dec 1, 2023
50c438a
Fix TS in codec and make replies easier to distinguish
nakajima Dec 1, 2023
ccf5e75
BREAKING CHANGE: Codecs no longer auto register and must be manually …
nplasterer Dec 2, 2023
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
10 changes: 7 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module.exports = {
root: true,
extends: ["universe/native", "universe/web"],
ignorePatterns: ["build"],
};
extends: ['universe/native', 'universe/web'],
ignorePatterns: ['build'],
plugins: ['prettier'],
globals: {
__dirname: true,
},
}
13 changes: 13 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Lint
on:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v3
- run: npm install
- run: npm run eslint
7 changes: 7 additions & 0 deletions .prettierrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'es5',
arrowParens: 'always',
printWidth: 80,
}
1 change: 1 addition & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--indent tab
42 changes: 30 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ The client's network connection and key storage method can be configured with th
| ------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| appVersion | `undefined` | Add a client app version identifier that's included with API requests.<br/>For example, you can use the following format: `appVersion: APP_NAME + '/' + APP_VERSION`.<br/>Setting this value provides telemetry that shows which apps are using the XMTP client SDK. This information can help XMTP developers provide app support, especially around communicating important SDK updates, including deprecations and required upgrades. |
| env | `dev` | Connect to the specified XMTP network environment. Valid values include `dev`, `production`, or `local`. For important details about working with these environments, see [XMTP `production` and `dev` network environments](#xmtp-production-and-dev-network-environments). |
| codecs | `[new XMTP.ReactionCodec()]` | Add codecs to support additional content types. |

## Handle conversations

Expand Down Expand Up @@ -327,23 +328,40 @@ All send functions support `SendOptions` as an optional parameter. The `contentT

To learn more about content types, see [Content types with XMTP](https://xmtp.org/docs/concepts/content-types).

The SDK preregisters the following codecs:
Support for other types of content can be added by registering additional `ContentCodecs` with the `Client`. Every codec is associated with a content type identifier, `ContentTypeId`, which is used to signal to the client which codec should be used to process the content that is being sent or received.
For example, see the [Native Codecs](https://github.com/xmtp/xmtp-react-native/tree/main/src/lib/NativeCodecs) available in `xmtp-react-native`.

- For [Android](https://github.com/xmtp/xmtp-react-native/blob/main/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt#L43-L53), using these [source codecs](https://github.com/xmtp/xmtp-android/tree/main/library/src/main/java/org/xmtp/android/library/codecs).
```ts
// Assuming we've loaded a fictional NumberCodec that can be used to encode numbers,
// and is identified with ContentTypeNumber, we can use it as follows.

- For [iOS](https://github.com/xmtp/xmtp-react-native/blob/main/ios/Wrappers/DecodedMessageWrapper.swift#L35-L48), using these [source codecs](https://github.com/xmtp/xmtp-ios/tree/main/Sources/XMTP/Codecs).
xmtp.register(new NumberCodec())
conversation.send(3.14, {
contentType: ContentTypeNumber
})
```

```tsx
await conversation.send({
reaction: {
reference: otherMessage.id,
action: "added",
schema: "unicode",
content: "💖",
},
});
Additional codecs can be configured through the `ClientOptions` parameter of `Client.create`. The `codecs` option is a list of codec instances that should be added to the default set of codecs (currently only the `TextCodec`). If a codec is added for a content type that is already in the default set, it will replace the original codec.

```ts
// Adding support for `xmtp.org/reaction` content type
import { ReactionCodec } from '@xmtp/react-native-sdk'
const xmtp = Client.create(wallet, { codecs: [new ReactionCodec()] })

await conversation.send({
reaction: {
reference: otherMessage.id,
action: "added",
schema: "unicode",
content: "💖",
},
});
```

To learn more about how to build a custom content type, see [Build a custom content type](https://xmtp.org/docs/content-types/introduction#create-custom-content-types).

Custom codecs and content types may be proposed as interoperable standards through XRCs. To learn about the custom content type proposal process, see [XIP-5](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-5-message-content-types.md).

## Manually handle private key storage

The SDK will handle key storage for the user by encrypting the private key bundle using a signature generated from the wallet, and storing the encrypted payload on the XMTP network. This can be awkward for some server-side applications, where you may only want to give the application access to the XMTP keys but not your wallet keys. Mobile applications may also want to store keys in a secure enclave rather than rely on decrypting the remote keys on the network each time the application starts up.
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ repositories {
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "org.xmtp:android:0.6.12"
implementation "org.xmtp:android:0.6.14"
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 @@ -268,6 +268,27 @@ class XMTPModule : Module() {
).toJson()
}

AsyncFunction("sendEncodedContent") { clientAddress: String, topic: String, encodedContentData: List<Int> ->
val conversation =
findConversation(
clientAddress = clientAddress,
topic = topic
) ?: throw XMTPException("no conversation found for $topic")

val encodedContentDataBytes =
encodedContentData.foldIndexed(ByteArray(encodedContentData.size)) { i, a, v ->
a.apply {
set(
i,
v.toByte()
)
}
}
val encodedContent = EncodedContent.parseFrom(encodedContentDataBytes)

conversation.send(encodedContent = encodedContent)
}

AsyncFunction("listConversations") { clientAddress: String ->
logV("listConversations")
val client = clients[clientAddress] ?: throw XMTPException("No client")
Expand All @@ -288,7 +309,7 @@ class XMTPModule : Module() {
val beforeDate = if (before != null) Date(before) else null
val afterDate = if (after != null) Date(after) else null

conversation.messages(
conversation.decryptedMessages(
limit = limit,
before = beforeDate,
after = afterDate,
Expand Down Expand Up @@ -340,7 +361,7 @@ class XMTPModule : Module() {
topicsList.add(Pair(topic, page))
}

client.conversations.listBatchMessages(topicsList)
client.conversations.listBatchDecryptedMessages(topicsList)
.map { DecodedMessageWrapper.encode(it) }
}

Expand Down Expand Up @@ -486,7 +507,7 @@ class XMTPModule : Module() {
topic = topic
)
?: throw XMTPException("no conversation found for $topic")
val decodedMessage = conversation.decode(envelope)
val decodedMessage = conversation.decrypt(envelope)
DecodedMessageWrapper.encode(decodedMessage)
}

Expand Down Expand Up @@ -581,7 +602,7 @@ class XMTPModule : Module() {
subscriptions[getMessagesKey(clientAddress)]?.cancel()
subscriptions[getMessagesKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch {
try {
client.conversations.streamAllMessages().collect { message ->
client.conversations.streamAllDecryptedMessages().collect { message ->
sendEvent(
"message",
mapOf(
Expand All @@ -607,7 +628,7 @@ class XMTPModule : Module() {
subscriptions[conversation.cacheKey(clientAddress)] =
CoroutineScope(Dispatchers.IO).launch {
try {
conversation.streamMessages().collect { message ->
conversation.streamDecryptedMessages().collect { message ->
sendEvent(
"message",
mapOf(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package expo.modules.xmtpreactnativesdk.wrappers

import android.util.Base64
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.protobuf.ByteString
Expand All @@ -14,6 +15,7 @@ import org.xmtp.android.library.codecs.ContentTypeReadReceipt
import org.xmtp.android.library.codecs.ContentTypeRemoteAttachment
import org.xmtp.android.library.codecs.ContentTypeReply
import org.xmtp.android.library.codecs.ContentTypeText
import org.xmtp.android.library.codecs.EncodedContent
import org.xmtp.android.library.codecs.Reaction
import org.xmtp.android.library.codecs.ReactionCodec
import org.xmtp.android.library.codecs.ReadReceipt
Expand All @@ -28,16 +30,17 @@ import org.xmtp.android.library.codecs.description
import org.xmtp.android.library.codecs.getReactionAction
import org.xmtp.android.library.codecs.getReactionSchema
import org.xmtp.android.library.codecs.id
import org.xmtp.proto.message.contents.Content.EncodedContent
import java.net.URL

class ContentJson(
val type: ContentTypeId,
val content: Any?,
private val encodedContent: EncodedContent? = null,
) {
constructor(encoded: EncodedContent) : this(
type = encoded.type,
content = encoded.decoded(),
encodedContent = encoded
);

companion object {
Expand Down Expand Up @@ -157,19 +160,44 @@ class ContentJson(
ContentTypeReply.id -> mapOf(
"reply" to mapOf(
"reference" to (content as Reply).reference,
"content" to ContentJson(content.contentType, content.content).toJsonMap(),
"content" to ContentJson(
content.contentType,
content.content,
encodedContent
).toJsonMap(),
"contentType" to content.contentType.description
)
)

ContentTypeReadReceipt.id -> mapOf(
"readReceipt" to ""
)

else -> mapOf(
"unknown" to mapOf(
"contentTypeId" to type.description
)
)
else -> {
val json = JsonObject()
encodedContent?.let {
val typeJson = JsonObject()
typeJson.addProperty("authorityId", encodedContent.type.authorityId)
typeJson.addProperty("typeId", encodedContent.type.typeId)
typeJson.addProperty("versionMajor", encodedContent.type.versionMajor)
typeJson.addProperty("versionMinor", encodedContent.type.versionMinor)
val parameters = GsonBuilder().create().toJson(encodedContent.parametersMap)

json.addProperty("fallback", encodedContent.fallback)
json.add("parameters", JsonParser.parseString(parameters))
json.add("type", typeJson)
}
val encodedContentJSON = json.toString()
if (encodedContentJSON.isNotBlank()) {
mapOf("encoded" to encodedContentJSON)
} else {
mapOf(
"unknown" to mapOf(
"contentTypeId" to type.description
)
)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
package expo.modules.xmtpreactnativesdk.wrappers

import com.google.gson.GsonBuilder
import org.xmtp.android.library.DecodedMessage
import org.xmtp.android.library.codecs.description
import org.xmtp.android.library.codecs.id
import org.xmtp.android.library.messages.DecryptedMessage

class DecodedMessageWrapper {

companion object {
fun encode(model: DecodedMessage): String {
fun encode(model: DecryptedMessage): String {
val gson = GsonBuilder().create()
val message = encodeMap(model)
return gson.toJson(message)
}

fun encodeMap(model: DecodedMessage): Map<String, Any> = mapOf(
fun encodeMap(model: DecryptedMessage): Map<String, Any> = mapOf(
"id" to model.id,
"topic" to model.topic,
"contentTypeId" to model.encodedContent.type.description,
"content" to ContentJson(model.encodedContent).toJsonMap(),
"senderAddress" to model.senderAddress,
"sent" to model.sent.time,
"fallback" to model.fallbackContent
"sent" to model.sentAt.time,
"fallback" to model.encodedContent.fallback
)
}
}
52 changes: 26 additions & 26 deletions example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React from "react";
import { NavigationContainer } from '@react-navigation/native'
import React from 'react'
import { Button } from 'react-native'
import { QueryClient, QueryClientProvider } from 'react-query'

import LaunchScreen from "./src/LaunchScreen";
import TestScreen from "./src/TestScreen";
import HomeScreen from "./src/HomeScreen";
import ConversationScreen from "./src/ConversationScreen";
import ConversationCreateScreen from "./src/ConversationCreateScreen";
import { NavigationContainer } from "@react-navigation/native";
import { XmtpContextProvider } from "./src/XmtpContext";
import { Navigator } from "./src/Navigation";
import { QueryClient, QueryClientProvider } from "react-query";
import { Button } from "react-native";
import ConversationCreateScreen from './src/ConversationCreateScreen'
import ConversationScreen from './src/ConversationScreen'
import HomeScreen from './src/HomeScreen'
import LaunchScreen from './src/LaunchScreen'
import { Navigator } from './src/Navigation'
import TestScreen from './src/TestScreen'
import { XmtpContextProvider } from './src/XmtpContext'

const queryClient = new QueryClient();
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
Expand All @@ -22,36 +22,36 @@ export default function App() {
name="launch"
component={LaunchScreen}
options={{
title: "XMTP RN Example",
title: 'XMTP RN Example',
headerStyle: {
backgroundColor: "rgb(49 0 110)",
backgroundColor: 'rgb(49 0 110)',
},
headerTintColor: "#fff",
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: "bold",
fontWeight: 'bold',
},
}}
/>
<Navigator.Screen
name="test"
component={TestScreen}
options={{ title: "Unit Tests" }}
options={{ title: 'Unit Tests' }}
/>
<Navigator.Screen
name="home"
component={HomeScreen}
options={({ navigation }) => ({
title: "My Conversations",
title: 'My Conversations',
headerStyle: {
backgroundColor: "rgb(49 0 110)",
backgroundColor: 'rgb(49 0 110)',
},
headerTintColor: "#fff",
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: "bold",
fontWeight: 'bold',
},
headerRight: () => (
<Button
onPress={() => navigation.navigate("conversationCreate")}
onPress={() => navigation.navigate('conversationCreate')}
title="New"
color="#fff"
/>
Expand All @@ -61,17 +61,17 @@ export default function App() {
<Navigator.Screen
name="conversation"
component={ConversationScreen}
options={{ title: "Conversation" }}
initialParams={{ topic: "" }}
options={{ title: 'Conversation' }}
initialParams={{ topic: '' }}
/>
<Navigator.Screen
name="conversationCreate"
component={ConversationCreateScreen}
options={{ title: "New Conversation" }}
options={{ title: 'New Conversation' }}
/>
</Navigator.Navigator>
</NavigationContainer>
</XmtpContextProvider>
</QueryClientProvider>
);
)
}
Loading
Loading