diff --git a/.changeset/silly-foxes-smell.md b/.changeset/silly-foxes-smell.md new file mode 100644 index 00000000..d19c05eb --- /dev/null +++ b/.changeset/silly-foxes-smell.md @@ -0,0 +1,9 @@ +--- +"@xmtp/react-native-sdk": patch +--- + +Disappearing Messages +DM membership adds (increases message length by 1 for dm creators) +Bug fix key package issues +Bug fix rate limiting +Mark addAccount as a delicate API diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 72166377..5a7a057c 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -20,6 +20,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.ConversationParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.CreateGroupParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.MessageWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment +import expo.modules.xmtpreactnativesdk.wrappers.DisappearingMessageSettingsWrapper import expo.modules.xmtpreactnativesdk.wrappers.DmWrapper import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper @@ -55,6 +56,7 @@ import org.xmtp.android.library.codecs.EncryptedEncodedContent import org.xmtp.android.library.codecs.RemoteAttachment import org.xmtp.android.library.codecs.decoded import org.xmtp.android.library.hexToByteArray +import org.xmtp.android.library.libxmtp.DisappearingMessageSettings import org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.libxmtp.PermissionOption @@ -387,7 +389,7 @@ class XMTPModule : Module() { } } - AsyncFunction("addAccount") Coroutine { installationId: String, newAddress: String, walletParams: String -> + AsyncFunction("addAccount") Coroutine { installationId: String, newAddress: String, walletParams: String, allowReassignInboxId: Boolean -> withContext(Dispatchers.IO) { logV("addAccount") val client = clients[installationId] ?: throw XMTPException("No client") @@ -402,7 +404,7 @@ class XMTPModule : Module() { ) signer = reactSigner - client.addAccount(reactSigner) + client.addAccount(reactSigner, allowReassignInboxId) signer = null } } @@ -783,20 +785,30 @@ class XMTPModule : Module() { } } - AsyncFunction("findOrCreateDm") Coroutine { installationId: String, peerAddress: String -> + AsyncFunction("findOrCreateDm") Coroutine { installationId: String, peerAddress: String, disappearStartingAtNs: Long?, retentionDurationInNs: Long? -> withContext(Dispatchers.IO) { logV("findOrCreateDm") val client = clients[installationId] ?: throw XMTPException("No client") - val dm = client.conversations.findOrCreateDm(peerAddress) + val settings = if (disappearStartingAtNs != null && retentionDurationInNs != null) { + DisappearingMessageSettings(disappearStartingAtNs, retentionDurationInNs) + } else { + null + } + val dm = client.conversations.findOrCreateDm(peerAddress, settings) DmWrapper.encode(client, dm) } } - AsyncFunction("findOrCreateDmWithInboxId") Coroutine { installationId: String, peerInboxId: String -> + AsyncFunction("findOrCreateDmWithInboxId") Coroutine { installationId: String, peerInboxId: String, disappearStartingAtNs: Long?, retentionDurationInNs: Long? -> withContext(Dispatchers.IO) { logV("findOrCreateDmWithInboxId") val client = clients[installationId] ?: throw XMTPException("No client") - val dm = client.conversations.findOrCreateDmWithInboxId(peerInboxId) + val settings = if (disappearStartingAtNs != null && retentionDurationInNs != null) { + DisappearingMessageSettings(disappearStartingAtNs, retentionDurationInNs) + } else { + null + } + val dm = client.conversations.findOrCreateDmWithInboxId(peerInboxId, settings) DmWrapper.encode(client, dm) } } @@ -817,6 +829,7 @@ class XMTPModule : Module() { createGroupParams.groupName, createGroupParams.groupImageUrlSquare, createGroupParams.groupDescription, + createGroupParams.disappearingMessageSettings ) GroupWrapper.encode(client, group) } @@ -838,6 +851,7 @@ class XMTPModule : Module() { createGroupParams.groupName, createGroupParams.groupImageUrlSquare, createGroupParams.groupDescription, + createGroupParams.disappearingMessageSettings ) GroupWrapper.encode(client, group) } @@ -859,6 +873,7 @@ class XMTPModule : Module() { createGroupParams.groupName, createGroupParams.groupImageUrlSquare, createGroupParams.groupDescription, + createGroupParams.disappearingMessageSettings ) GroupWrapper.encode(client, group) } @@ -880,6 +895,7 @@ class XMTPModule : Module() { createGroupParams.groupName, createGroupParams.groupImageUrlSquare, createGroupParams.groupDescription, + createGroupParams.disappearingMessageSettings ) GroupWrapper.encode(client, group) } @@ -1045,6 +1061,52 @@ class XMTPModule : Module() { } } + AsyncFunction("disappearingMessageSettings") Coroutine { installationId: String, conversationId: String -> + withContext(Dispatchers.IO) { + logV("disappearingMessageSettings") + val client = clients[installationId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + ?: throw XMTPException("no conversation found for $conversationId") + val settings = conversation.disappearingMessageSettings + settings?.let { DisappearingMessageSettingsWrapper.encode(it) } + } + } + + AsyncFunction("isDisappearingMessagesEnabled") Coroutine { installationId: String, conversationId: String -> + withContext(Dispatchers.IO) { + logV("isDisappearingMessagesEnabled") + val client = clients[installationId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + ?: throw XMTPException("no conversation found for $conversationId") + conversation.isDisappearingMessagesEnabled + } + } + + AsyncFunction("clearDisappearingMessageSettings") Coroutine { installationId: String, conversationId: String -> + withContext(Dispatchers.IO) { + logV("clearDisappearingMessageSettings") + val client = clients[installationId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + ?: throw XMTPException("no conversation found for $conversationId") + conversation.clearDisappearingMessageSettings() + } + } + + AsyncFunction("updateDisappearingMessageSettings") Coroutine { installationId: String, conversationId: String, startAtNs: Long, durationInNs: Long -> + withContext(Dispatchers.IO) { + logV("updateDisappearingMessageSettings") + val client = clients[installationId] ?: throw XMTPException("No client") + val conversation = client.findConversation(conversationId) + ?: throw XMTPException("no conversation found for $conversationId") + conversation.updateDisappearingMessageSettings( + DisappearingMessageSettings( + startAtNs, + durationInNs + ) + ) + } + } + AsyncFunction("isGroupActive") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("isGroupActive") diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/CreateGroupParamsWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/CreateGroupParamsWrapper.kt index b330f4ea..41c3c0c8 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/CreateGroupParamsWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/CreateGroupParamsWrapper.kt @@ -1,19 +1,27 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.JsonParser +import org.xmtp.android.library.libxmtp.DisappearingMessageSettings class CreateGroupParamsWrapper( val groupName: String, val groupImageUrlSquare: String, val groupDescription: String, + val disappearingMessageSettings: DisappearingMessageSettings, ) { companion object { fun createGroupParamsFromJson(authParams: String): CreateGroupParamsWrapper { val jsonOptions = JsonParser.parseString(authParams).asJsonObject + val settings = DisappearingMessageSettings( + if (jsonOptions.has("disappearStartingAtNs")) jsonOptions.get("disappearStartingAtNs").asLong else 0, + if (jsonOptions.has("retentionDurationInNs")) jsonOptions.get("retentionDurationInNs").asLong else 0 + ) + return CreateGroupParamsWrapper( if (jsonOptions.has("name")) jsonOptions.get("name").asString else "", if (jsonOptions.has("imageUrlSquare")) jsonOptions.get("imageUrlSquare").asString else "", if (jsonOptions.has("description")) jsonOptions.get("description").asString else "", + settings ) } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DisappearingMessageSettingsWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DisappearingMessageSettingsWrapper.kt new file mode 100644 index 00000000..afdfe362 --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DisappearingMessageSettingsWrapper.kt @@ -0,0 +1,20 @@ +package expo.modules.xmtpreactnativesdk.wrappers + +import com.google.gson.GsonBuilder +import org.xmtp.android.library.libxmtp.DisappearingMessageSettings + +class DisappearingMessageSettingsWrapper { + + companion object { + fun encode(model: DisappearingMessageSettings): String { + val gson = GsonBuilder().create() + val message = encodeMap(model) + return gson.toJson(message) + } + + fun encodeMap(model: DisappearingMessageSettings): Map = mapOf( + "disappearStartingAtNs" to model.disappearStartingAtNs, + "retentionDurationInNs" to model.retentionDurationInNs, + ) + } +} \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index af67aa22..31c8f077 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -55,7 +55,7 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (3.0.26) + - LibXMTP (3.0.27) - MessagePacker (0.4.7) - MMKV (2.0.2): - MMKVCore (~> 2.0.2) @@ -448,18 +448,18 @@ PODS: - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.28.2) - - XMTP (3.0.29): + - XMTP (3.0.30): - Connect-Swift (= 1.0.0) - CryptoSwift (= 1.8.3) - CSecp256k1 (~> 0.2) - - LibXMTP (= 3.0.26) + - LibXMTP (= 3.0.27) - SQLCipher (= 4.5.7) - XMTPReactNative (3.1.12): - CSecp256k1 (~> 0.2) - ExpoModulesCore - MessagePacker - SQLCipher (= 4.5.7) - - XMTP (= 3.0.29) + - XMTP (= 3.0.30) - Yoga (1.14.0) DEPENDENCIES: @@ -530,7 +530,6 @@ DEPENDENCIES: - RNFS (from `../node_modules/react-native-fs`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) - - XMTP (from `https://github.com/xmtp/xmtp-ios.git`, commit `77940ee4790390154248d0fed0a2d5316fd99b3b`) - XMTPReactNative (from `../../ios`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -549,6 +548,7 @@ SPEC REPOS: - OpenSSL-Universal - SQLCipher - SwiftProtobuf + - XMTP EXTERNAL SOURCES: boost: @@ -679,21 +679,13 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-screens" RNSVG: :path: "../node_modules/react-native-svg" - XMTP: - :commit: 77940ee4790390154248d0fed0a2d5316fd99b3b - :git: https://github.com/xmtp/xmtp-ios.git XMTPReactNative: :path: "../../ios" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" -CHECKOUT OPTIONS: - XMTP: - :commit: 77940ee4790390154248d0fed0a2d5316fd99b3b - :git: https://github.com/xmtp/xmtp-ios.git - SPEC CHECKSUMS: - boost: 7dcd2de282d72e344012f7d6564d024930a6a440 + boost: 57d2868c099736d80fcd648bf211b4431e51a558 CoinbaseWalletSDK: ea1f37512bbc69ebe07416e3b29bf840f5cc3152 CoinbaseWalletSDKExpo: c79420eb009f482f768c23b6768fc5b2d7c98777 Connect-Swift: 84e043b904f63dc93a2c01c6c125da25e765b50d @@ -719,7 +711,7 @@ SPEC CHECKSUMS: glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: e80c8c226e67d8c820e81c5a2bfa1934ab5d263c + LibXMTP: 312922ac85f2b20983ba14beb08722b08002ded4 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: 3eacda84cd1c4fc95cf848d3ecb69d85ed56006c MMKVCore: 508b4d3a8ce031f1b5c8bd235f0517fb3f4c73a9 @@ -770,10 +762,10 @@ SPEC CHECKSUMS: RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d - XMTP: 5815a5886b5a698e910d7eb49c11681e9001c931 - XMTPReactNative: 68152197c8135a6e0fe03637e7cde6bdd896d75c + XMTP: 49cba75672b7a8e1962acb5202d9e6c839fca984 + XMTPReactNative: 1b030fd3a857084edcf9055965bbc1d7cb77b4f6 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 -PODFILE CHECKSUM: bb988e1a087fa2d3c0bc34c187128b2c7c6b2e58 +PODFILE CHECKSUM: 2d04c11c2661aeaad852cd3ada0b0f1b06e0cf24 COCOAPODS: 1.15.2 diff --git a/example/src/tests/clientTests.ts b/example/src/tests/clientTests.ts index 397bb405..f82495aa 100644 --- a/example/src/tests/clientTests.ts +++ b/example/src/tests/clientTests.ts @@ -6,6 +6,7 @@ import { assert, createClients, adaptEthersWalletToSigner, + assertEqual, } from './test-utils' import { Client } from '../../../src/index' @@ -435,6 +436,54 @@ test('can verify signatures', async () => { return true }) +test('test add account with existing InboxIds', async () => { + const [alixClient] = await createClients(1) + + const keyBytes = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + + const boWallet = Wallet.createRandom() + + const boClient = await Client.create(adaptEthersWalletToSigner(boWallet), { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + }) + + let errorThrown = false + try { + await alixClient.addAccount(adaptEthersWalletToSigner(boWallet)) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + errorThrown = true + } + + if (!errorThrown) { + throw new Error('Expected addAccount to throw an error but it did not') + } + + // Ensure that both clients have different inbox IDs + assert( + alixClient.inboxId !== boClient.inboxId, + 'Inbox ids should not be equal' + ) + + // Forcefully add the boClient account to alixClient + await alixClient.addAccount(adaptEthersWalletToSigner(boWallet), true) + + // Retrieve the inbox state and check the number of associated addresses + const state = await alixClient.inboxState(true) + await assertEqual(state.addresses.length, 2, 'Length should be 2') + + // Validate that the inbox ID from the address matches alixClient's inbox ID + const inboxId = await alixClient.findInboxIdFromAddress(boClient.address) + await assertEqual(inboxId, alixClient.inboxId, 'InboxIds should be equal') + + return true +}) + test('can add and remove accounts', async () => { const keyBytes = new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index f078bcc1..a06703d9 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -604,8 +604,8 @@ test('can list conversation messages', async () => { ) assert( - boDmMessages?.length === 2, - `alix conversation lengths should be 2 but was ${boDmMessages?.length}` + boDmMessages?.length === 3, + `alix conversation lengths should be 3 but was ${boDmMessages?.length}` ) return true diff --git a/example/src/tests/dmTests.ts b/example/src/tests/dmTests.ts index ce5b92b7..eb6a44f7 100644 --- a/example/src/tests/dmTests.ts +++ b/example/src/tests/dmTests.ts @@ -1,4 +1,10 @@ -import { Test, assert, createClients, delayToPropogate } from './test-utils' +import { + Test, + assert, + assertEqual, + createClients, + delayToPropogate, +} from './test-utils' import { Conversation } from '../../../src/index' export const dmTests: Test[] = [] @@ -223,3 +229,212 @@ test('can stream all dms', async () => { return true }) + +test('handles disappearing messages in a dm', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const initialSettings = { + disappearStartingAtNs: 1_000_000_000, + retentionDurationInNs: 1_000_000_000, // 1s duration + } + + // Create group with disappearing messages enabled + const boDm = await boClient.conversations.findOrCreateDm( + alixClient.address, + initialSettings + ) + + await boClient.conversations.findOrCreateDmWithInboxId( + caroClient.inboxId, + initialSettings + ) + + await boDm.send('howdy') + await alixClient.conversations.syncAllConversations() + + const alixDm = await alixClient.conversations.findDmByInboxId( + boClient.inboxId + ) + + // Validate initial state + await assertEqual( + () => boDm.messages().then((m) => m.length), + 2, + 'boDm should have 2 messages' + ) + await assertEqual( + () => alixDm!.messages().then((m) => m.length), + 1, + 'alixDm should have 1 message' + ) + await assertEqual( + () => boDm.disappearingMessageSettings() !== undefined, + true, + 'boDm should have disappearing settings' + ) + await assertEqual( + () => + boDm.disappearingMessageSettings().then((s) => s!.retentionDurationInNs), + 1_000_000_000, + 'Retention duration should be 1s' + ) + await assertEqual( + () => + boDm.disappearingMessageSettings().then((s) => s!.disappearStartingAtNs), + 1_000_000_000, + 'Disappearing should start at 1s' + ) + + // Wait for messages to disappear + await delayToPropogate(5000) + + // Validate messages are deleted + await assertEqual( + () => boDm.messages().then((m) => m.length), + 1, + 'boDm should have 1 remaining message' + ) + await assertEqual( + () => alixDm!.messages().then((m) => m.length), + 0, + 'alixDm should have 0 messages left' + ) + + // Disable disappearing messages + await boDm.clearDisappearingMessageSettings() + await delayToPropogate(1000) + + await boDm.sync() + await alixDm!.sync() + + await delayToPropogate(1000) + + // Validate disappearing messages are disabled + await assertEqual( + () => boDm.disappearingMessageSettings(), + undefined, + 'boDm should not have disappearing settings' + ) + await assertEqual( + () => alixDm!.disappearingMessageSettings(), + undefined, + 'alixDm should not have disappearing settings' + ) + + await assertEqual( + () => boDm.isDisappearingMessagesEnabled(), + false, + 'boDm should have disappearing disabled' + ) + await assertEqual( + () => alixDm!.isDisappearingMessagesEnabled(), + false, + 'alixDm should have disappearing disabled' + ) + + // Send messages after disabling disappearing settings + await boDm.send('message after disabling disappearing') + await alixDm!.send('another message after disabling') + await boDm.sync() + + await delayToPropogate(1000) + + // Ensure messages persist + await assertEqual( + () => boDm.messages().then((m) => m.length), + 5, + 'boDm should have 5 messages' + ) + await assertEqual( + () => alixDm!.messages().then((m) => m.length), + 4, + 'alixDm should have 4 messages' + ) + + // Re-enable disappearing messages + const updatedSettings = { + disappearStartingAtNs: (await boDm.messages())[0].sentNs + 1_000_000_000, // 1s from now + retentionDurationInNs: 1_000_000_000, + } + await boDm.updateDisappearingMessageSettings(updatedSettings) + await delayToPropogate(1000) + + await boDm.sync() + await alixDm!.sync() + + await delayToPropogate(1000) + + // Validate updated settings + await assertEqual( + () => + boDm.disappearingMessageSettings().then((s) => s!.disappearStartingAtNs), + updatedSettings.disappearStartingAtNs, + 'boDm disappearStartingAtNs should match updated settings' + ) + await assertEqual( + () => + alixDm! + .disappearingMessageSettings() + .then((s) => s!.disappearStartingAtNs), + updatedSettings.disappearStartingAtNs, + 'alixDm disappearStartingAtNs should match updated settings' + ) + + // Send new messages + await boDm.send('this will disappear soon') + await alixDm!.send('so will this') + await boDm.sync() + + await assertEqual( + () => boDm.messages().then((m) => m.length), + 9, + 'boDm should have 9 messages' + ) + await assertEqual( + () => alixDm!.messages().then((m) => m.length), + 8, + 'alixDm should have 8 messages' + ) + + await delayToPropogate(6000) + + // Validate messages were deleted + await assertEqual( + () => boDm.messages().then((m) => m.length), + 7, + 'boDm should have 7 messages left' + ) + await assertEqual( + () => alixDm!.messages().then((m) => m.length), + 6, + 'alixDm should have 6 messages left' + ) + + // Final validation that settings persist + await assertEqual( + () => + boDm.disappearingMessageSettings().then((s) => s!.retentionDurationInNs), + updatedSettings.retentionDurationInNs, + 'boDm retentionDuration should match updated settings' + ) + await assertEqual( + () => + alixDm! + .disappearingMessageSettings() + .then((s) => s!.retentionDurationInNs), + updatedSettings.retentionDurationInNs, + 'alixDm retentionDuration should match updated settings' + ) + await assertEqual( + () => boDm.isDisappearingMessagesEnabled(), + true, + 'boDm should have disappearing enabled' + ) + await assertEqual( + () => alixDm!.isDisappearingMessagesEnabled(), + true, + 'alixDm should have disappearing enabled' + ) + + return true +}) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index c5e2b671..0ad6906b 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -1,5 +1,6 @@ import { Wallet } from 'ethers' import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' +import { PermissionPolicySet } from 'xmtp-react-native-sdk/lib/types/PermissionPolicySet' import { Test, @@ -8,6 +9,7 @@ import { createGroups, delayToPropogate, adaptEthersWalletToSigner, + assertEqual, } from './test-utils' import { Client, @@ -2014,6 +2016,243 @@ test('can create new installation without breaking group', async () => { return true }) +test('handles disappearing messages in a group', async () => { + const [alixClient, boClient] = await createClients(2) + + const initialSettings = { + disappearStartingAtNs: 1_000_000_000, + retentionDurationInNs: 1_000_000_000, // 1s duration + } + + const customPermissionsPolicySet: PermissionPolicySet = { + addMemberPolicy: 'allow', + removeMemberPolicy: 'deny', + addAdminPolicy: 'admin', + removeAdminPolicy: 'superAdmin', + updateGroupNamePolicy: 'admin', + updateGroupDescriptionPolicy: 'allow', + updateGroupImagePolicy: 'admin', + updateMessageDisappearingPolicy: 'deny', + } + + // Create group with disappearing messages enabled + const boGroup = await boClient.conversations.newGroup([alixClient.address], { + disappearingMessageSettings: initialSettings, + }) + await boClient.conversations.newGroupWithInboxIds([alixClient.inboxId], { + disappearingMessageSettings: initialSettings, + }) + await boClient.conversations.newGroupCustomPermissions( + [alixClient.address], + customPermissionsPolicySet, + { + disappearingMessageSettings: initialSettings, + } + ) + await boClient.conversations.newGroupCustomPermissionsWithInboxIds( + [alixClient.inboxId], + customPermissionsPolicySet, + { + disappearingMessageSettings: initialSettings, + } + ) + + await boGroup.send('howdy') + await alixClient.conversations.syncAllConversations() + + const alixGroup = await alixClient.conversations.findGroup(boGroup.id) + + // Validate initial state + await assertEqual( + () => boGroup.messages().then((m) => m.length), + 2, + 'BoGroup should have 2 messages' + ) + await assertEqual( + () => alixGroup!.messages().then((m) => m.length), + 1, + 'AlixGroup should have 1 message' + ) + await assertEqual( + () => boGroup.disappearingMessageSettings() !== undefined, + true, + 'BoGroup should have disappearing settings' + ) + await assertEqual( + () => + boGroup + .disappearingMessageSettings() + .then((s) => s!.retentionDurationInNs), + 1_000_000_000, + 'Retention duration should be 1s' + ) + await assertEqual( + () => + boGroup + .disappearingMessageSettings() + .then((s) => s!.disappearStartingAtNs), + 1_000_000_000, + 'Disappearing should start at 1s' + ) + + // Wait for messages to disappear + await delayToPropogate(5000) + + // Validate messages are deleted + await assertEqual( + () => boGroup.messages().then((m) => m.length), + 1, + 'BoGroup should have 1 remaining message' + ) + await assertEqual( + () => alixGroup!.messages().then((m) => m.length), + 0, + 'AlixGroup should have 0 messages left' + ) + + // Disable disappearing messages + await boGroup.clearDisappearingMessageSettings() + await delayToPropogate(1000) + + await boGroup.sync() + await alixGroup!.sync() + + await delayToPropogate(1000) + + // Validate disappearing messages are disabled + await assertEqual( + () => boGroup.disappearingMessageSettings(), + undefined, + 'BoGroup should not have disappearing settings' + ) + await assertEqual( + () => alixGroup!.disappearingMessageSettings(), + undefined, + 'AlixGroup should not have disappearing settings' + ) + + await assertEqual( + () => boGroup.isDisappearingMessagesEnabled(), + false, + 'BoGroup should have disappearing disabled' + ) + await assertEqual( + () => alixGroup!.isDisappearingMessagesEnabled(), + false, + 'AlixGroup should have disappearing disabled' + ) + + // Send messages after disabling disappearing settings + await boGroup.send('message after disabling disappearing') + await alixGroup!.send('another message after disabling') + await boGroup.sync() + + await delayToPropogate(1000) + + // Ensure messages persist + await assertEqual( + () => boGroup.messages().then((m) => m.length), + 5, + 'BoGroup should have 5 messages' + ) + await assertEqual( + () => alixGroup!.messages().then((m) => m.length), + 4, + 'AlixGroup should have 4 messages' + ) + + // Re-enable disappearing messages + const updatedSettings = { + disappearStartingAtNs: (await boGroup.messages())[0].sentNs + 1_000_000_000, // 1s from now + retentionDurationInNs: 1_000_000_000, + } + await boGroup.updateDisappearingMessageSettings(updatedSettings) + await delayToPropogate(1000) + + await boGroup.sync() + await alixGroup!.sync() + + await delayToPropogate(1000) + + // Validate updated settings + await assertEqual( + () => + boGroup + .disappearingMessageSettings() + .then((s) => s!.disappearStartingAtNs), + updatedSettings.disappearStartingAtNs, + 'BoGroup disappearStartingAtNs should match updated settings' + ) + await assertEqual( + () => + alixGroup! + .disappearingMessageSettings() + .then((s) => s!.disappearStartingAtNs), + updatedSettings.disappearStartingAtNs, + 'AlixGroup disappearStartingAtNs should match updated settings' + ) + + // Send new messages + await boGroup.send('this will disappear soon') + await alixGroup!.send('so will this') + await boGroup.sync() + + await assertEqual( + () => boGroup.messages().then((m) => m.length), + 9, + 'BoGroup should have 9 messages' + ) + await assertEqual( + () => alixGroup!.messages().then((m) => m.length), + 8, + 'AlixGroup should have 8 messages' + ) + + await delayToPropogate(6000) + + // Validate messages were deleted + await assertEqual( + () => boGroup.messages().then((m) => m.length), + 7, + 'BoGroup should have 7 messages left' + ) + await assertEqual( + () => alixGroup!.messages().then((m) => m.length), + 6, + 'AlixGroup should have 6 messages left' + ) + + // Final validation that settings persist + await assertEqual( + () => + boGroup + .disappearingMessageSettings() + .then((s) => s!.retentionDurationInNs), + updatedSettings.retentionDurationInNs, + 'BoGroup retentionDuration should match updated settings' + ) + await assertEqual( + () => + alixGroup! + .disappearingMessageSettings() + .then((s) => s!.retentionDurationInNs), + updatedSettings.retentionDurationInNs, + 'AlixGroup retentionDuration should match updated settings' + ) + await assertEqual( + () => boGroup.isDisappearingMessagesEnabled(), + true, + 'BoGroup should have disappearing enabled' + ) + await assertEqual( + () => alixGroup!.isDisappearingMessagesEnabled(), + true, + 'AlixGroup should have disappearing enabled' + ) + + return true +}) + // Commenting this out so it doesn't block people, but nice to have? // test('can stream messages for a long time', async () => { // const bo = await Client.createRandom({ env: 'local', enableV3: true }) diff --git a/example/src/tests/test-utils.ts b/example/src/tests/test-utils.ts index 766e661e..378cd030 100644 --- a/example/src/tests/test-utils.ts +++ b/example/src/tests/test-utils.ts @@ -84,3 +84,12 @@ export function adaptEthersWalletToSigner(wallet: Wallet): Signer { signMessage: async (message: string) => wallet.signMessage(message), } } + +export async function assertEqual(actual: any, expected: any, message: string) { + const resolvedActual = typeof actual === 'function' ? await actual() : actual + + assert( + resolvedActual === expected, + `${message} Expected: ${expected}, but was: ${resolvedActual}` + ) +} diff --git a/ios/Wrappers/CreateGroupParamsWrapper.swift b/ios/Wrappers/CreateGroupParamsWrapper.swift index e4c36ac1..6a6fbda7 100644 --- a/ios/Wrappers/CreateGroupParamsWrapper.swift +++ b/ios/Wrappers/CreateGroupParamsWrapper.swift @@ -1,22 +1,36 @@ import Foundation +import XMTP struct CreateGroupParamsWrapper { - let groupName: String - let groupImageUrlSquare: String - let groupDescription: String + let groupName: String + let groupImageUrlSquare: String + let groupDescription: String + let disappearingMessageSettings: DisappearingMessageSettings - static func createGroupParamsFromJson(_ authParams: String) -> CreateGroupParamsWrapper { - let data = authParams.data(using: .utf8) ?? Data() - let jsonOptions = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] ?? [:] - - let groupName = jsonOptions["name"] as? String ?? "" - let groupImageUrlSquare = jsonOptions["imageUrlSquare"] as? String ?? "" - let groupDescription = jsonOptions["description"] as? String ?? "" - - return CreateGroupParamsWrapper( - groupName: groupName, - groupImageUrlSquare: groupImageUrlSquare, - groupDescription: groupDescription - ) - } + static func createGroupParamsFromJson(_ authParams: String) + -> CreateGroupParamsWrapper + { + let data = authParams.data(using: .utf8) ?? Data() + let jsonOptions = + (try? JSONSerialization.jsonObject(with: data, options: [])) + as? [String: Any] ?? [:] + + let settings = DisappearingMessageSettings( + disappearStartingAtNs: jsonOptions["disappearStartingAtNs"] + as? Int64 ?? 0, + retentionDurationInNs: jsonOptions["retentionDurationInNs"] + as? Int64 ?? 0 + ) + + let groupName = jsonOptions["name"] as? String ?? "" + let groupImageUrlSquare = jsonOptions["imageUrlSquare"] as? String ?? "" + let groupDescription = jsonOptions["description"] as? String ?? "" + + return CreateGroupParamsWrapper( + groupName: groupName, + groupImageUrlSquare: groupImageUrlSquare, + groupDescription: groupDescription, + disappearingMessageSettings: settings + ) + } } diff --git a/ios/Wrappers/DisappearingMessageSettingsWrapper.swift b/ios/Wrappers/DisappearingMessageSettingsWrapper.swift new file mode 100644 index 00000000..c30cbd69 --- /dev/null +++ b/ios/Wrappers/DisappearingMessageSettingsWrapper.swift @@ -0,0 +1,24 @@ +import Foundation +import XMTP + +struct DisappearingMessageSettingsWrapper { + static func encodeToObj(_ settings: XMTP.DisappearingMessageSettings) throws + -> [String: Any] + { + return [ + "disappearStartingAtNs": settings.disappearStartingAtNs, + "retentionDurationInNs": settings.retentionDurationInNs, + ] + } + + static func encode(_ entry: XMTP.DisappearingMessageSettings) throws + -> String + { + let obj = try encodeToObj(entry) + let data = try JSONSerialization.data(withJSONObject: obj) + guard let result = String(data: data, encoding: .utf8) else { + throw WrapperError.encodeError("could not encode expirations") + } + return result + } +} diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 8bd3188d..eb7f4518 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -341,7 +341,10 @@ public class XMTPModule: Module { } AsyncFunction("addAccount") { - (installationId: String, newAddress: String, walletParams: String) + ( + installationId: String, newAddress: String, + walletParams: String, allowReassignInboxId: Bool + ) in guard let client = await clientsManager.getClient(key: installationId) @@ -357,7 +360,8 @@ public class XMTPModule: Module { blockNumber: walletOptions.blockNumber) self.signer = signer - try await client.addAccount(newAccount: signer) + try await client.addAccount( + newAccount: signer, allowReassignInboxId: allowReassignInboxId) self.signer = nil } @@ -658,27 +662,36 @@ public class XMTPModule: Module { } AsyncFunction("conversationMessagesWithReactions") { - ( - installationId: String, conversationId: String, limit: Int?, - beforeNs: Double?, afterNs: Double?, direction: String? - ) -> [String] in - guard let client = await clientsManager.getClient(key: installationId) else { + ( + installationId: String, conversationId: String, limit: Int?, + beforeNs: Double?, afterNs: Double?, direction: String? + ) -> [String] in + guard + let client = await clientsManager.getClient(key: installationId) + else { throw Error.noClient } - guard let conversation = try await client.findConversation(conversationId: conversationId) else { - throw Error.conversationNotFound("no conversation found for \(conversationId)") + guard + let conversation = try await client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "no conversation found for \(conversationId)") } let messages = try await conversation.messagesWithReactions( limit: limit, beforeNs: beforeNs != nil ? Int64(beforeNs!) : nil, afterNs: afterNs != nil ? Int64(afterNs!) : nil, - direction: getSortDirection(direction: direction ?? "DESCENDING") + direction: getSortDirection( + direction: direction ?? "DESCENDING") ) return messages.compactMap { msg in do { return try MessageWrapper.encode(msg) } catch { - print("discarding message, unable to encode wrapper \(msg.id)") + print( + "discarding message, unable to encode wrapper \(msg.id)" + ) return nil } } @@ -888,34 +901,52 @@ public class XMTPModule: Module { } AsyncFunction("findOrCreateDm") { - (installationId: String, peerAddress: String) -> String in + ( + installationId: String, peerAddress: String, + disappearStartingAtNs: Int64?, retentionDurationInNs: Int64? + ) -> String in guard let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } + let settings = + (disappearStartingAtNs != nil && retentionDurationInNs != nil) + ? DisappearingMessageSettings( + disappearStartingAtNs: disappearStartingAtNs!, + retentionDurationInNs: retentionDurationInNs!) : nil do { let dm = try await client.conversations.findOrCreateDm( - with: peerAddress) + with: peerAddress, disappearingMessageSettings: settings) return try await DmWrapper.encode(dm, client: client) } catch { print("ERRRO!: \(error.localizedDescription)") throw error } } - + AsyncFunction("findOrCreateDmWithInboxId") { - (installationId: String, peerInboxId: String) -> String in + ( + installationId: String, peerInboxId: String, + disappearStartingAtNs: Int64?, retentionDurationInNs: Int64? + ) -> String in guard let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } + let settings = + (disappearStartingAtNs != nil && retentionDurationInNs != nil) + ? DisappearingMessageSettings( + disappearStartingAtNs: disappearStartingAtNs!, + retentionDurationInNs: retentionDurationInNs!) : nil do { - let dm = try await client.conversations.findOrCreateDmWithInboxId( - with: peerInboxId) + let dm = try await client.conversations + .findOrCreateDmWithInboxId( + with: peerInboxId, disappearingMessageSettings: settings + ) return try await DmWrapper.encode(dm, client: client) } catch { print("ERRRO!: \(error.localizedDescription)") @@ -951,7 +982,9 @@ public class XMTPModule: Module { permissions: permissionLevel, name: createGroupParams.groupName, imageUrlSquare: createGroupParams.groupImageUrlSquare, - description: createGroupParams.groupDescription + description: createGroupParams.groupDescription, + disappearingMessageSettings: createGroupParams + .disappearingMessageSettings ) return try await GroupWrapper.encode(group, client: client) } catch { @@ -983,7 +1016,9 @@ public class XMTPModule: Module { permissionPolicySet: permissionPolicySet, name: createGroupParams.groupName, imageUrlSquare: createGroupParams.groupImageUrlSquare, - description: createGroupParams.groupDescription + description: createGroupParams.groupDescription, + disappearingMessageSettings: createGroupParams + .disappearingMessageSettings ) return try await GroupWrapper.encode(group, client: client) } catch { @@ -991,7 +1026,7 @@ public class XMTPModule: Module { throw error } } - + AsyncFunction("createGroupWithInboxIds") { ( installationId: String, inboxIds: [String], @@ -1020,7 +1055,9 @@ public class XMTPModule: Module { permissions: permissionLevel, name: createGroupParams.groupName, imageUrlSquare: createGroupParams.groupImageUrlSquare, - description: createGroupParams.groupDescription + description: createGroupParams.groupDescription, + disappearingMessageSettings: createGroupParams + .disappearingMessageSettings ) return try await GroupWrapper.encode(group, client: client) } catch { @@ -1052,7 +1089,9 @@ public class XMTPModule: Module { permissionPolicySet: permissionPolicySet, name: createGroupParams.groupName, imageUrlSquare: createGroupParams.groupImageUrlSquare, - description: createGroupParams.groupDescription + description: createGroupParams.groupDescription, + disappearingMessageSettings: createGroupParams + .disappearingMessageSettings ) return try await GroupWrapper.encode(group, client: client) } catch { @@ -1133,7 +1172,8 @@ public class XMTPModule: Module { } AsyncFunction("syncAllConversations") { - (installationId: String, consentStringStates: [String]?) -> UInt32 in + (installationId: String, consentStringStates: [String]?) -> UInt32 + in guard let client = await clientsManager.getClient(key: installationId) else { @@ -1143,7 +1183,7 @@ public class XMTPModule: Module { if let states = consentStringStates { consentStates = try getConsentStates(states: states) } else { - consentStates = nil + consentStates = nil } return try await client.conversations.syncAllConversations( consentStates: consentStates) @@ -1315,6 +1355,83 @@ public class XMTPModule: Module { groupDescription: description) } + AsyncFunction("disappearingMessageSettings") { + (installationId: String, conversationId: String) -> String? in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + guard + let conversation = try await client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "No conversation found for \(conversationId)") + } + return try conversation.disappearingMessageSettings.map { + try DisappearingMessageSettingsWrapper.encode($0) + } + } + + AsyncFunction("isDisappearingMessagesEnabled") { + (installationId: String, conversationId: String) -> Bool in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + guard + let conversation = try await client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "No conversation found for \(conversationId)") + } + return try conversation.isDisappearingMessagesEnabled() + } + + AsyncFunction("clearDisappearingMessageSettings") { + (installationId: String, conversationId: String) in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + guard + let conversation = try await client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "No conversation found for \(conversationId)") + } + try await conversation.clearDisappearingMessageSettings() + } + + AsyncFunction("updateDisappearingMessageSettings") { + ( + installationId: String, conversationId: String, + startAtNs: Int64, durationInNs: Int64 + ) in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + guard + let conversation = try await client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "No conversation found for \(conversationId)") + } + try await conversation.updateDisappearingMessageSettings( + DisappearingMessageSettings( + disappearStartingAtNs: startAtNs, + retentionDurationInNs: durationInNs) + ) + } + AsyncFunction("isGroupActive") { (installationId: String, id: String) -> Bool in guard diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index b2d341e7..f86c0f77 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", "= 3.0.29" + s.dependency "XMTP", "= 3.0.30" s.dependency 'CSecp256k1', '~> 0.2' s.dependency "SQLCipher", "= 4.5.7" end diff --git a/src/index.ts b/src/index.ts index 2ac6f54c..fe26ed11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { } from './lib/ContentCodec' import { Conversation, ConversationVersion } from './lib/Conversation' import { DecodedMessage, MessageDeliveryStatus } from './lib/DecodedMessage' +import { DisappearingMessageSettings } from './lib/DisappearingMessageSettings' import { Dm } from './lib/Dm' import { Group, PermissionUpdateOption } from './lib/Group' import { InboxState } from './lib/InboxState' @@ -236,7 +237,8 @@ export async function addAccount( newAddress: Address, walletType?: WalletType | undefined, chainId?: number | undefined, - blockNumber?: number | undefined + blockNumber?: number | undefined, + allowReassignInboxId: boolean = false ) { const walletParams: WalletParams = { walletType, @@ -246,7 +248,8 @@ export async function addAccount( return XMTPModule.addAccount( installationId, newAddress, - JSON.stringify(walletParams) + JSON.stringify(walletParams), + allowReassignInboxId ) } @@ -682,10 +685,17 @@ export async function findOrCreateDm< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - peerAddress: Address + peerAddress: Address, + disappearStartingAtNs: number | undefined, + retentionDurationInNs: number | undefined ): Promise> { const dm = JSON.parse( - await XMTPModule.findOrCreateDm(client.installationId, peerAddress) + await XMTPModule.findOrCreateDm( + client.installationId, + peerAddress, + disappearStartingAtNs, + retentionDurationInNs + ) ) return new Dm(client, dm) } @@ -694,12 +704,16 @@ export async function findOrCreateDmWithInboxId< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - peerInboxId: InboxId + peerInboxId: InboxId, + disappearStartingAtNs: number | undefined, + retentionDurationInNs: number | undefined ): Promise> { const dm = JSON.parse( await XMTPModule.findOrCreateDmWithInboxId( client.installationId, - peerInboxId + peerInboxId, + disappearStartingAtNs, + retentionDurationInNs ) ) return new Dm(client, dm) @@ -713,12 +727,16 @@ export async function createGroup< permissionLevel: 'all_members' | 'admin_only' = 'all_members', name: string = '', imageUrlSquare: string = '', - description: string = '' + description: string = '', + disappearStartingAtNs: number = 0, + retentionDurationInNs: number = 0 ): Promise> { const options: CreateGroupParams = { name, imageUrlSquare, description, + disappearStartingAtNs, + retentionDurationInNs, } const group = JSON.parse( await XMTPModule.createGroup( @@ -740,12 +758,16 @@ export async function createGroupCustomPermissionsWithInboxIds< permissionPolicySet: PermissionPolicySet, name: string = '', imageUrlSquare: string = '', - description: string = '' + description: string = '', + disappearStartingAtNs: number = 0, + retentionDurationInNs: number = 0 ): Promise> { const options: CreateGroupParams = { name, imageUrlSquare, description, + disappearStartingAtNs, + retentionDurationInNs, } const group = JSON.parse( await XMTPModule.createGroupCustomPermissionsWithInboxIds( @@ -767,12 +789,16 @@ export async function createGroupWithInboxIds< permissionLevel: 'all_members' | 'admin_only' = 'all_members', name: string = '', imageUrlSquare: string = '', - description: string = '' + description: string = '', + disappearStartingAtNs: number = 0, + retentionDurationInNs: number = 0 ): Promise> { const options: CreateGroupParams = { name, imageUrlSquare, description, + disappearStartingAtNs, + retentionDurationInNs, } const group = JSON.parse( await XMTPModule.createGroupWithInboxIds( @@ -794,12 +820,16 @@ export async function createGroupCustomPermissions< permissionPolicySet: PermissionPolicySet, name: string = '', imageUrlSquare: string = '', - description: string = '' + description: string = '', + disappearStartingAtNs: number = 0, + retentionDurationInNs: number = 0 ): Promise> { const options: CreateGroupParams = { name, imageUrlSquare, description, + disappearStartingAtNs, + retentionDurationInNs, } const group = JSON.parse( await XMTPModule.createGroupCustomPermissions( @@ -937,6 +967,58 @@ export function updateGroupDescription( return XMTPModule.updateGroupDescription(installationId, id, description) } +export async function disappearingMessageSettings( + installationId: string, + conversationId: string +): Promise { + const settings = JSON.parse( + await XMTPModule.disappearingMessageSettings(installationId, conversationId) + ) + + if (!settings) { + return undefined + } else { + return new DisappearingMessageSettings( + settings.disappearStartingAtNs, + settings.retentionDurationInNs + ) + } +} + +export async function isDisappearingMessagesEnabled( + installationId: string, + conversationId: string +): Promise { + return await XMTPModule.isDisappearingMessagesEnabled( + installationId, + conversationId + ) +} + +export async function clearDisappearingMessageSettings( + installationId: string, + conversationId: string +): Promise { + return await XMTPModule.clearDisappearingMessageSettings( + installationId, + conversationId + ) +} + +export async function updateDisappearingMessageSettings( + installationId: string, + conversationId: string, + startAtNs: number, + durationInNs: number +): Promise { + return await XMTPModule.updateDisappearingMessageSettings( + installationId, + conversationId, + startAtNs, + durationInNs + ) +} + export function isGroupActive( installationId: InstallationId, id: ConversationId @@ -1297,6 +1379,8 @@ interface CreateGroupParams { name: string imageUrlSquare: string description: string + disappearStartingAtNs: number + retentionDurationInNs: number } export { Client } from './lib/Client' @@ -1316,3 +1400,4 @@ export { } from './lib/types/ConversationOptions' export { MessageId, MessageOrder } from './lib/types/MessagesOptions' export { DecodedMessageUnion } from './lib/types/DecodedMessageUnion' +export { DisappearingMessageSettings } from './lib/DisappearingMessageSettings' diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 1b6d8dd0..66fe9b8b 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -355,9 +355,18 @@ export class Client< /** * Add this account to the current inboxId. + * Adding a wallet already associated with an inboxId will cause the wallet to lose access to that inbox. * @param {Signer} newAccount - The signer of the new account to be added. + * @param {boolean} allowReassignInboxId - A boolean specifying if the inboxId should be reassigned or not. */ - async addAccount(newAccount: Signer | WalletClient) { + async addAccount( + newAccount: Signer | WalletClient, + allowReassignInboxId: boolean = false + ) { + console.warn( + '⚠️ This function is delicate and should be used with caution. ' + + 'Adding a wallet already associated with an inboxId will cause the wallet to lose access to that inbox.' + ) const signer = getSigner(newAccount) if (!signer) { throw new Error('Signer is not configured') @@ -385,7 +394,8 @@ export class Client< await signer.getAddress(), signer.walletType?.(), signer.getChainId?.(), - signer.getBlockNumber?.() + signer.getBlockNumber?.(), + allowReassignInboxId ) Client.signSubscription?.remove() resolve() @@ -584,6 +594,10 @@ export class Client< * Drop the local database connection. This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase() */ async dropLocalDatabaseConnection() { + console.warn( + '⚠️ This function is delicate and should be used with caution. ' + + 'App will error if database not properly reconnected. See: reconnectLocalDatabase()' + ) return await XMTPModule.dropLocalDatabaseConnection(this.installationId) } diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index 9457b01e..7529c4a2 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -7,7 +7,14 @@ import { } from './types' import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' -import { DecodedMessage, Member, Dm, Group, Client } from '../index' +import { + DecodedMessage, + Member, + Dm, + Group, + Client, + DisappearingMessageSettings, +} from '../index' export enum ConversationVersion { GROUP = 'GROUP', @@ -38,6 +45,14 @@ export interface ConversationBase { ): Promise<() => void> consentState(): Promise updateConsent(state: ConsentState): Promise + disappearingMessageSettings(): Promise< + DisappearingMessageSettings | undefined + > + isDisappearingMessagesEnabled(): Promise + clearDisappearingMessageSettings(): Promise + updateDisappearingMessageSettings( + disappearingMessageSettings: DisappearingMessageSettings + ): Promise processMessage( encryptedMessage: string ): Promise> diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index fd748dfe..469ac260 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -3,6 +3,7 @@ import { keystore } from '@xmtp/proto' import { Client, InboxId } from './Client' import { ConversationVersion } from './Conversation' import { DecodedMessage } from './DecodedMessage' +import { DisappearingMessageSettings } from './DisappearingMessageSettings' import { Dm, DmParams } from './Dm' import { Group, GroupParams } from './Group' import { ConversationOptions } from './types/ConversationOptions' @@ -125,13 +126,20 @@ export default class Conversations< * This method creates a new conversation with the specified peer address and context. * * @param {Address} peerAddress - The address of the peer to create a conversation with. + * @param {DisappearingMessageSettings} disappearingMessageSettings - The disappearing message settings for this dm or undefined. * @returns {Promise} A Promise that resolves to a Conversation object. */ async newConversation( - peerAddress: Address + peerAddress: Address, + disappearingMessageSettings?: DisappearingMessageSettings | undefined ): Promise> { const checksumAddress = getAddress(peerAddress) - return await XMTPModule.findOrCreateDm(this.client, checksumAddress) + return await XMTPModule.findOrCreateDm( + this.client, + checksumAddress, + disappearingMessageSettings?.disappearStartingAtNs, + disappearingMessageSettings?.retentionDurationInNs + ) } /** @@ -140,10 +148,19 @@ export default class Conversations< * This method creates a new conversation with the specified peer address. * * @param {Address} peerAddress - The address of the peer to create a conversation with. + * @param {DisappearingMessageSettings} disappearingMessageSettings - The disappearing message settings for this dm or undefined. * @returns {Promise} A Promise that resolves to a Dm object. */ - async findOrCreateDm(peerAddress: Address): Promise> { - return await XMTPModule.findOrCreateDm(this.client, peerAddress) + async findOrCreateDm( + peerAddress: Address, + disappearingMessageSettings?: DisappearingMessageSettings | undefined + ): Promise> { + return await XMTPModule.findOrCreateDm( + this.client, + peerAddress, + disappearingMessageSettings?.disappearStartingAtNs, + disappearingMessageSettings?.retentionDurationInNs + ) } /** @@ -152,12 +169,19 @@ export default class Conversations< * This method creates a new conversation with the specified peer inboxId. * * @param {InboxId} peerInboxId - The inboxId of the peer to create a conversation with. + * @param {DisappearingMessageSettings} disappearingMessageSettings - The disappearing message settings for this dm or undefined. * @returns {Promise} A Promise that resolves to a Dm object. */ async findOrCreateDmWithInboxId( - peerInboxId: InboxId + peerInboxId: InboxId, + disappearingMessageSettings?: DisappearingMessageSettings | undefined ): Promise> { - return await XMTPModule.findOrCreateDmWithInboxId(this.client, peerInboxId) + return await XMTPModule.findOrCreateDmWithInboxId( + this.client, + peerInboxId, + disappearingMessageSettings?.disappearStartingAtNs, + disappearingMessageSettings?.retentionDurationInNs + ) } /** @@ -179,7 +203,9 @@ export default class Conversations< opts?.permissionLevel, opts?.name, opts?.imageUrlSquare, - opts?.description + opts?.description, + opts?.disappearingMessageSettings?.disappearStartingAtNs, + opts?.disappearingMessageSettings?.retentionDurationInNs ) } @@ -204,7 +230,9 @@ export default class Conversations< permissionPolicySet, opts?.name, opts?.imageUrlSquare, - opts?.description + opts?.description, + opts?.disappearingMessageSettings?.disappearStartingAtNs, + opts?.disappearingMessageSettings?.retentionDurationInNs ) } @@ -227,7 +255,9 @@ export default class Conversations< opts?.permissionLevel, opts?.name, opts?.imageUrlSquare, - opts?.description + opts?.description, + opts?.disappearingMessageSettings?.disappearStartingAtNs, + opts?.disappearingMessageSettings?.retentionDurationInNs ) } @@ -252,7 +282,9 @@ export default class Conversations< permissionPolicySet, opts?.name, opts?.imageUrlSquare, - opts?.description + opts?.description, + opts?.disappearingMessageSettings?.disappearStartingAtNs, + opts?.disappearingMessageSettings?.retentionDurationInNs ) } diff --git a/src/lib/DisappearingMessageSettings.ts b/src/lib/DisappearingMessageSettings.ts new file mode 100644 index 00000000..2ad2931e --- /dev/null +++ b/src/lib/DisappearingMessageSettings.ts @@ -0,0 +1,17 @@ +export class DisappearingMessageSettings { + disappearStartingAtNs: number + retentionDurationInNs: number + + constructor(disappearStartingAtNs: number, retentionDurationInNs: number) { + this.disappearStartingAtNs = disappearStartingAtNs + this.retentionDurationInNs = retentionDurationInNs + } + + static from(json: string): DisappearingMessageSettings { + const entry = JSON.parse(json) + return new DisappearingMessageSettings( + entry.disappearStartingAtNs, + entry.retentionDurationInNs + ) + } +} diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index 85d0f146..85621f2c 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -10,7 +10,11 @@ import { EventTypes } from './types/EventTypes' import { MessageId, MessagesOptions } from './types/MessagesOptions' import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' -import { ConversationId, ConversationTopic } from '../index' +import { + ConversationId, + ConversationTopic, + DisappearingMessageSettings, +} from '../index' export interface DmParams { id: ConversationId @@ -301,6 +305,57 @@ export class Dm ) } + /** + * Returns the disappearing message settings. + * To get the latest settings from the network, call sync() first. + * @returns {Promise} A Promise that resolves to the disappearing message settings. + */ + async disappearingMessageSettings(): Promise< + DisappearingMessageSettings | undefined + > { + return XMTP.disappearingMessageSettings(this.client.installationId, this.id) + } + + /** + * Checks if disappearing messages are enabled. + * @returns {Promise} A Promise that resolves to a boolean indicating whether disappearing messages are enabled. + */ + async isDisappearingMessagesEnabled(): Promise { + return XMTP.isDisappearingMessagesEnabled( + this.client.installationId, + this.id + ) + } + + /** + * Clears the disappearing message settings for this group. + * Will throw if the user does not have the required permissions. + * @returns {Promise} A Promise that resolves when the settings are cleared. + */ + async clearDisappearingMessageSettings(): Promise { + return XMTP.clearDisappearingMessageSettings( + this.client.installationId, + this.id + ) + } + + /** + * Updates the disappearing message settings. + * Will throw if the user does not have the required permissions. + * @param {DisappearingMessageSettings} disappearingMessageSettings The new disappearing message setting. + * @returns {Promise} A Promise that resolves when the settings are updated. + */ + async updateDisappearingMessageSettings( + disappearingMessageSettings: DisappearingMessageSettings + ): Promise { + return XMTP.updateDisappearingMessageSettings( + this.client.installationId, + this.id, + disappearingMessageSettings.disappearStartingAtNs, + disappearingMessageSettings.retentionDurationInNs + ) + } + /** * * @returns {Promise} A Promise that resolves to an array of Member objects. diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 09b5f36c..0b4b853a 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -11,7 +11,12 @@ import { MessageId, MessagesOptions } from './types/MessagesOptions' import { PermissionPolicySet } from './types/PermissionPolicySet' import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' -import { Address, ConversationId, ConversationTopic } from '../index' +import { + Address, + ConversationId, + ConversationTopic, + DisappearingMessageSettings, +} from '../index' export type PermissionUpdateOption = 'allow' | 'deny' | 'admin' | 'super_admin' @@ -416,6 +421,57 @@ export class Group< ) } + /** + * Returns the disappearing message settings. + * To get the latest settings from the network, call sync() first. + * @returns {Promise} A Promise that resolves to the disappearing message settings. + */ + async disappearingMessageSettings(): Promise< + DisappearingMessageSettings | undefined + > { + return XMTP.disappearingMessageSettings(this.client.installationId, this.id) + } + + /** + * Checks if disappearing messages are enabled. + * @returns {Promise} A Promise that resolves to a boolean indicating whether disappearing messages are enabled. + */ + async isDisappearingMessagesEnabled(): Promise { + return XMTP.isDisappearingMessagesEnabled( + this.client.installationId, + this.id + ) + } + + /** + * Clears the disappearing message settings for this group. + * Will throw if the user does not have the required permissions. + * @returns {Promise} A Promise that resolves when the settings are cleared. + */ + async clearDisappearingMessageSettings(): Promise { + return XMTP.clearDisappearingMessageSettings( + this.client.installationId, + this.id + ) + } + + /** + * Updates the disappearing message settings. + * Will throw if the user does not have the required permissions. + * @param {DisappearingMessageSettings} disappearingMessageSettings The new disappearing message setting. + * @returns {Promise} A Promise that resolves when the settings are updated. + */ + async updateDisappearingMessageSettings( + disappearingMessageSettings: DisappearingMessageSettings + ): Promise { + return XMTP.updateDisappearingMessageSettings( + this.client.installationId, + this.id, + disappearingMessageSettings.disappearStartingAtNs, + disappearingMessageSettings.retentionDurationInNs + ) + } + /** * Returns whether the group is active. * To get the latest active status from the network, call sync() first diff --git a/src/lib/types/CreateGroupOptions.ts b/src/lib/types/CreateGroupOptions.ts index c527f026..853398e5 100644 --- a/src/lib/types/CreateGroupOptions.ts +++ b/src/lib/types/CreateGroupOptions.ts @@ -1,6 +1,9 @@ +import { DisappearingMessageSettings } from '../DisappearingMessageSettings' + export type CreateGroupOptions = { permissionLevel?: 'all_members' | 'admin_only' | undefined name?: string | undefined imageUrlSquare?: string | undefined description?: string | undefined + disappearingMessageSettings?: DisappearingMessageSettings | undefined }