Skip to content

Commit

Permalink
feat: revamp example app, implement android types text/attachment/rea…
Browse files Browse the repository at this point in the history
…ction
  • Loading branch information
dmccartney committed Aug 3, 2023
1 parent d8ebda2 commit 0169520
Show file tree
Hide file tree
Showing 42 changed files with 1,293 additions and 1,046 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
root: true,
extends: ['universe/native', 'universe/web'],
ignorePatterns: ['build'],
extends: ["universe/native", "universe/web"],
ignorePatterns: ["build"],
};
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ repositories {
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "org.xmtp:android:0.3.2"
implementation "org.xmtp:android:0.3.3"
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
30 changes: 11 additions & 19 deletions android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.xmtpreactnativesdk.wrappers.ConversationWithClientAddress
import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper
import expo.modules.xmtpreactnativesdk.wrappers.ContentJson
import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper
import expo.modules.xmtpreactnativesdk.wrappers.EncodedMessageWrapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand All @@ -19,6 +19,7 @@ import org.json.JSONObject
import org.xmtp.android.library.Client
import org.xmtp.android.library.ClientOptions
import org.xmtp.android.library.Conversation
import org.xmtp.android.library.SendOptions
import org.xmtp.android.library.SigningKey
import org.xmtp.android.library.XMTPEnvironment
import org.xmtp.android.library.XMTPException
Expand Down Expand Up @@ -222,9 +223,7 @@ class XMTPModule : Module() {
val afterDate = if (after != null) Date(after) else null

conversation.messages(limit = limit, before = beforeDate, after = afterDate)
.map {
EncodedMessageWrapper.encode(it)
}
.map { DecodedMessageWrapper.encode(it) }
}

AsyncFunction("loadBatchMessages") { clientAddress: String, topics: List<String> ->
Expand Down Expand Up @@ -258,30 +257,23 @@ class XMTPModule : Module() {
topicsList.add(Pair(topic, page))
}

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

AsyncFunction("sendEncodedContentData") { clientAddress: String, conversationTopic: String, conversationID: String?, content: List<Int> ->
AsyncFunction("sendMessage") { clientAddress: String, conversationTopic: String, conversationID: String?, contentJson: String ->
logV("sendMessage")
val conversation =
findConversation(
clientAddress = clientAddress,
topic = conversationTopic
)
?: throw XMTPException("no conversation found for $conversationTopic")

val contentData = content.foldIndexed(ByteArray(content.size)) { i, a, v ->
a.apply {
set(
i,
v.toByte()
)
}
}
val encodedContent = EncodedContent.parseFrom(contentData)
conversation.send(encodedContent = encodedContent)
val sending = ContentJson.fromJson(contentJson)
conversation.send(
content = sending.content,
options = SendOptions(contentType = sending.type)
)
}

AsyncFunction("createConversation") { clientAddress: String, peerAddress: String, conversationID: String? ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package expo.modules.xmtpreactnativesdk.wrappers

import android.util.Base64
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import com.google.protobuf.ByteString
import org.xmtp.android.library.Client
import org.xmtp.android.library.DecodedMessage
import org.xmtp.proto.message.contents.Content.EncodedContent
import org.xmtp.android.library.codecs.decoded
import org.xmtp.android.library.codecs.ContentTypeAttachment
import org.xmtp.android.library.codecs.ContentTypeId
import org.xmtp.android.library.codecs.ContentTypeReaction
import org.xmtp.android.library.codecs.ContentTypeText
import org.xmtp.android.library.codecs.AttachmentCodec
import org.xmtp.android.library.codecs.Attachment
import org.xmtp.android.library.codecs.ReactionAction
import org.xmtp.android.library.codecs.ReactionSchema
import org.xmtp.android.library.codecs.ReactionCodec
import org.xmtp.android.library.codecs.Reaction
import org.xmtp.android.library.codecs.TextCodec
import org.xmtp.android.library.codecs.id

import java.lang.Exception

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

companion object {
init {
Client.register(TextCodec())
Client.register(AttachmentCodec())
Client.register(ReactionCodec())
// TODO:
//Client.register(ReplyCodec())
//Client.register(CompositeCodec())
//Client.register(GroupChatMemberAddedCodec())
//Client.register(GroupChatTitleChangedCodec())
//Client.register(RemoteAttachmentCodec())
}

fun fromJson(json: String): ContentJson {
val obj = JsonParser.parseString(json).asJsonObject
if (obj.has("text")) {
return ContentJson(ContentTypeText, obj.get("text").asString)
} else if (obj.has("attachment")) {
val attachment = obj.get("attachment").asJsonObject
return ContentJson(ContentTypeAttachment, Attachment(
filename = attachment.get("filename").asString,
mimeType = attachment.get("mimeType").asString,
data = ByteString.copyFrom(bytesFrom64(attachment.get("data").asString)),
))
} else if (obj.has("reaction")) {
val reaction = obj.get("reaction").asJsonObject
return ContentJson(ContentTypeReaction, Reaction(
reference = reaction.get("reference").asString,
action = ReactionAction.valueOf(reaction.get("action").asString),
schema = ReactionSchema.valueOf(reaction.get("schema").asString),
content = reaction.get("content").asString,
))
} else {
throw Exception("Unknown content type")
}
}

fun bytesFrom64(bytes64: String): ByteArray = Base64.decode(bytes64, Base64.DEFAULT)
fun bytesTo64(bytes: ByteArray): String = Base64.encodeToString(bytes, Base64.DEFAULT)
}

fun toJsonMap(): Map<String, Any> {
return when (type.id) {
ContentTypeText.id -> mapOf(
"text" to (content as String? ?: ""),
)

ContentTypeAttachment.id -> mapOf(
"attachment" to mapOf(
"filename" to (content as Attachment).filename,
"mimeType" to content.mimeType,
"data" to bytesTo64(content.data.toByteArray()),
)
)

ContentTypeReaction.id -> mapOf(
"reaction" to mapOf(
"reference" to (content as Reaction).reference,
"action" to content.action,
"schema" to content.schema,
"content" to content.content,
)
)

else -> mapOf(
"unknown" to mapOf(
"contentTypeId" to type.id
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ package expo.modules.xmtpreactnativesdk.wrappers
import com.google.gson.GsonBuilder
import org.xmtp.android.library.DecodedMessage

import java.lang.Exception

class DecodedMessageWrapper {

companion object {
fun encode(model: DecodedMessage): String {
val gson = GsonBuilder().create()
val message = mapOf(
"id" to model.id,
"content" to model.body,
"senderAddress" to model.senderAddress,
"sent" to model.sent
)
val message = encodeMap(model)
return gson.toJson(message)
}

fun encodeMap(model: DecodedMessage): Map<String, Any> = mapOf(
"id" to model.id,
"content" to ContentJson(model.encodedContent).toJsonMap(),
"senderAddress" to model.senderAddress,
"sent" to model.sent.getTime(),
)
}
}
}

This file was deleted.

83 changes: 46 additions & 37 deletions example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,54 @@
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
import React, { useState } from "react";
import { Button, SafeAreaView, StyleSheet, View } from "react-native";
import * as XMTP from "xmtp-react-native-sdk";
import React from "react";

import AuthView from "./src/AuthView";
import HomeView from "./src/HomeView";
import TestsView from "./src/TestsView";
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";

const queryClient = new QueryClient();
export default function App() {
const [client, setClient] = useState<XMTP.Client | null>(null);
const [isTesting, setIsTesting] = useState<boolean>(false);

return isTesting ? (
<SafeAreaView style={{ flexGrow: 1 }}>
<TestsView />
</SafeAreaView>
) : (
return (
<ThirdwebProvider activeChain="mainnet">
<SafeAreaView style={{ flexGrow: 1 }}>
{client != null ? (
<HomeView client={client} />
) : (
<View>
<AuthView setClient={setClient} />
<Button
onPress={() => setIsTesting(true)}
title="Unit tests"
accessibilityLabel="Unit-tests"
/>
</View>
)}
</SafeAreaView>
<QueryClientProvider client={queryClient}>
<XmtpContextProvider>
<NavigationContainer>
<Navigator.Navigator>
<Navigator.Screen
name="launch"
component={LaunchScreen}
options={{ title: "XMTP RN Example" }}
/>
<Navigator.Screen
name="test"
component={TestScreen}
options={{ title: "Unit Tests" }}
/>
<Navigator.Screen
name="home"
component={HomeScreen}
options={{ title: "My Conversations" }}
/>
<Navigator.Screen
name="conversation"
component={ConversationScreen}
options={{ title: "Conversation" }}
initialParams={{ topic: "" }}
/>
<Navigator.Screen
name="conversationCreate"
component={ConversationCreateScreen}
options={{ title: "New Conversation" }}
/>
</Navigator.Navigator>
</NavigationContainer>
</XmtpContextProvider>
</QueryClientProvider>
</ThirdwebProvider>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
2 changes: 1 addition & 1 deletion example/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = function (api) {
__dirname,
"..",
"src",
"index.ts"
"index.ts",
),
},
},
Expand Down
4 changes: 3 additions & 1 deletion example/dev/local/test/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ async function checkAll() {
continue;
}

await message.conversation.send("HI " + message.senderAddress);
await message.conversation.send({
text: "HI " + message.senderAddress,
});
console.log(`Replied to ${message.senderAddress}`);
}
} catch (e) {
Expand Down
Loading

0 comments on commit 0169520

Please sign in to comment.