Skip to content

Commit

Permalink
Merge pull request #81 from xmtp/daniel-revamp-example
Browse files Browse the repository at this point in the history
feat: revamp example app, implement prelim android content types
  • Loading branch information
dmccartney authored Aug 5, 2023
2 parents d8ebda2 + ec32626 commit 49fe3be
Show file tree
Hide file tree
Showing 47 changed files with 2,411 additions and 1,511 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.5"
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,135 @@
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
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.ContentTypeReply
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.Reply
import org.xmtp.android.library.codecs.ReplyCodec
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())
Client.register(ReplyCodec())
// TODO:
//Client.register(CompositeCodec())
//Client.register(GroupChatMemberAddedCodec())
//Client.register(GroupChatTitleChangedCodec())
//Client.register(RemoteAttachmentCodec())
}

fun fromJsonObject(obj: JsonObject): ContentJson {
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 if (obj.has("reply")) {
val reply = obj.get("reply").asJsonObject
val nested = fromJsonObject(reply.get("content").asJsonObject)
if (nested.type.id == ContentTypeReply.id) {
throw Exception("Reply cannot contain a reply")
}
if (nested.content == null) {
throw Exception("Bad reply content")
}
return ContentJson(ContentTypeReply, Reply(
reference = reply.get("reference").asString,
content = nested.content,
contentType = nested.type,
))
} else {
throw Exception("Unknown content type")
}
}

fun fromJson(json: String): ContentJson {
val obj = JsonParser.parseString(json).asJsonObject
return fromJsonObject(obj);
}

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,
)
)

ContentTypeReply.id -> mapOf(
"reply" to mapOf(
"reference" to (content as Reply).reference,
"content" to ContentJson(content.contentType, content.content).toJsonMap(),
)
)

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.

109 changes: 72 additions & 37 deletions example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,80 @@
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";
import { Button } from "react-native";

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",
headerStyle: {
backgroundColor: "rgb(49 0 110)",
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "bold",
},
}}
/>
<Navigator.Screen
name="test"
component={TestScreen}
options={{ title: "Unit Tests" }}
/>
<Navigator.Screen
name="home"
component={HomeScreen}
options={({ navigation }) => ({
title: "My Conversations",
headerStyle: {
backgroundColor: "rgb(49 0 110)",
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "bold",
},
headerRight: () => (
<Button
onPress={() => navigation.navigate("conversationCreate")}
title="New"
color="#fff"
/>
),
})}
/>
<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",
},
});
Loading

0 comments on commit 49fe3be

Please sign in to comment.