Skip to content

Commit

Permalink
Merge pull request #155 from xmtp/custom-content-types
Browse files Browse the repository at this point in the history
Custom content types
  • Loading branch information
nplasterer authored Dec 2, 2023
2 parents 3a4f98d + ccf5e75 commit 718f8a9
Show file tree
Hide file tree
Showing 57 changed files with 20,168 additions and 16,833 deletions.
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

0 comments on commit 718f8a9

Please sign in to comment.