diff --git a/.eslintrc.js b/.eslintrc.js index 45cf2c1e..8e39d62a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { root: true, - extends: ['universe/native', 'universe/web'], - ignorePatterns: ['build'], + extends: ["universe/native", "universe/web"], + ignorePatterns: ["build"], }; diff --git a/android/build.gradle b/android/build.gradle index fe9ef453..ac7552ba 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index c6d1c0a7..636b8719 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -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 @@ -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 @@ -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 -> @@ -258,12 +257,11 @@ 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 -> + AsyncFunction("sendMessage") { clientAddress: String, conversationTopic: String, conversationID: String?, contentJson: String -> logV("sendMessage") val conversation = findConversation( @@ -271,17 +269,11 @@ class XMTPModule : Module() { 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? -> diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt new file mode 100644 index 00000000..2779c0e5 --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt @@ -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 { + 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 + ) + ) + } + } +} diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt index bca192bb..acb32a73 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt @@ -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 = mapOf( + "id" to model.id, + "content" to ContentJson(model.encodedContent).toJsonMap(), + "senderAddress" to model.senderAddress, + "sent" to model.sent.getTime(), + ) } -} \ No newline at end of file +} diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EncodeMessageWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EncodeMessageWrapper.kt deleted file mode 100644 index 788313fb..00000000 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EncodeMessageWrapper.kt +++ /dev/null @@ -1,27 +0,0 @@ -package expo.modules.xmtpreactnativesdk.wrappers - -import com.daveanthonythomas.moshipack.MoshiPack -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.WritableNativeArray -import org.xmtp.android.library.DecodedMessage - -class EncodedMessageWrapper { - - companion object { - fun encode(model: DecodedMessage): ReadableArray { - val message = mapOf( - "id" to model.id, - "content" to model.encodedContent.toByteArray(), - "senderAddress" to model.senderAddress.toByteArray(), - "sent" to model.sent.time.toString() - ) - val encodedContent = MoshiPack().pack(message).readByteArray() - - val byteArray = WritableNativeArray() - encodedContent.forEach { - byteArray.pushString(it.toString()) - } - return byteArray - } - } -} \ No newline at end of file diff --git a/example/App.tsx b/example/App.tsx index 427f3c59..29c9895e 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -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(null); - const [isTesting, setIsTesting] = useState(false); - - return isTesting ? ( - - - - ) : ( + return ( - - {client != null ? ( - - ) : ( - - -