diff --git a/android/build.gradle b/android/build.gradle index 2e510b7b4..81acca2df 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -95,7 +95,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:4.6.1-rc1" + implementation "org.xmtp:android:4.7.0-dev.252eebc" 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 cad9e9b3f..7601c7048 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -71,6 +71,7 @@ import org.xmtp.android.library.hexToByteArray import org.xmtp.android.library.libxmtp.ArchiveElement import org.xmtp.android.library.libxmtp.ArchiveOptions import org.xmtp.android.library.libxmtp.DecodedMessage +import org.xmtp.android.library.libxmtp.DecodedMessage.SortBy import org.xmtp.android.library.libxmtp.DisappearingMessageSettings import org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration import org.xmtp.android.library.libxmtp.PermissionOption @@ -1012,7 +1013,13 @@ class XMTPModule : Module() { afterNs = queryParams.afterNs, direction = DecodedMessage.SortDirection.valueOf( queryParams.direction ?: "DESCENDING" - ) + ), + insertedAfterNs = queryParams.insertedAfterNs, + insertedBeforeNs = queryParams.insertedBeforeNs, + sortBy = when (queryParams.sortBy) { + "INSERTED" -> SortBy.INSERTED_TIME + else -> SortBy.SENT_TIME + } )?.map { MessageWrapper.encode(it) } } } @@ -1029,11 +1036,32 @@ class XMTPModule : Module() { afterNs = queryParams.afterNs, direction = DecodedMessage.SortDirection.valueOf( queryParams.direction ?: "DESCENDING" - ) + ), + insertedAfterNs = queryParams.insertedAfterNs, + insertedBeforeNs = queryParams.insertedBeforeNs, + sortBy = when (queryParams.sortBy) { + "INSERTED" -> SortBy.INSERTED_TIME + else -> SortBy.SENT_TIME + } )?.map { MessageWrapper.encode(it) } } } + AsyncFunction("countMessages") Coroutine { installationId: String, conversationId: String, queryParamsJson: String? -> + withContext(Dispatchers.IO) { + logV("countMessages") + val client = clients[installationId] ?: throw XMTPException("No client") + val conversation = client.conversations.findConversation(conversationId) + val queryParams = MessageQueryParamsWrapper.messageQueryParamsFromJson(queryParamsJson ?: "") + conversation?.countMessages( + beforeNs = queryParams.beforeNs, + afterNs = queryParams.afterNs, + insertedAfterNs = queryParams.insertedAfterNs, + insertedBeforeNs = queryParams.insertedBeforeNs + ) ?: 0 + } + } + AsyncFunction("findMessage") Coroutine { installationId: String, messageId: String -> withContext(Dispatchers.IO) { logV("findMessage") diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageQueryParamsWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageQueryParamsWrapper.kt index 554953b28..279782010 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageQueryParamsWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageQueryParamsWrapper.kt @@ -9,6 +9,9 @@ class MessageQueryParamsWrapper( val direction: String?, val excludeContentTypes: List?, val excludeSenderInboxIds: List?, + val insertedAfterNs: Long?, + val insertedBeforeNs: Long?, + val sortBy: String?, ) { companion object { fun messageQueryParamsFromJson(paramsJson: String): MessageQueryParamsWrapper { @@ -20,6 +23,9 @@ class MessageQueryParamsWrapper( null, null, null, + null, + null, + null, ) } @@ -69,6 +75,27 @@ class MessageQueryParamsWrapper( null } + val insertedAfterNs = + if (jsonOptions.has("insertedAfterNs")) { + jsonOptions.get("insertedAfterNs").asLong + } else { + null + } + + val insertedBeforeNs = + if (jsonOptions.has("insertedBeforeNs")) { + jsonOptions.get("insertedBeforeNs").asLong + } else { + null + } + + val sortBy = + if (jsonOptions.has("sortBy")) { + jsonOptions.get("sortBy").asString + } else { + null + } + return MessageQueryParamsWrapper( limit, beforeNs, @@ -76,6 +103,9 @@ class MessageQueryParamsWrapper( direction, excludeContentTypes, excludeSenderInboxIds, + insertedAfterNs, + insertedBeforeNs, + sortBy, ) } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageWrapper.kt index c9f079445..f86ad582e 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MessageWrapper.kt @@ -5,7 +5,6 @@ import org.xmtp.android.library.codecs.description import org.xmtp.android.library.libxmtp.DecodedMessage class MessageWrapper { - companion object { fun encode(model: DecodedMessage): String { val gson = GsonBuilder().create() @@ -24,9 +23,10 @@ class MessageWrapper { "content" to ContentJson(model.encodedContent).toJsonMap(), "senderInboxId" to model.senderInboxId, "sentNs" to model.sentAtNs, + "insertedAtNs" to model.insertedAtNs, "fallback" to fallback, "deliveryStatus" to model.deliveryStatus.toString(), - "childMessages" to model.childMessages?.map { childMessage -> encodeMap(childMessage) } + "childMessages" to model.childMessages?.map { childMessage -> encodeMap(childMessage) }, ) } } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 1dcfe0e27..bdf11514c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - EXImageLoader (5.0.0): - ExpoModulesCore - React-Core - - Expo (52.0.47): + - Expo (52.0.44): - ExpoModulesCore - ExpoAsset (11.0.5): - ExpoModulesCore @@ -51,9 +51,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ExpoSplashScreen (0.29.24): - - ExpoModulesCore - - ExpoSystemUI (4.0.9): + - ExpoSplashScreen (0.29.22): - ExpoModulesCore - fast_float (6.1.4) - FBLazyVector (0.76.9) @@ -1377,7 +1375,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-netinfo (11.4.1): + - react-native-netinfo (9.3.7): - React-Core - react-native-quick-base64 (2.1.2): - DoubleConversion @@ -1425,31 +1423,12 @@ PODS: - Yoga - react-native-randombytes (3.6.1): - React-Core - - react-native-safe-area-context (4.12.0): + - react-native-safe-area-context (5.3.0): - React-Core - react-native-sqlite-storage (6.0.1): - React-Core - - react-native-webview (13.12.5): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2024.10.14.00) - - RCTRequired - - RCTTypeSafety + - react-native-webview (11.26.0): - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - React-nativeconfig (0.76.9) - React-NativeModulesApple (0.76.9): - glog @@ -1722,11 +1701,11 @@ PODS: - React-logger - React-perflogger - React-utils (= 0.76.9) - - RNCAsyncStorage (1.23.1): + - RNCAsyncStorage (1.17.11): - React-Core - RNFS (2.20.0): - React-Core - - RNScreens (4.4.0): + - RNScreens (4.10.0): - DoubleConversion - glog - hermes-engine @@ -1748,7 +1727,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNSVG (15.8.0): + - RNSVG (15.11.2): - React-Core - SocketRocket (0.7.1) - SQLCipher (4.5.7): @@ -1757,16 +1736,16 @@ PODS: - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.28.2) - - XMTP (4.6.1-rc3): + - XMTP (4.7.0-dev.ff66c0f): - Connect-Swift (= 1.0.0) - CryptoSwift (= 1.8.3) - SQLCipher (= 4.5.7) - - XMTPReactNative (5.0.6): + - XMTPReactNative (5.1.0-rc1): - CSecp256k1 (~> 0.2) - ExpoModulesCore - MessagePacker - SQLCipher (= 4.5.7) - - XMTP (= 4.6.1-rc3) + - XMTP (= 4.7.0-dev.ff66c0f) - Yoga (0.0.0) DEPENDENCIES: @@ -1785,7 +1764,6 @@ DEPENDENCIES: - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`) - - ExpoSystemUI (from `../node_modules/expo-system-ui/ios`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) @@ -1913,8 +1891,6 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-modules-core" ExpoSplashScreen: :path: "../node_modules/expo-splash-screen/ios" - ExpoSystemUI: - :path: "../node_modules/expo-system-ui/ios" fast_float: :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: @@ -2083,7 +2059,7 @@ SPEC CHECKSUMS: DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b EXImageLoader: e5da974e25b13585c196b658a440720c075482d5 - Expo: 1687edb10c76b0c0f135306d6ae245379f50ed54 + Expo: 75e002fc29a18a72aa3db967b41b29c2b206875d ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516 ExpoClipboard: 44fd1c8959ee8f6175d059dc011b154c9709a969 ExpoCrypto: e97e864c8d7b9ce4a000bca45dddb93544a1b2b4 @@ -2093,8 +2069,7 @@ SPEC CHECKSUMS: ExpoImagePicker: 24e5ba8da111f74519b1e6dc556e0b438b2b8464 ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 ExpoModulesCore: 725faec070d590810d2ea5983d9f78f7cf6a38ec - ExpoSplashScreen: 399ee9f85b6c8a61b965e13a1ecff8384db591c2 - ExpoSystemUI: b82a45cf0f6a4fa18d07c46deba8725dd27688b4 + ExpoSplashScreen: cb4e3d3ee646ed59810f7776cca0ae5c03ab4285 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6 @@ -2138,13 +2113,13 @@ SPEC CHECKSUMS: react-native-encrypted-storage: 569d114e329b1c2c2d9f8c84bcdbe4478dda2258 react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-mmkv: f0574e88f254d13d1a87cf6d38c36bc5d3910d49 - react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 + react-native-netinfo: be701059f57093572e5ba08cba14483d334b425d react-native-quick-base64: 5565249122493bef017004646d73f918e8c2dfb0 react-native-quick-crypto: c168ffba24470d8edfd03961d9492638431b9869 react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116 - react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448 + react-native-safe-area-context: fdb0a66feac038cb6eb1edafcf2ccee2b5cf0284 react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed - react-native-webview: 69a5462ca94921ff695e1b52b12fffe62af7d312 + react-native-webview: 5bb1454f1eb43e0bad229bb428a378d6b865a0ad React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 @@ -2172,15 +2147,15 @@ SPEC CHECKSUMS: React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 - RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11 + RNCAsyncStorage: 357676e1dc19095208c80d4271066c407cd02ed1 RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 - RNScreens: 295d9c0aaeb7f680d03d7e9b476569a4959aae89 - RNSVG: 8542aa11770b27563714bbd8494a8436385fc85f + RNScreens: 5cac36d8f7b3d92fb4304abcb44c5de336413df8 + RNSVG: a07e14363aa208062c6483bad24a438d5986d490 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d - XMTP: d9e99e75df20472dd4845f5b9f0476e4748d1fd6 - XMTPReactNative: ae7fe2223f35aae19235a1c792f3035f8ff70cb9 + XMTP: fbef51f8aa0cb762cfac902668f9648b77465a7f + XMTPReactNative: 439874cb0553196c7f6804014f829a2f5ec9c215 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a PODFILE CHECKSUM: 5b1b93f724b9cde6043d1824960f7bfd9a7973cd diff --git a/ios/Wrappers/MessageQueryParamsWrapper.swift b/ios/Wrappers/MessageQueryParamsWrapper.swift index 393c143dc..017aebcc4 100644 --- a/ios/Wrappers/MessageQueryParamsWrapper.swift +++ b/ios/Wrappers/MessageQueryParamsWrapper.swift @@ -8,6 +8,9 @@ struct MessageQueryParamsWrapper { let direction: String? let excludeContentTypes: [String]? let excludeSenderInboxIds: [String]? + let insertedAfterNs: Int64? + let insertedBeforeNs: Int64? + let sortBy: String? static func messageQueryParamsFromJson(_ paramsJson: String) -> MessageQueryParamsWrapper @@ -19,14 +22,17 @@ struct MessageQueryParamsWrapper { afterNs: nil, direction: nil, excludeContentTypes: nil, - excludeSenderInboxIds: nil + excludeSenderInboxIds: nil, + insertedAfterNs: nil, + insertedBeforeNs: nil, + sortBy: nil ) } let data = paramsJson.data(using: .utf8) ?? Data() let jsonOptions = (try? JSONSerialization.jsonObject(with: data, options: [])) - as? [String: Any] ?? [:] + as? [String: Any] ?? [:] let limit = jsonOptions["limit"] as? Int let beforeNs = jsonOptions["beforeNs"] as? Int64 @@ -34,6 +40,9 @@ struct MessageQueryParamsWrapper { let direction = jsonOptions["direction"] as? String let excludeContentTypes = jsonOptions["excludeContentTypes"] as? [String] let excludeSenderInboxIds = jsonOptions["excludeSenderInboxIds"] as? [String] + let insertedAfterNs = jsonOptions["insertedAfterNs"] as? Int64 + let insertedBeforeNs = jsonOptions["insertedBeforeNs"] as? Int64 + let sortBy = jsonOptions["sortBy"] as? String return MessageQueryParamsWrapper( limit: limit, @@ -41,7 +50,10 @@ struct MessageQueryParamsWrapper { afterNs: afterNs, direction: direction, excludeContentTypes: excludeContentTypes, - excludeSenderInboxIds: excludeSenderInboxIds + excludeSenderInboxIds: excludeSenderInboxIds, + insertedAfterNs: insertedAfterNs, + insertedBeforeNs: insertedBeforeNs, + sortBy: sortBy ) } } diff --git a/ios/Wrappers/MessageWrapper.swift b/ios/Wrappers/MessageWrapper.swift index b4fd2ac4a..1885ae6a2 100644 --- a/ios/Wrappers/MessageWrapper.swift +++ b/ios/Wrappers/MessageWrapper.swift @@ -3,24 +3,25 @@ import XMTP // Wrapper around XMTP.DecodedMessage to allow passing these objects back // into react native. -struct MessageWrapper { +enum MessageWrapper { static func encodeToObj(_ model: XMTP.DecodedMessage) throws -> [String: Any] { - // Swift Protos don't support null values and will always put the default "" - // Check if there is a fallback, if there is then make it the set fallback, if not null + // Swift Protos don't support null values and will always put the default "" + // Check if there is a fallback, if there is then make it the set fallback, if not null let fallback = try model.encodedContent.hasFallback ? model.encodedContent.fallback : nil - return [ + return try [ "id": model.id, "topic": model.topic, - "contentTypeId": try model.encodedContent.type.description, - "content": try ContentJson.fromEncoded(model.encodedContent).toJsonMap() as Any, + "contentTypeId": model.encodedContent.type.description, + "content": ContentJson.fromEncoded(model.encodedContent).toJsonMap() as Any, "senderInboxId": model.senderInboxId, "sentNs": model.sentAtNs, + "insertedAtNs": model.insertedAtNs, "fallback": fallback, "deliveryStatus": model.deliveryStatus.rawValue.uppercased(), - "childMessages": model.childMessages?.map { childMessage in - try? encodeToObj(childMessage) - } - ] + "childMessages": model.childMessages?.map { childMessage in + try? encodeToObj(childMessage) + }, + ] } static func encode(_ model: XMTP.DecodedMessage) throws -> String { @@ -76,15 +77,15 @@ struct ContentJson { schema: ReactionSchema(rawValue: reaction["schema"] as? String ?? "") )) } else if let reaction = obj["reactionV2"] as? [String: Any] { - return ContentJson(type: ContentTypeReactionV2, content: FfiReactionPayload( + return ContentJson(type: ContentTypeReactionV2, content: FfiReactionPayload( reference: reaction["reference"] as? String ?? "", // Update if we add referenceInboxId to ../src/lib/types/ContentCodec.ts#L19-L24 - referenceInboxId: "", + referenceInboxId: "", action: ReactionV2Action.fromString(reaction["action"] as? String ?? ""), content: reaction["content"] as? String ?? "", schema: ReactionV2Schema.fromString(reaction["schema"] as? String ?? "") )) - }else if let reply = obj["reply"] as? [String: Any] { + } else if let reply = obj["reply"] as? [String: Any] { guard let nestedContent = reply["content"] as? [String: Any] else { throw Error.badReplyContent } @@ -123,29 +124,30 @@ struct ContentJson { content.contentLength = metadata.contentLength return ContentJson(type: ContentTypeRemoteAttachment, content: content) } else if let multiRemoteAttachment = obj["multiRemoteAttachment"] as? [String: Any] { - guard let attachmentsArray = multiRemoteAttachment["attachments"] as? [[String: Any]] else { - throw Error.badRemoteAttachmentMetadata - } - - let attachments = try attachmentsArray.map { attachment -> MultiRemoteAttachment.RemoteAttachmentInfo in - guard let metadata = try? EncryptedAttachmentMetadata.fromJsonObj(attachment), - let urlString = attachment["url"] as? String else { - throw Error.badRemoteAttachmentMetadata - } - - return MultiRemoteAttachment.RemoteAttachmentInfo( - url: urlString, - filename: metadata.filename, - contentLength: UInt32(metadata.contentLength), - contentDigest: metadata.contentDigest, - nonce: metadata.nonce, - scheme: "https", - salt: metadata.salt, - secret: metadata.secret - ) - } - return ContentJson(type: ContentTypeMultiRemoteAttachment, content: MultiRemoteAttachment(remoteAttachments: attachments)) - } else if let readReceipt = obj["readReceipt"] as? [String: Any] { + guard let attachmentsArray = multiRemoteAttachment["attachments"] as? [[String: Any]] else { + throw Error.badRemoteAttachmentMetadata + } + + let attachments = try attachmentsArray.map { attachment -> MultiRemoteAttachment.RemoteAttachmentInfo in + guard let metadata = try? EncryptedAttachmentMetadata.fromJsonObj(attachment), + let urlString = attachment["url"] as? String + else { + throw Error.badRemoteAttachmentMetadata + } + + return MultiRemoteAttachment.RemoteAttachmentInfo( + url: urlString, + filename: metadata.filename, + contentLength: UInt32(metadata.contentLength), + contentDigest: metadata.contentDigest, + nonce: metadata.nonce, + scheme: "https", + salt: metadata.salt, + secret: metadata.secret + ) + } + return ContentJson(type: ContentTypeMultiRemoteAttachment, content: MultiRemoteAttachment(remoteAttachments: attachments)) + } else if let readReceipt = obj["readReceipt"] as? [String: Any] { return ContentJson(type: ContentTypeReadReceipt, content: ReadReceipt()) } else { throw Error.unknownContentType @@ -170,29 +172,29 @@ struct ContentJson { "schema": reaction.schema.rawValue, "content": reaction.content, ]] - case ContentTypeReactionV2.id: - guard let encodedContent = encodedContent else { - return ["error": "Missing encoded content for reaction"] - } - do { - let bytes = try encodedContent.serializedData() - let reaction = try decodeReaction(bytes: bytes) - return ["reaction": [ - "reference": reaction.reference, - "action": ReactionV2Action.toString(reaction.action), - "schema": ReactionV2Schema.toString(reaction.schema), - "content": reaction.content, - ]] - } catch { - return ["error": "Failed to decode reaction: \(error.localizedDescription)"] - } + case ContentTypeReactionV2.id: + guard let encodedContent = encodedContent else { + return ["error": "Missing encoded content for reaction"] + } + do { + let bytes = try encodedContent.serializedData() + let reaction = try decodeReaction(bytes: bytes) + return ["reaction": [ + "reference": reaction.reference, + "action": ReactionV2Action.toString(reaction.action), + "schema": ReactionV2Schema.toString(reaction.schema), + "content": reaction.content, + ]] + } catch { + return ["error": "Failed to decode reaction: \(error.localizedDescription)"] + } case ContentTypeReply.id where content is XMTP.Reply: let reply = content as! XMTP.Reply let nested = ContentJson(type: reply.contentType, content: reply.content) return ["reply": [ "reference": reply.reference, "content": nested.toJsonMap(), - "contentType": reply.contentType.description + "contentType": reply.contentType.description, ] as [String: Any]] case ContentTypeAttachment.id where content is XMTP.Attachment: let attachment = content as! XMTP.Attachment @@ -214,30 +216,30 @@ struct ContentJson { "url": remoteAttachment.url, ]] case ContentTypeMultiRemoteAttachment.id where content is XMTP.MultiRemoteAttachment: - guard let encodedContent = encodedContent else { - return ["error": "Missing encoded content for multi remote attachment"] - } - do { - let bytes = try encodedContent.serializedData() - let multiRemoteAttachment = try decodeMultiRemoteAttachment(bytes: bytes) - let attachmentMaps = multiRemoteAttachment.attachments.map { attachment in - return [ - "scheme": "https", - "url": attachment.url, - "filename": attachment.filename ?? "", - "contentLength": String(attachment.contentLength ?? 0), - "contentDigest": attachment.contentDigest, - "secret": attachment.secret.toHex, - "salt": attachment.salt.toHex, - "nonce": attachment.nonce.toHex - ] - } - return ["multiRemoteAttachment": [ - "attachments": attachmentMaps - ]] - } catch { - return ["error": "Failed to decode multi remote attachment: \(error.localizedDescription)"] - } + guard let encodedContent = encodedContent else { + return ["error": "Missing encoded content for multi remote attachment"] + } + do { + let bytes = try encodedContent.serializedData() + let multiRemoteAttachment = try decodeMultiRemoteAttachment(bytes: bytes) + let attachmentMaps = multiRemoteAttachment.attachments.map { attachment in + [ + "scheme": "https", + "url": attachment.url, + "filename": attachment.filename ?? "", + "contentLength": String(attachment.contentLength ?? 0), + "contentDigest": attachment.contentDigest, + "secret": attachment.secret.toHex, + "salt": attachment.salt.toHex, + "nonce": attachment.nonce.toHex, + ] + } + return ["multiRemoteAttachment": [ + "attachments": attachmentMaps, + ]] + } catch { + return ["error": "Failed to decode multi remote attachment: \(error.localizedDescription)"] + } case ContentTypeReadReceipt.id where content is XMTP.ReadReceipt: return ["readReceipt": ""] case ContentTypeGroupUpdated.id where content is XMTP.GroupUpdated: @@ -260,7 +262,7 @@ struct ContentJson { "newValue": metadata.newValue, "fieldName": metadata.fieldName, ] - } + }, ]] default: if let encodedContent, let encodedContentJSON = try? encodedContent.jsonString() { @@ -270,59 +272,58 @@ struct ContentJson { } } } - } -struct ReactionV2Schema { - static func fromString(_ schema: String) -> FfiReactionSchema { - switch schema { - case "unicode": - return .unicode - case "shortcode": - return .shortcode - case "custom": - return .custom - default: - return .unknown - } - } - - static func toString(_ schema: FfiReactionSchema) -> String { - switch schema { - case .unicode: - return "unicode" - case .shortcode: - return "shortcode" - case .custom: - return "custom" - case .unknown: - return "unknown" - } - } +enum ReactionV2Schema { + static func fromString(_ schema: String) -> FfiReactionSchema { + switch schema { + case "unicode": + return .unicode + case "shortcode": + return .shortcode + case "custom": + return .custom + default: + return .unknown + } + } + + static func toString(_ schema: FfiReactionSchema) -> String { + switch schema { + case .unicode: + return "unicode" + case .shortcode: + return "shortcode" + case .custom: + return "custom" + case .unknown: + return "unknown" + } + } } -struct ReactionV2Action { - static func fromString(_ action: String) -> FfiReactionAction { - switch action { - case "removed": - return .removed - case "added": - return .added - default: - return .unknown - } - } - - static func toString(_ action: FfiReactionAction) -> String { - switch action { - case .removed: - return "removed" - case .added: - return "added" - case .unknown: - return "unknown" - } - } +enum ReactionV2Action { + static func fromString(_ action: String) -> FfiReactionAction { + switch action { + case "removed": + return .removed + case "added": + return .added + default: + return .unknown + } + } + + static func toString(_ action: FfiReactionAction) -> String { + switch action { + case .removed: + return "removed" + case .added: + return "added" + case .unknown: + return "unknown" + } + } } struct EncryptedAttachmentMetadata { @@ -354,7 +355,7 @@ struct EncryptedAttachmentMetadata { let secret = (obj["secret"] as? String ?? "").hexToData let salt = (obj["salt"] as? String ?? "").hexToData let nonce = (obj["nonce"] as? String ?? "").hexToData - + return EncryptedAttachmentMetadata( filename: obj["filename"] as? String ?? "", secret: secret, diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 2a5d95c82..83dd4c8ce 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -1062,7 +1062,10 @@ public class XMTPModule: Module { direction: getSortDirection( direction: queryParams.direction ?? "DESCENDING"), excludeContentTypes: nil, - excludeSenderInboxIds: queryParams.excludeSenderInboxIds + excludeSenderInboxIds: queryParams.excludeSenderInboxIds, + sortBy: getSortBy(sortBy: queryParams.sortBy), + insertedAfterNs: queryParams.insertedAfterNs, + insertedBeforeNs: queryParams.insertedBeforeNs ) return messages.compactMap { msg in @@ -1104,7 +1107,10 @@ public class XMTPModule: Module { direction: getSortDirection( direction: queryParams.direction ?? "DESCENDING"), excludeContentTypes: nil, - excludeSenderInboxIds: queryParams.excludeSenderInboxIds + excludeSenderInboxIds: queryParams.excludeSenderInboxIds, + sortBy: getSortBy(sortBy: queryParams.sortBy), + insertedAfterNs: queryParams.insertedAfterNs, + insertedBeforeNs: queryParams.insertedBeforeNs ) return messages.compactMap { msg in do { @@ -1118,6 +1124,35 @@ public class XMTPModule: Module { } } + AsyncFunction("countMessages") { + ( + installationId: String, conversationId: String, + queryParamsJson: String? + ) -> Int in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + + guard + let conversation = try await client.conversations + .findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "no conversation found for \(conversationId)") + } + let queryParams = MessageQueryParamsWrapper.messageQueryParamsFromJson( + queryParamsJson ?? "") + return try Int(await conversation.countMessages( + beforeNs: queryParams.beforeNs, + afterNs: queryParams.afterNs, + insertedAfterNs: queryParams.insertedAfterNs, + insertedBeforeNs: queryParams.insertedBeforeNs + )) + } + AsyncFunction("findMessage") { (installationId: String, messageId: String) -> String? in guard @@ -1686,7 +1721,7 @@ public class XMTPModule: Module { consentStates = nil } return try await client.conversations.syncAllConversations( - consentStates: consentStates).numSynced + consentStates: consentStates).numSynced } AsyncFunction("syncConversation") { @@ -2855,26 +2890,26 @@ public class XMTPModule: Module { return try ArchiveMetadataWrapper.encode(metadata) } - - AsyncFunction("leaveGroup") { ( + + AsyncFunction("leaveGroup") { ( installationId: String, groupId: String ) in - guard - let client = await clientsManager.getClient( - key: installationId) - else { - throw Error.noClient - } - guard - let group = try await client.conversations.findGroup( - groupId: groupId) - else { - throw Error.conversationNotFound( - "no conversation found for \(groupId)") - } - try await group.leaveGroup() - } + guard + let client = await clientsManager.getClient( + key: installationId) + else { + throw Error.noClient + } + guard + let group = try await client.conversations.findGroup( + groupId: groupId) + else { + throw Error.conversationNotFound( + "no conversation found for \(groupId)") + } + try await group.leaveGroup() + } } // @@ -3039,6 +3074,15 @@ public class XMTPModule: Module { } } + private func getSortBy(sortBy: String?) -> MessageSortBy { + switch sortBy { + case "INSERTED": + return .insertedAt + default: + return .sentAt + } + } + private func getConversationType(type: String) throws -> ConversationFilterType { @@ -3063,7 +3107,7 @@ public class XMTPModule: Module { case revokeInstallations } - func createApiClient(env: String, customLocalUrl: String? = nil, appVersion: String? = nil, gatewayHost: String? = nil) + func createApiClient(env: String, customLocalUrl: String? = nil, appVersion: String? = nil, gatewayHost: String? = nil) -> XMTP.ClientOptions.Api { switch env { @@ -3075,21 +3119,21 @@ public class XMTPModule: Module { env: XMTP.XMTPEnvironment.local, isSecure: false, appVersion: appVersion, - gatewayHost: gatewayHost + gatewayHost: gatewayHost ) case "production": return XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.production, isSecure: true, appVersion: appVersion, - gatewayHost: gatewayHost + gatewayHost: gatewayHost ) default: return XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.dev, isSecure: true, appVersion: appVersion, - gatewayHost: gatewayHost + gatewayHost: gatewayHost ) } } @@ -3105,7 +3149,7 @@ public class XMTPModule: Module { env: authOptions.environment, customLocalUrl: authOptions.customLocalUrl, appVersion: authOptions.appVersion, - gatewayHost: authOptions.gatewayHost, + gatewayHost: authOptions.gatewayHost ), preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, dbEncryptionKey: dbEncryptionKey, @@ -3113,7 +3157,7 @@ public class XMTPModule: Module { historySyncUrl: authOptions.historySyncUrl, deviceSyncEnabled: authOptions.deviceSyncEnabled, debugEventsEnabled: authOptions.debugEventsEnabled, - forkRecoveryOptions: authOptions.forkRecoveryOptions + forkRecoveryOptions: authOptions.forkRecoveryOptions ) } diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index b649b196f..f18254fc7 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency "MessagePacker" - s.dependency "XMTP", "= 4.6.1-rc3" + s.dependency "XMTP", "= 4.7.0-dev.ff66c0f" s.dependency 'CSecp256k1', '~> 0.2' s.dependency "SQLCipher", "= 4.5.7" end diff --git a/package.json b/package.json index e8571a371..2a7ed4e54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xmtp/react-native-sdk", - "version": "5.1.0-rc1", + "version": "5.2.0-dev", "description": "Wraps for native xmtp sdks for react native", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/src/index.ts b/src/index.ts index 92d928f84..f78f4d189 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,7 +41,11 @@ import { import { DecodedMessageUnion } from './lib/types/DecodedMessageUnion' import { DefaultContentTypes } from './lib/types/DefaultContentType' import { LogLevel, LogRotation } from './lib/types/LogTypes' -import { MessageId, MessageOrder } from './lib/types/MessagesOptions' +import { + MessageId, + MessageOrder, + MessageSortBy, +} from './lib/types/MessagesOptions' import { PermissionPolicySet } from './lib/types/PermissionPolicySet' export * from './context' @@ -787,7 +791,10 @@ export async function conversationMessages< afterNs?: number | undefined, direction?: MessageOrder | undefined, excludeContentTypes?: string[] | undefined, - excludeSenderInboxIds?: string[] | undefined + excludeSenderInboxIds?: string[] | undefined, + insertedAfterNs?: number | undefined, + insertedBeforeNs?: number | undefined, + sortBy?: MessageSortBy | undefined ): Promise[]> { const queryParamsJson = JSON.stringify({ limit, @@ -796,6 +803,9 @@ export async function conversationMessages< direction, excludeContentTypes, excludeSenderInboxIds, + insertedAfterNs, + insertedBeforeNs, + sortBy, }) const messages = await XMTPModule.conversationMessages( clientInstallationId, @@ -817,7 +827,10 @@ export async function conversationMessagesWithReactions< afterNs?: number | undefined, direction?: MessageOrder | undefined, excludeContentTypes?: string[] | undefined, - excludeSenderInboxIds?: string[] | undefined + excludeSenderInboxIds?: string[] | undefined, + insertedAfterNs?: number | undefined, + insertedBeforeNs?: number | undefined, + sortBy?: MessageSortBy | undefined ): Promise[]> { const queryParamsJson = JSON.stringify({ limit, @@ -826,6 +839,9 @@ export async function conversationMessagesWithReactions< direction, excludeContentTypes, excludeSenderInboxIds, + insertedAfterNs, + insertedBeforeNs, + sortBy, }) const messages = await XMTPModule.conversationMessagesWithReactions( clientInstallationId, @@ -837,6 +853,27 @@ export async function conversationMessagesWithReactions< }) } +export async function countMessages( + clientInstallationId: InstallationId, + conversationId: ConversationId, + beforeNs?: number | undefined, + afterNs?: number | undefined, + insertedAfterNs?: number | undefined, + insertedBeforeNs?: number | undefined +): Promise { + const queryParamsJson = JSON.stringify({ + beforeNs, + afterNs, + insertedAfterNs, + insertedBeforeNs, + }) + return await XMTPModule.countMessages( + clientInstallationId, + conversationId, + queryParamsJson + ) +} + export async function findMessage< ContentType extends DefaultContentTypes[number] = DefaultContentTypes[number], ContentTypes extends DefaultContentTypes = [ContentType], // Adjusted to work with arrays @@ -1906,7 +1943,11 @@ export { ConversationTopic, ConversationFilterType, } from './lib/types/ConversationOptions' -export { MessageId, MessageOrder } from './lib/types/MessagesOptions' +export { + MessageId, + MessageOrder, + MessageSortBy, +} from './lib/types/MessagesOptions' export { DecodedMessageUnion } from './lib/types/DecodedMessageUnion' export { DisappearingMessageSettings } from './lib/DisappearingMessageSettings' export { PublicIdentity } from './lib/PublicIdentity' diff --git a/src/lib/DecodedMessage.ts b/src/lib/DecodedMessage.ts index a89b4cbf7..2ea8a569f 100644 --- a/src/lib/DecodedMessage.ts +++ b/src/lib/DecodedMessage.ts @@ -29,6 +29,7 @@ export class DecodedMessage< contentTypeId: string senderInboxId: InboxId sentNs: number // timestamp in nanoseconds + insertedAtNs: number // timestamp when inserted into local db in nanoseconds nativeContent: NativeMessageContent fallback: string | undefined deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED @@ -56,6 +57,7 @@ export class DecodedMessage< decoded.contentTypeId, decoded.senderInboxId, decoded.sentNs, + decoded.insertedAtNs, decoded.content, decoded.fallback, decoded.deliveryStatus, @@ -72,6 +74,7 @@ export class DecodedMessage< contentTypeId: string senderInboxId: InboxId sentNs: number // timestamp in nanoseconds + insertedAtNs?: number // timestamp when inserted into local db in nanoseconds content: any fallback: string | undefined deliveryStatus: MessageDeliveryStatus | undefined @@ -82,6 +85,7 @@ export class DecodedMessage< object.contentTypeId, object.senderInboxId, object.sentNs, + object.insertedAtNs ?? object.sentNs, object.content, object.fallback, object.deliveryStatus @@ -94,6 +98,7 @@ export class DecodedMessage< contentTypeId: string, senderInboxId: InboxId, sentNs: number, + insertedAtNs: number, content: any, fallback: string | undefined, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED, @@ -104,6 +109,7 @@ export class DecodedMessage< this.contentTypeId = contentTypeId this.senderInboxId = senderInboxId this.sentNs = sentNs + this.insertedAtNs = insertedAtNs this.nativeContent = content // undefined comes back as null when bridged, ensure undefined so integrators don't have to add a new check for null as well this.fallback = fallback ?? undefined diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index b30ee473f..1d8e9eaed 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -214,7 +214,10 @@ export class Dm opts?.afterNs, opts?.direction, opts?.excludeContentTypes, - opts?.excludeSenderInboxIds + opts?.excludeSenderInboxIds, + opts?.insertedAfterNs, + opts?.insertedBeforeNs, + opts?.sortBy ) } @@ -240,7 +243,35 @@ export class Dm opts?.afterNs, opts?.direction, opts?.excludeContentTypes, - opts?.excludeSenderInboxIds + opts?.excludeSenderInboxIds, + opts?.insertedAfterNs, + opts?.insertedBeforeNs, + opts?.sortBy + ) + } + + /** + * Returns the count of messages in the dm. + * + * @param {number | undefined} beforeNs - Optional filter for messages before this timestamp. + * @param {number | undefined} afterNs - Optional filter for messages after this timestamp. + * @param {number | undefined} insertedAfterNs - Optional filter for messages inserted after this timestamp. + * @param {number | undefined} insertedBeforeNs - Optional filter for messages inserted before this timestamp. + * @returns {Promise} A Promise that resolves to the count of messages. + */ + async countMessages( + beforeNs?: number, + afterNs?: number, + insertedAfterNs?: number, + insertedBeforeNs?: number + ): Promise { + return await XMTP.countMessages( + this.client.installationId, + this.id, + beforeNs, + afterNs, + insertedAfterNs, + insertedBeforeNs ) } diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 36dfb0548..c1818f3b4 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -240,7 +240,10 @@ export class Group< opts?.afterNs, opts?.direction, opts?.excludeContentTypes, - opts?.excludeSenderInboxIds + opts?.excludeSenderInboxIds, + opts?.insertedAfterNs, + opts?.insertedBeforeNs, + opts?.sortBy ) } @@ -267,7 +270,35 @@ export class Group< opts?.afterNs, opts?.direction, opts?.excludeContentTypes, - opts?.excludeSenderInboxIds + opts?.excludeSenderInboxIds, + opts?.insertedAfterNs, + opts?.insertedBeforeNs, + opts?.sortBy + ) + } + + /** + * Returns the count of messages in the group. + * + * @param {number | undefined} beforeNs - Optional filter for messages before this timestamp. + * @param {number | undefined} afterNs - Optional filter for messages after this timestamp. + * @param {number | undefined} insertedAfterNs - Optional filter for messages inserted after this timestamp. + * @param {number | undefined} insertedBeforeNs - Optional filter for messages inserted before this timestamp. + * @returns {Promise} A Promise that resolves to the count of messages. + */ + async countMessages( + beforeNs?: number, + afterNs?: number, + insertedAfterNs?: number, + insertedBeforeNs?: number + ): Promise { + return await XMTP.countMessages( + this.client.installationId, + this.id, + beforeNs, + afterNs, + insertedAfterNs, + insertedBeforeNs ) } diff --git a/src/lib/types/MessagesOptions.ts b/src/lib/types/MessagesOptions.ts index 0adaefb46..5c34cdf8f 100644 --- a/src/lib/types/MessagesOptions.ts +++ b/src/lib/types/MessagesOptions.ts @@ -5,7 +5,11 @@ export type MessagesOptions = { direction?: MessageOrder | undefined excludeContentTypes?: string[] | undefined excludeSenderInboxIds?: string[] | undefined + insertedAfterNs?: number | undefined + insertedBeforeNs?: number | undefined + sortBy?: MessageSortBy | undefined } export type MessageOrder = 'ASCENDING' | 'DESCENDING' +export type MessageSortBy = 'SENT' | 'INSERTED' export type MessageId = string & { readonly brand: unique symbol }