From e3003fd1f517ac1a08ca22b30bc7422fdca6256f Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:37:21 +0000 Subject: [PATCH 1/5] simplex-chat-nodejs: fix userChatRelay type error in apiCreateActiveUser (#6764) The @simplex-chat/types package (auto-generated from Haskell types) added a required `userChatRelay: boolean` field to the NewUser interface, but apiCreateActiveUser was never updated to pass it, causing a TypeScript compilation error. Set userChatRelay to false, which preserves the pre-existing behavior (no chat relay provisioned for the new user profile). --- packages/simplex-chat-nodejs/src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index c3e85b3915..f5d2a5168e 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -813,7 +813,7 @@ export class ChatApi { * Network usage: no. */ async apiCreateActiveUser(profile?: T.Profile): Promise { - const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false}})) + const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false}})) if (r.type === "activeUser") return r.user throw new ChatCommandError("unexpected response", r) } From 6583aafbdd4423b8085ba7302184d388c6ba8a83 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:25:57 +0000 Subject: [PATCH 2/5] core: bump @simplex-chat/types (#6765) --- packages/simplex-chat-client/types/typescript/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json index a135b286c2..8d05eb460c 100644 --- a/packages/simplex-chat-client/types/typescript/package.json +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/types", - "version": "0.3.0", + "version": "0.4.0", "description": "TypeScript types for SimpleX Chat bot libraries", "main": "dist/index.js", "types": "dist/index.d.ts", From 24435f5b74318b98cd13f048a86b9ab26e81222a Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:44:23 +0000 Subject: [PATCH 3/5] ui: fix edit channel profile texts (#6766) --- .../Views/Chat/Group/GroupProfileView.swift | 26 +++++++++++------- .../chat/simplex/common/model/SimpleXAPI.kt | 7 ++--- .../views/chat/group/GroupPreferences.kt | 2 +- .../views/chat/group/GroupProfileView.kt | 27 ++++++++++++------- .../views/chat/group/MemberAdmission.kt | 2 +- .../views/chat/group/WelcomeMessageView.kt | 2 +- .../commonMain/resources/MR/base/strings.xml | 5 ++++ 7 files changed, 46 insertions(+), 25 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 69587c0152..24a52b4b60 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -42,7 +42,7 @@ struct GroupProfileView: View { Section { HStack { - TextField("Group display name", text: $groupProfile.displayName) + TextField(groupInfo.useRelays ? "Channel display name" : "Group display name", text: $groupProfile.displayName) .focused($focusDisplayName) if !validNewProfileName { Button { @@ -54,7 +54,7 @@ struct GroupProfileView: View { } let fullName = groupInfo.groupProfile.fullName if fullName != "" && fullName != groupProfile.displayName { - TextField("Group full name (optional)", text: $groupProfile.fullName) + TextField(groupInfo.useRelays ? "Channel full name (optional)" : "Group full name (optional)", text: $groupProfile.fullName) } HStack { TextField("Short description", text: $shortDescr) @@ -67,7 +67,7 @@ struct GroupProfileView: View { } } } footer: { - Text("Group profile is stored on members' devices, not on the servers.") + Text(groupInfo.useRelays ? "Channel profile is stored on subscribers' devices and on the chat relays." : "Group profile is stored on members' devices, not on the servers.") } Section { @@ -80,11 +80,11 @@ struct GroupProfileView: View { currentProfileHash == groupProfile.hashValue && (groupInfo.groupProfile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces) ) - Button("Save group profile", action: saveProfile) + Button(groupInfo.useRelays ? "Save channel profile" : "Save group profile", action: saveProfile) .disabled(!canUpdateProfile) } } - .confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) { + .confirmationDialog(groupInfo.useRelays ? "Channel image" : "Group image", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true } @@ -130,9 +130,15 @@ struct GroupProfileView: View { .onDisappear { if canUpdateProfile { showAlert( - title: NSLocalizedString("Save group profile?", comment: "alert title"), - message: NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"), - buttonTitle: NSLocalizedString("Save (and notify members)", comment: "alert button"), + title: groupInfo.useRelays + ? NSLocalizedString("Save channel profile?", comment: "alert title") + : NSLocalizedString("Save group profile?", comment: "alert title"), + message: groupInfo.useRelays + ? NSLocalizedString("Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers.", comment: "alert message") + : NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"), + buttonTitle: groupInfo.useRelays + ? NSLocalizedString("Save (and notify subscribers)", comment: "alert button") + : NSLocalizedString("Save (and notify members)", comment: "alert button"), buttonAction: saveProfile, cancelButton: true ) @@ -142,14 +148,14 @@ struct GroupProfileView: View { switch a { case let .saveError(err): return Alert( - title: Text("Error saving group profile"), + title: Text(groupInfo.useRelays ? "Error saving channel profile" : "Error saving group profile"), message: Text(err) ) case let .invalidName(name): return createInvalidNameAlert(name, $groupProfile.displayName) } } - .navigationBarTitle("Group profile") + .navigationBarTitle(groupInfo.useRelays ? "Channel profile" : "Group profile") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(focusDisplayName ? .inline : .large) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index cb42ee2aba..661b7e767f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2217,18 +2217,19 @@ object ChatController { return emptyList() } - suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile): GroupInfo? { + suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile, isChannel: Boolean): GroupInfo? { val r = sendCmd(rh, CC.ApiUpdateGroupProfile(groupId, groupProfile)) + val errorTitle = if (isChannel) MR.strings.error_saving_channel_profile else MR.strings.error_saving_group_profile return when { r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup r is API.Error -> { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_group_profile), "$r.err") + AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "$r.err") null } else -> { Log.e(TAG, "apiUpdateGroup bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.error_saving_group_profile), + generalGetString(errorTitle), "${r.responseType}: ${r.details}" ) null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index b8db5969a1..ddf0456822 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -43,7 +43,7 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> fun savePrefs(afterSave: () -> Unit = {}) { withBGApi { val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) - val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) + val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp, gInfo.useRelays) if (g != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, g) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index f15f70673a..d144065399 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -32,10 +32,11 @@ import java.net.URI fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) { GroupProfileLayout( close = close, + groupInfo = groupInfo, groupProfile = groupInfo.groupProfile, saveProfile = { p -> withBGApi { - val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) + val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p, groupInfo.useRelays) if (gInfo != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, gInfo) @@ -50,9 +51,11 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl @Composable fun GroupProfileLayout( close: () -> Unit, + groupInfo: GroupInfo, groupProfile: GroupProfile, saveProfile: (GroupProfile) -> Unit, ) { + val isChannel = groupInfo.useRelays val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) } val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) } @@ -71,7 +74,7 @@ fun GroupProfileLayout( if (dataUnchanged || !canUpdateProfile(displayName.value, shortDescr.value, groupProfile)) { close() } else { - showUnsavedChangesAlert({ + showUnsavedChangesAlert(isChannel, { saveProfile( groupProfile.copy( displayName = displayName.value.trim(), @@ -103,7 +106,11 @@ fun GroupProfileLayout( Modifier.fillMaxWidth() .padding(horizontal = DEFAULT_PADDING) ) { - ReadableText(MR.strings.group_profile_is_stored_on_members_devices, TextAlign.Center) + ReadableText( + if (isChannel) MR.strings.channel_profile_is_stored_on_subscribers_devices + else MR.strings.group_profile_is_stored_on_members_devices, + TextAlign.Center + ) Box( Modifier .fillMaxWidth() @@ -122,7 +129,7 @@ fun GroupProfileLayout( } Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( - stringResource(MR.strings.group_display_name_field), + stringResource(if (isChannel) MR.strings.channel_display_name_field else MR.strings.group_display_name_field), fontSize = 16.sp ) if (!isValidNewProfileName(displayName.value, groupProfile)) { @@ -136,7 +143,7 @@ fun GroupProfileLayout( if (groupProfile.fullName.trim().isNotEmpty() && groupProfile.fullName.trim() != groupProfile.displayName.trim()) { Spacer(Modifier.height(DEFAULT_PADDING)) Text( - stringResource(MR.strings.group_full_name_field), + stringResource(if (isChannel) MR.strings.channel_full_name_field else MR.strings.group_full_name_field), fontSize = 16.sp, modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) ) @@ -164,9 +171,10 @@ fun GroupProfileLayout( Spacer(Modifier.height(DEFAULT_PADDING)) val enabled = !dataUnchanged && canUpdateProfile(displayName.value, shortDescr.value, groupProfile) + val saveProfileLabel = if (isChannel) MR.strings.save_channel_profile else MR.strings.save_group_profile if (enabled) { Text( - stringResource(MR.strings.save_group_profile), + stringResource(saveProfileLabel), modifier = Modifier.clickable { saveProfile( groupProfile.copy( @@ -181,7 +189,7 @@ fun GroupProfileLayout( ) } else { Text( - stringResource(MR.strings.save_group_profile), + stringResource(saveProfileLabel), color = MaterialTheme.colors.secondary ) } @@ -204,10 +212,10 @@ private fun canUpdateProfile(displayName: String, shortDescr: String, groupProfi private fun isValidNewProfileName(displayName: String, groupProfile: GroupProfile): Boolean = displayName == groupProfile.displayName || isValidDisplayName(displayName.trim()) -private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { +private fun showUnsavedChangesAlert(isChannel: Boolean, save: () -> Unit, revert: () -> Unit) { AlertManager.shared.showAlertDialogStacked( title = generalGetString(MR.strings.save_preferences_question), - confirmText = generalGetString(MR.strings.save_and_notify_group_members), + confirmText = generalGetString(if (isChannel) MR.strings.save_and_notify_channel_subscribers else MR.strings.save_and_notify_group_members), dismissText = generalGetString(MR.strings.exit_without_saving), onConfirm = save, onDismiss = revert, @@ -224,6 +232,7 @@ fun PreviewGroupProfileLayout() { SimpleXTheme { GroupProfileLayout( close = {}, + groupInfo = GroupInfo.sampleData, groupProfile = GroupProfile.sampleData, saveProfile = { _ -> } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt index 48171bfeb7..7c9db58316 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt @@ -34,7 +34,7 @@ fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () -> fun saveAdmission(afterSave: () -> Unit = {}) { withBGApi { val gp = gInfo.groupProfile.copy(memberAdmission = admission) - val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) + val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp, gInfo.useRelays) if (g != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, g) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 1e99c7f527..927e9940b5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -45,7 +45,7 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () welcome = null } val groupProfileUpdated = gInfo.groupProfile.copy(description = welcome) - val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) + val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated, gInfo.useRelays) if (res != null) { gInfo = res withContext(Dispatchers.Main) { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index ac9f9b2fc8..f2872f3f0a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1171,6 +1171,7 @@ Save and notify contact Save and notify contacts Save and notify group members + Save and notify channel subscribers Exit without saving @@ -1999,6 +2000,7 @@ Fully decentralized – visible only to members. Enter group name: Group full name: + Channel full name: Short description: Description too large Your chat profile will be sent to group members @@ -2007,8 +2009,11 @@ Group profile is stored on members\' devices, not on the servers. + Channel profile is stored on subscribers\' devices and on the chat relays. Save group profile + Save channel profile Error saving group profile + Error saving channel profile Preset servers From e6dde90c40da36c0509220bfa7302969dbf2916b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 10 Apr 2026 09:31:26 +0100 Subject: [PATCH 4/5] core: 6.5.0.14 --- simplex-chat.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index c4317c85c7..621a784ed8 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.0.12 +version: 6.5.0.14 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From e2ecff7215971feffdc262aac826bb8fa21068f2 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:37:37 +0000 Subject: [PATCH 5/5] simplex-chat-nodejs: add member contact API methods (#6763) * simplex-chat-nodejs: add apiCreateMemberContact and apiSendMemberContactInvitation * simplex-chat-nodejs: add integration test for apiCreateMemberContact and apiSendMemberContactInvitation Test creates a 3-user group with direct messages enabled, then verifies: - apiCreateMemberContact creates a DM contact between group members - apiSendMemberContactInvitation sends an invitation that the recipient receives * simplex-chat-nodejs: bump @simplex-chat/types to ^0.4.0 --- packages/simplex-chat-nodejs/package.json | 2 +- packages/simplex-chat-nodejs/src/api.ts | 30 +++++++ .../simplex-chat-nodejs/tests/api.test.ts | 85 +++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 498d502edd..657c6e05f2 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -24,7 +24,7 @@ "docs": "typedoc" }, "dependencies": { - "@simplex-chat/types": "^0.3.0", + "@simplex-chat/types": "^0.4.0", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.3", "node-addon-api": "^8.5.0" diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index f5d2a5168e..8bc56db41c 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -872,4 +872,34 @@ export class ChatApi { const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences})) if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r) } + + /** + * Create a direct message contact with a group member. + * Returns the created contact. + * Network usage: interactive. + */ + async apiCreateMemberContact(groupId: number, groupMemberId: number): Promise { + const r: any = await this.sendChatCmd(`/_create member contact #${groupId} ${groupMemberId}`) + if (r.type === "newMemberContact") return r.contact + throw new ChatCommandError("error creating member contact", r) + } + + /** + * Send a direct message invitation to a group member contact. + * The contact must have been created with {@link apiCreateMemberContact}. + * Network usage: interactive. + */ + async apiSendMemberContactInvitation(contactId: number, message?: T.MsgContent | string): Promise { + let cmd = `/_invite member contact @${contactId}` + if (message !== undefined) { + if (typeof message === "string") { + cmd += ` text ${message}` + } else { + cmd += ` json ${JSON.stringify(message)}` + } + } + const r: any = await this.sendChatCmd(cmd) + if (r.type === "newMemberContactSentInv") return r.contact + throw new ChatCommandError("error sending member contact invitation", r) + } } diff --git a/packages/simplex-chat-nodejs/tests/api.test.ts b/packages/simplex-chat-nodejs/tests/api.test.ts index 52153ecfed..7bc1a89b86 100644 --- a/packages/simplex-chat-nodejs/tests/api.test.ts +++ b/packages/simplex-chat-nodejs/tests/api.test.ts @@ -64,4 +64,89 @@ describe("API tests (use preset servers)", () => { expect(servers[0] !== servers[1]).toBe(true) expect(eventCount > 0).toBe(true) }, 30000) + + it("should create member contact and send invitation", async () => { + // create 3 users and start chat controllers + const alice = await api.ChatApi.init(alicePath) + const bob = await api.ChatApi.init(bobPath) + const carolPath = path.join(tmpDir, "carol") + const carol = await api.ChatApi.init(carolPath) + const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) + await bob.apiCreateActiveUser({displayName: "bob", fullName: ""}) + await carol.apiCreateActiveUser({displayName: "carol", fullName: ""}) + await alice.startChat() + await bob.startChat() + await carol.startChat() + // connect alice <-> bob + const aliceLink1 = await alice.apiCreateLink(aliceUser.userId) + await expect(bob.apiConnectActiveUser(aliceLink1)).resolves.toBe(api.ConnReqType.Invitation) + const [bobContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await bob.wait("contactConnected")).contact + ]) + // connect alice <-> carol + const aliceLink2 = await alice.apiCreateLink(aliceUser.userId) + await expect(carol.apiConnectActiveUser(aliceLink2)).resolves.toBe(api.ConnReqType.Invitation) + const [carolContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await carol.wait("contactConnected")).contact + ]) + // create group with direct messages enabled + const group = await alice.apiNewGroup(aliceUser.userId, { + displayName: "test-group", + fullName: "", + groupPreferences: { + directMessages: {enable: T.GroupFeatureEnabled.On}, + }, + }) + const groupId = group.groupId + // add bob to the group + const bobInvP = bob.wait("receivedGroupInvitation", 15000) + await alice.apiAddMember(groupId, bobContact.contactId, T.GroupMemberRole.Member) + const bobInvEvt = await bobInvP + expect(bobInvEvt).toBeDefined() + const aliceBobConnP = alice.wait("connectedToGroupMember", 15000) + const bobAliceConnP = bob.wait("connectedToGroupMember", 15000) + await bob.apiJoinGroup(bobInvEvt!.groupInfo.groupId) + await Promise.all([aliceBobConnP, bobAliceConnP]) + // add carol to the group + const carolInvP = carol.wait("receivedGroupInvitation", 30000) + await alice.apiAddMember(groupId, carolContact.contactId, T.GroupMemberRole.Member) + const carolInvEvt = await carolInvP + expect(carolInvEvt).toBeDefined() + // wait for carol to connect to both alice and bob (and vice versa) + const bobCarolConnP = bob.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000) + const carolAliceConnP = carol.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "alice", 30000) + const carolBobConnP = carol.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "bob", 30000) + const aliceCarolConnP = alice.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000) + await carol.apiJoinGroup(carolInvEvt!.groupInfo.groupId) + await Promise.all([bobCarolConnP, carolAliceConnP, carolBobConnP, aliceCarolConnP]) + // find carol's memberId from bob's perspective + const members = await bob.apiListMembers(groupId) + const carolMember = members.find(m => m.memberProfile.displayName === "carol") + expect(carolMember).toBeDefined() + // test apiCreateMemberContact + const dmContact = await bob.apiCreateMemberContact(groupId, carolMember!.groupMemberId) + expect(dmContact).toBeDefined() + expect(dmContact.contactId).toBeDefined() + // test apiSendMemberContactInvitation + const carolDmP = carol.wait("newMemberContactReceivedInv" as CEvt.Tag, 30000) + const invContact = await bob.apiSendMemberContactInvitation(dmContact.contactId, "hello from bob") + expect(invContact).toBeDefined() + // carol should receive the member contact invitation + const carolDmEvt = await carolDmP + expect(carolDmEvt).toBeDefined() + expect((carolDmEvt as any).contact).toBeDefined() + // cleanup + await alice.stopChat() + await bob.stopChat() + await carol.stopChat() + await alice.close() + await bob.close() + await carol.close() + }, 90000) })