diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 40b88ec338..db70511a5a 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -21,6 +21,7 @@ enum ChatCommand: ChatCmdProtocol { case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) case apiSetUserAutoAcceptMemberContacts(userId: Int64, enable: Bool) + case apiSetUserAutoAcceptGroupInvitations(userId: Int64, enable: Bool) case apiHideUser(userId: Int64, viewPwd: String) case apiUnhideUser(userId: Int64, viewPwd: String) case apiMuteUser(userId: Int64) @@ -215,6 +216,8 @@ enum ChatCommand: ChatCmdProtocol { return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" case let .apiSetUserAutoAcceptMemberContacts(userId, enable): return "/_set accept member contacts \(userId) \(onOff(enable))" + case let .apiSetUserAutoAcceptGroupInvitations(userId, enable): + return "/_set accept group invitations \(userId) \(onOff(enable))" case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))" case let .apiMuteUser(userId): return "/_mute user \(userId)" @@ -430,6 +433,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiSetUserContactReceipts: return "apiSetUserContactReceipts" case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts" case .apiSetUserAutoAcceptMemberContacts: return "apiSetUserAutoAcceptMemberContacts" + case .apiSetUserAutoAcceptGroupInvitations: return "apiSetUserAutoAcceptGroupInvitations" case .apiHideUser: return "apiHideUser" case .apiUnhideUser: return "apiUnhideUser" case .apiMuteUser: return "apiMuteUser" diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 7a934fc746..74f97134d7 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -302,6 +302,10 @@ func apiSetUserAutoAcceptMemberContacts(_ userId: Int64, enable: Bool) async thr try await sendCommandOkResp(.apiSetUserAutoAcceptMemberContacts(userId: userId, enable: enable)) } +func apiSetUserAutoAcceptGroupInvitations(_ userId: Int64, enable: Bool) async throws { + try await sendCommandOkResp(.apiSetUserAutoAcceptGroupInvitations(userId: userId, enable: enable)) +} + func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User { try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd)) } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index d891efcd90..bf3dbac440 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -37,6 +37,8 @@ struct PrivacySettings: View { @State private var groupReceiptsDialogue = false @State private var autoAcceptMemberContacts = false @State private var autoAcceptMemberContactsReset = false + @State private var autoAcceptGroupInvitations = false + @State private var autoAcceptGroupInvitationsReset = false @State private var alert: PrivacySettingsViewAlert? enum PrivacySettingsViewAlert: Identifiable { @@ -117,14 +119,17 @@ struct PrivacySettings: View { } Section { - settingsRow("checkmark", color: theme.colors.secondary) { - Toggle("Auto-accept", isOn: $autoAcceptMemberContacts) + settingsRow("person", color: theme.colors.secondary) { + Toggle("Contact requests in groups", isOn: $autoAcceptMemberContacts) + } + settingsRow("person.2", color: theme.colors.secondary) { + Toggle("Group invitations", isOn: $autoAcceptGroupInvitations) } } header: { - Text("Contact requests from groups") + Text("Auto-accept") .foregroundColor(theme.colors.secondary) } footer: { - Text("This setting is for your current profile **\(m.currentUser?.displayName ?? "")**.") + Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.") .foregroundColor(theme.colors.secondary) } @@ -139,7 +144,14 @@ struct PrivacySettings: View { if autoAcceptMemberContactsReset { autoAcceptMemberContactsReset = false } else { - setAutoAcceptGrpDirectInvs(autoAcceptMemberContacts) + setAutoAcceptMemberContacts(autoAcceptMemberContacts) + } + } + .onChange(of: autoAcceptGroupInvitations) { _ in + if autoAcceptGroupInvitationsReset { + autoAcceptGroupInvitationsReset = false + } else { + setAutoAcceptGroupInvitations(autoAcceptGroupInvitations) } } .onAppear { @@ -148,6 +160,10 @@ struct PrivacySettings: View { autoAcceptMemberContactsReset = true autoAcceptMemberContacts = u.autoAcceptMemberContacts } + if autoAcceptGroupInvitations != u.autoAcceptGroupInvitations { + autoAcceptGroupInvitationsReset = true + autoAcceptGroupInvitations = u.autoAcceptGroupInvitations + } } } .alert(item: $alert) { alert in @@ -426,7 +442,7 @@ struct PrivacySettings: View { } } - private func setAutoAcceptGrpDirectInvs(_ enable: Bool) { + private func setAutoAcceptMemberContacts(_ enable: Bool) { Task { do { if let currentUser = m.currentUser { @@ -443,6 +459,23 @@ struct PrivacySettings: View { } } + private func setAutoAcceptGroupInvitations(_ enable: Bool) { + Task { + do { + if let currentUser = m.currentUser { + try await apiSetUserAutoAcceptGroupInvitations(currentUser.userId, enable: enable) + await MainActor.run { + var updatedUser = currentUser + updatedUser.autoAcceptGroupInvitations = enable + m.updateUser(updatedUser) + } + } + } catch let error { + alert = .error(title: "Error setting auto-accept", error: "Error: \(responseError(error))") + } + } + } + private func simplexLockRow(_ value: LocalizedStringKey) -> some View { HStack { Text("SimpleX Lock") diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index ddc9454b86..7f9f1a4fcc 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -42,6 +42,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { public var sendRcptsContacts: Bool public var sendRcptsSmallGroups: Bool public var autoAcceptMemberContacts: Bool + public var autoAcceptGroupInvitations: Bool public var viewPwdHash: UserPwdHash? public var uiThemes: ThemeModeOverrides? public var userChatRelay: Bool @@ -71,6 +72,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { sendRcptsContacts: true, sendRcptsSmallGroups: false, autoAcceptMemberContacts: false, + autoAcceptGroupInvitations: false, userChatRelay: false ) } diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 41f64e5400..91ee6f3388 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1692,7 +1692,7 @@ server test step */ "Contact preferences" = "Kontakt-Präferenzen"; /* No comment provided by engineer. */ -"Contact requests from groups" = "KONTAKTANFRAGEN VON GRUPPEN"; +"Contact requests in groups" = "KONTAKTANFRAGEN VON GRUPPEN"; /* No comment provided by engineer. */ "contact should accept…" = "Kontakt sollte annehmen…"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index a6d4e9d20c..46e86419b2 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1692,7 +1692,7 @@ server test step */ "Contact preferences" = "Preferencias de contacto"; /* No comment provided by engineer. */ -"Contact requests from groups" = "Solicitudes de contacto en grupo"; +"Contact requests in groups" = "Solicitudes de contacto en grupo"; /* No comment provided by engineer. */ "contact should accept…" = "el contacto debe aceptarte…"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 0952cacb90..5bfc7dca1d 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1612,7 +1612,7 @@ server test step */ "Contact preferences" = "Préférences de contact"; /* No comment provided by engineer. */ -"Contact requests from groups" = "Demandes de contact des groupes"; +"Contact requests in groups" = "Demandes de contact des groupes"; /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "Le contact sera supprimé - il n'est pas possible de revenir en arrière !"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 330688163e..ea687106b4 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -1692,7 +1692,7 @@ server test step */ "Contact preferences" = "Partnerbeállítások"; /* No comment provided by engineer. */ -"Contact requests from groups" = "Partneri kapcsolatkérések a csoportokból"; +"Contact requests in groups" = "Partneri kapcsolatkérések a csoportokból"; /* No comment provided by engineer. */ "contact should accept…" = "a partnernek el kell fogadnia…"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 3544dc1813..5871fb7615 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1692,7 +1692,7 @@ server test step */ "Contact preferences" = "Preferenze del contatto"; /* No comment provided by engineer. */ -"Contact requests from groups" = "Richieste di contatto dai gruppi"; +"Contact requests in groups" = "Richieste di contatto dai gruppi"; /* No comment provided by engineer. */ "contact should accept…" = "il contatto deve accettare…"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 07c407416a..c7aa746e53 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1416,7 +1416,7 @@ server test step */ "Contact preferences" = "Preferencje kontaktu"; /* No comment provided by engineer. */ -"Contact requests from groups" = "Prośby o kontakt od grup"; +"Contact requests in groups" = "Prośby o kontakt od grup"; /* No comment provided by engineer. */ "contact should accept…" = "kontakt powinien zaakceptować…"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index ce7e446211..79c28bdfb6 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1692,7 +1692,7 @@ server test step */ "Contact preferences" = "Предпочтения контакта"; /* No comment provided by engineer. */ -"Contact requests from groups" = "Запросы на соединение из групп"; +"Contact requests in groups" = "Запросы на соединение из групп"; /* No comment provided by engineer. */ "contact should accept…" = "контакт должен принять…"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index b0dd32c383..d2e87a9fb5 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1454,7 +1454,7 @@ server test step */ "Contact preferences" = "Kişi tercihleri"; /* No comment provided by engineer. */ -"Contact requests from groups" = "Gruplardan gelen iletişim talepleri"; +"Contact requests in groups" = "Gruplardan gelen iletişim talepleri"; /* No comment provided by engineer. */ "contact should accept…" = "kişi kabul etmeli…"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index e5d35b7426..e8a06c7d00 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1680,7 +1680,7 @@ server test step */ "Contact preferences" = "联系人偏好设置"; /* No comment provided by engineer. */ -"Contact requests from groups" = "来自群的联络请求"; +"Contact requests in groups" = "来自群的联络请求"; /* No comment provided by engineer. */ "contact should accept…" = "联系人应当接受…"; diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 340ba2c1ad..11da8b874e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1293,6 +1293,7 @@ data class User( val sendRcptsContacts: Boolean, val sendRcptsSmallGroups: Boolean, val autoAcceptMemberContacts: Boolean, + val autoAcceptGroupInvitations: Boolean, val viewPwdHash: UserPwdHash?, val uiThemes: ThemeModeOverrides? = null, val userChatRelay: Boolean, @@ -1325,6 +1326,7 @@ data class User( sendRcptsContacts = true, sendRcptsSmallGroups = false, autoAcceptMemberContacts = false, + autoAcceptGroupInvitations = false, viewPwdHash = null, uiThemes = null, userChatRelay = false, 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 037c02b1c2..c351ee3281 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 @@ -941,6 +941,12 @@ object ChatController { throw Exception("failed to set auto-accept ${r.responseType} ${r.details}") } + suspend fun apiSetUserAutoAcceptGroupInvitations(u: User, enable: Boolean) { + val r = sendCmd(u.remoteHostId, CC.ApiSetUserAutoAcceptGroupInvitations(u.userId, enable)) + if (r.result is CR.CmdOk) return + throw Exception("failed to set auto-accept group invitations ${r.responseType} ${r.details}") + } + suspend fun apiHideUser(u: User, viewPwd: String): User = setUserPrivacy(u.remoteHostId, CC.ApiHideUser(u.userId, viewPwd)) @@ -3775,6 +3781,7 @@ sealed class CC { class ApiSetUserContactReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC() class ApiSetUserGroupReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC() class ApiSetUserAutoAcceptMemberContacts(val userId: Long, val enable: Boolean): CC() + class ApiSetUserAutoAcceptGroupInvitations(val userId: Long, val enable: Boolean): CC() class ApiHideUser(val userId: Long, val viewPwd: String): CC() class ApiUnhideUser(val userId: Long, val viewPwd: String): CC() class ApiMuteUser(val userId: Long): CC() @@ -3965,6 +3972,7 @@ sealed class CC { "/_set receipts groups $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}" } is ApiSetUserAutoAcceptMemberContacts -> "/_set accept member contacts $userId ${onOff(enable)}" + is ApiSetUserAutoAcceptGroupInvitations -> "/_set accept group invitations $userId ${onOff(enable)}" is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}" is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}" is ApiMuteUser -> "/_mute user $userId" @@ -4176,6 +4184,7 @@ sealed class CC { is ApiSetUserContactReceipts -> "apiSetUserContactReceipts" is ApiSetUserGroupReceipts -> "apiSetUserGroupReceipts" is ApiSetUserAutoAcceptMemberContacts -> "apiSetUserAutoAcceptMemberContacts" + is ApiSetUserAutoAcceptGroupInvitations -> "apiSetUserAutoAcceptGroupInvitations" is ApiHideUser -> "apiHideUser" is ApiUnhideUser -> "apiUnhideUser" is ApiMuteUser -> "apiMuteUser" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 59136e90d2..99aacf65ba 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -87,13 +87,19 @@ fun PrivacySettingsView( val currentUser = chatModel.currentUser.value if (currentUser != null && !chatModel.desktopNoUserNoRemote) { SectionDividerSpaced() - ContacRequestsFromGroupsSection( + AutoAcceptSection( currentUser = currentUser, - setAutoAcceptGrpDirectInvs = { enable -> + setAutoAcceptMemberContacts = { enable -> withApi { chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable) chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable) } + }, + setAutoAcceptGroupInvitations = { enable -> + withApi { + chatModel.controller.apiSetUserAutoAcceptGroupInvitations(currentUser, enable) + chatModel.currentUser.value = currentUser.copy(autoAcceptGroupInvitations = enable) + } } ) } @@ -333,16 +339,26 @@ expect fun PrivacyDeviceSection( ) @Composable -private fun ContacRequestsFromGroupsSection( +private fun AutoAcceptSection( currentUser: User, - setAutoAcceptGrpDirectInvs: (Boolean) -> Unit + setAutoAcceptMemberContacts: (Boolean) -> Unit, + setAutoAcceptGroupInvitations: (Boolean) -> Unit ) { - SectionView(stringResource(MR.strings.settings_section_title_contact_requests_from_groups)) { - SettingsActionItemWithContent(painterResource(MR.images.ic_check), stringResource(MR.strings.auto_accept_contact)) { + // legacy string key names, reused for their values so this section stays translated + SectionView(stringResource(MR.strings.auto_accept_contact)) { + SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.settings_section_title_contact_requests_from_groups)) { DefaultSwitch( checked = currentUser.autoAcceptMemberContacts, onCheckedChange = { enable -> - setAutoAcceptGrpDirectInvs(enable) + setAutoAcceptMemberContacts(enable) + } + ) + } + SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.group_invitations)) { + DefaultSwitch( + checked = currentUser.autoAcceptGroupInvitations, + onCheckedChange = { enable -> + setAutoAcceptGroupInvitations(enable) } ) } @@ -350,7 +366,7 @@ private fun ContacRequestsFromGroupsSection( SectionTextFooter( remember(currentUser.displayName) { buildAnnotatedString { - append(generalGetString(MR.strings.this_setting_is_for_your_current_profile) + " ") + append(generalGetString(MR.strings.receipts_section_description) + " ") withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(currentUser.displayName) } 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 ead51b31ea..09564a2348 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1206,6 +1206,7 @@ Stop sharing address? Stop sharing Auto-accept + Group invitations Sent to your contact after connection. Welcome message Enter welcome message… (optional) @@ -1602,7 +1603,7 @@ Chats Files Send delivery receipts to - Contact requests from groups + Contact requests in groups About Contact Support the project diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index 476ee4f94d..d88181b778 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -61,6 +61,7 @@ This file is generated automatically. - [APISetGroupCustomData](#apisetgroupcustomdata) - [APISetContactCustomData](#apisetcontactcustomdata) - [APISetUserAutoAcceptMemberContacts](#apisetuserautoacceptmembercontacts) +- [APISetUserAutoAcceptGroupInvitations](#apisetuserautoacceptgroupinvitations) [User profile commands](#user-profile-commands) - [ShowActiveUser](#showactiveuser) @@ -1972,6 +1973,43 @@ ChatCmdError: Command error (only used in WebSockets API). --- +### APISetUserAutoAcceptGroupInvitations + +Set auto-accept group invitations. + +*Network usage*: no. + +**Parameters**: +- userId: int64 +- onOff: bool + +**Syntax**: + +``` +/_set accept group invitations on|off +``` + +```javascript +'/_set accept group invitations ' + userId + ' ' + (onOff ? 'on' : 'off') // JavaScript +``` + +```python +'/_set accept group invitations ' + str(userId) + ' ' + ('on' if onOff else 'off') # Python +``` + +**Responses**: + +CmdOk: Ok. +- type: "cmdOk" +- user_: [User](./TYPES.md#user)? + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + ## User profile commands Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents). diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 4546a6aaa7..9d4f1e16f8 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -4366,6 +4366,7 @@ Handshake: - sendRcptsContacts: bool - sendRcptsSmallGroups: bool - autoAcceptMemberContacts: bool +- autoAcceptGroupInvitations: bool - userMemberProfileUpdatedAt: UTCTime? - userChatRelay: bool - clientService: bool diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 09ae1faa5b..d126ff1844 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -154,7 +154,8 @@ chatCommandsDocsData = ("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser", "CRChatCmdError"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode"), ("APISetGroupCustomData", [], "Set group custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom #" <> Param "groupId" <> Optional "" (" " <> Json "$0") "customData"), ("APISetContactCustomData", [], "Set contact custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom @" <> Param "contactId" <> Optional "" (" " <> Json "$0") "customData"), - ("APISetUserAutoAcceptMemberContacts", [], "Set auto-accept member contacts.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set accept member contacts " <> Param "userId" <> " " <> OnOff "onOff") + ("APISetUserAutoAcceptMemberContacts", [], "Set auto-accept member contacts.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set accept member contacts " <> Param "userId" <> " " <> OnOff "onOff"), + ("APISetUserAutoAcceptGroupInvitations", [], "Set auto-accept group invitations.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set accept group invitations " <> Param "userId" <> " " <> OnOff "onOff") -- ("APIChatItemsRead", [], "Mark items as read.", ["CRItemsReadForChat"], [], Nothing, ""), -- ("APIChatRead", [], "Mark chat as read.", ["CRCmdOk"], [], Nothing, ""), -- ("APIChatUnread", [], "Mark chat as unread.", ["CRCmdOk"], [], Nothing, ""), @@ -297,6 +298,7 @@ cliCommands = "SetUserFeature", "SetUserGroupReceipts", "SetUserAutoAcceptMemberContacts", + "SetUserAutoAcceptGroupInvitations", "SetUserTimedMessages", "ShareMyAddress", "SharePublicGroup", diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index 14b03f560f..0dfd63ba88 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -728,6 +728,21 @@ export namespace APISetUserAutoAcceptMemberContacts { } } +// Set auto-accept group invitations. +// Network usage: no. +export interface APISetUserAutoAcceptGroupInvitations { + userId: number // int64 + onOff: boolean +} + +export namespace APISetUserAutoAcceptGroupInvitations { + export type Response = CR.CmdOk | CR.ChatCmdError + + export function cmdString(self: APISetUserAutoAcceptGroupInvitations): string { + return '/_set accept group invitations ' + self.userId + ' ' + (self.onOff ? 'on' : 'off') + } +} + // User profile commands // Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents). diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 5faf084dce..b9e61e496b 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -5042,6 +5042,7 @@ export interface User { sendRcptsContacts: boolean sendRcptsSmallGroups: boolean autoAcceptMemberContacts: boolean + autoAcceptGroupInvitations: boolean userMemberProfileUpdatedAt?: string // ISO-8601 timestamp userChatRelay: boolean clientService: boolean diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py index f73a4fa4f7..086b3cfd29 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py @@ -637,6 +637,19 @@ def APISetUserAutoAcceptMemberContacts_cmd_string(self: APISetUserAutoAcceptMemb APISetUserAutoAcceptMemberContacts_Response = CR.CmdOk | CR.ChatCmdError +# Set auto-accept group invitations. +# Network usage: no. +class APISetUserAutoAcceptGroupInvitations(TypedDict): + userId: int # int64 + onOff: bool + + +def APISetUserAutoAcceptGroupInvitations_cmd_string(self: APISetUserAutoAcceptGroupInvitations) -> str: + return '/_set accept group invitations ' + str(self['userId']) + ' ' + ('on' if self['onOff'] else 'off') + +APISetUserAutoAcceptGroupInvitations_Response = CR.CmdOk | CR.ChatCmdError + + # User profile commands # Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents). diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index b00a559f92..d37a08f674 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -3526,6 +3526,7 @@ class User(TypedDict): sendRcptsContacts: bool sendRcptsSmallGroups: bool autoAcceptMemberContacts: bool + autoAcceptGroupInvitations: bool userMemberProfileUpdatedAt: NotRequired[str] # ISO-8601 timestamp userChatRelay: bool clientService: bool diff --git a/plans/auto-accept-group-invitations.md b/plans/auto-accept-group-invitations.md new file mode 100644 index 0000000000..fdce3ebd2f --- /dev/null +++ b/plans/auto-accept-group-invitations.md @@ -0,0 +1,149 @@ +# Auto-accept Group Invitations — Plan + +## Table of Contents +1. [Context](#1-context) +2. [Why This Belongs in the Core](#2-why-this-belongs-in-the-core) +3. [Design](#3-design) +4. [Decisions and Justification](#4-decisions-and-justification) +5. [Scope](#5-scope) +6. [Verification](#6-verification) + +--- + +## 1. Context + +**Problem**: Every group invitation requires the user to tap accept, even when they have +already decided they want to join groups from their contacts. Users in active communities +accumulate invitations that are pure friction — the decision was made when they added the +contact, not when the invitation arrived. + +**Precedent**: Privacy & security already carries a per-profile "Contact requests from +groups / Auto-accept" toggle (`users.auto_accept_member_contacts`, added in +`M20250729_member_contact_requests`). This change adds the equivalent for group +invitations — per-profile flag, same command pair — and regroups both under a single +**Auto-accept** section, so the two rows name what is being accepted rather than repeating +"Auto-accept" as two adjacent section headers: + +``` +Auto-accept + Contact requests in groups [ ] + Group invitations [ ] + These settings are for your current profile . +``` + +--- + +## 2. Why This Belongs in the Core + +The naive implementation is client-side: observe `CEvtReceivedGroupInvitation`, then call +`APIJoinGroup`. That is wrong here for three reasons. + +1. **It does not work when the app is closed.** Invitations arrive through the notification + extension and background message processing. A client-side rule cannot run there. +2. **`APIJoinGroup` blocks.** It takes `withGroupLock`, calls the agent's `joinConnection` + synchronously, and rolls member status back to `GSMemInvited` via `catchAllErrors` on + failure. Driving that from an event handler couples message processing to a network + round trip. +3. **It would be reimplemented per client.** Android, desktop and iOS would each carry the + decision, and they would drift. + +The flag therefore lives on `users`, and the decision is made where the invitation is +received. + +--- + +## 3. Design + +`processGroupInvitation` already contained an async accept path, used when an invitation +matches a group link the user opened: + +``` +prepareAgentJoin -> createMemberConnectionAsync -> joinAgentConnectionAsync +``` + +The outcome is reported later against the `CFJoinConn` command id, so nothing blocks. This +change does not add a second mechanism — it turns the existing two-way branch into three, +and auto-accept takes the path that already exists: + +| Condition | Behaviour | +|---|---| +| invitation matches an opened group link | join async, no chat item (unchanged) | +| profile auto-accepts, membership still `GSMemInvited` | join async, record accepted item | +| otherwise | create pending invitation item, notify (unchanged) | + +The shared sequence is extracted as `joinGroupAsync`; the invitation item as +`createInvitationItem`, parameterised by `CIGroupInvitationStatus` rather than a boolean. + +--- + +## 4. Decisions and Justification + +**Per-profile, not global.** Matches the sibling toggle and the user's mental model: a +profile is an identity, and willingness to auto-join groups is a property of that identity. +An incognito or work profile should not inherit a personal profile's setting. + +**A chat item is still recorded.** The group-link branch creates no item because the user +initiated the join. Auto-accept is not user-initiated, so a `CIRcvGroupInvitation` with +status `CIGISAccepted` is written to the chat with the inviting contact — a durable record +of who added the user to what. It is deliberately left counting as unread +(`ciRequiresAttention`), so an auto-join is noticed rather than silent. + +**`hostContact` is reported only for group links.** Clients respond to `hostContact` on +`CEvtUserAcceptedGroupSent` by replacing the transient host connection view with the group +and removing that chat. That is right for a group link, where the contact is a placeholder +created to join. For a plain invitation the contact is a real one, and removing their chat +would be destructive — so auto-accept passes `Nothing`, matching `APIJoinGroup`. + +**The join only runs while membership is `GSMemInvited`.** `createGroupInvitation` is +idempotent on `inv_queue_info`: a resent invitation returns the existing group rather than +failing. Without the guard, every resend would open another agent connection, which a +hostile or buggy host could drive indefinitely. A resend after joining is ignored. + +**UI strings reuse legacy keys deliberately.** The section header, the contact-requests row +and the footer use `auto_accept_contact`, `settings_section_title_contact_requests_from_groups` +and `receipts_section_description` — key names that no longer describe where they are used. +This is intentional: those keys are already translated in 35, 20 and 28 of 41 locales +respectively, while any newly added key ships English everywhere until translators catch up, +which would have put an English header and footer around a translated row. Only +`group_invitations` is genuinely new, so the feature adds exactly one string. Renaming these +keys to match their new use would discard the existing translations. All 28 translations of +`receipts_section_description` were checked and none mention receipts, so the reuse is safe. + +**Security note.** This converts a user-gated action into an automatic one: a contact can +cause the client to open group connections and fetch history without a prompt. It is +opt-in and per-profile, and channels cannot be used for it — `processGroupInvitation` +rejects `publicGroup` invitations outright. No rate cap is applied; the setting is +explicit. + +**Known behaviour.** Clients drop events for non-active profiles (`active(user)` guard), so +a group auto-accepted on a background profile becomes visible when switching to that +profile. The join itself happens on arrival: `subscribeUsers` passes the agent the active +user's id rather than the user list, and the agent enumerates every user's servers from its +own store — the active id orders subscriptions, it does not filter them. + +--- + +## 5. Scope + +| Layer | Change | +|---|---| +| Schema | `users.auto_accept_group_invitations` (`M20260813`, SQLite + Postgres) | +| Core | `User` field; `APISetUserAutoAcceptGroupInvitations` / `SetUserAutoAcceptGroupInvitations`; third branch in `processGroupInvitation` | +| Bot API | documented command; regenerated TypeScript/Python bindings and markdown | +| Android/desktop | Privacy & security: two per-profile toggles regrouped under one **Auto-accept** section; the contact-requests row relabelled "Contact requests in groups" | +| iOS | same section in `PrivacySettings.swift` | + +--- + +## 6. Verification + +- Schema dump, `.lint`, strict tables, and **down-migration round-trip** pass; the down + migration restores the original DDL byte-for-byte, so no skip-list entry is needed. +- JSON fixtures and all Bot API doc/codegen specs pass with no regenerated drift. +- `testGroupCheckMessages` and `testGroupLink` pass — the manual and group-link branches + are behaviour-preserving through the refactor. +- Three new tests: auto-accept on the active profile, on a second profile, and on an + inactive profile while another is active. + +**Not verified**: iOS is not compiled (no toolchain available); the Postgres schema test is +gated behind `#if defined(dbPostgres)` and was not run. diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 404456b122..89016936ff 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -152,6 +152,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles + Simplex.Chat.Store.Postgres.Migrations.M20260813_auto_accept_group_invitations Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection else exposed-modules: @@ -321,6 +322,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history + Simplex.Chat.Store.SQLite.Migrations.M20260813_auto_accept_group_invitations Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection other-modules: diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 6cf5453fd8..be79fba9a9 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -339,6 +339,8 @@ data ChatCommand | SetUserGroupReceipts UserMsgReceiptSettings | APISetUserAutoAcceptMemberContacts {userId :: UserId, onOff :: Bool} | SetUserAutoAcceptMemberContacts Bool + | APISetUserAutoAcceptGroupInvitations {userId :: UserId, onOff :: Bool} + | SetUserAutoAcceptGroupInvitations Bool | APIHideUser UserId UserPwd | APIUnhideUser UserId UserPwd | APIMuteUser UserId diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 336474880f..c7f7677825 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -506,6 +506,12 @@ processChatCommand cxt nm = \case withFastStore' $ \db -> updateUserAutoAcceptMemberContacts db user' onOff ok user SetUserAutoAcceptMemberContacts onOff -> withUser $ \User {userId} -> processChatCommand cxt nm $ APISetUserAutoAcceptMemberContacts userId onOff + APISetUserAutoAcceptGroupInvitations userId' onOff -> withUser $ \user -> do + user' <- privateGetUser userId' + validateUserPassword user user' Nothing + withFastStore' $ \db -> updateUserAutoAcceptGroupInvitations db user' onOff + ok user + SetUserAutoAcceptGroupInvitations onOff -> withUser $ \User {userId} -> processChatCommand cxt nm $ APISetUserAutoAcceptGroupInvitations userId onOff APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do user' <- privateGetUser userId' case viewPwdHash user' of @@ -5434,6 +5440,8 @@ chatCommandP = "/set receipts groups " *> (SetUserGroupReceipts <$> receiptSettings), "/_set accept member contacts " *> (APISetUserAutoAcceptMemberContacts <$> A.decimal <* A.space <*> onOffP), "/set accept member contacts " *> (SetUserAutoAcceptMemberContacts <$> onOffP), + "/_set accept group invitations " *> (APISetUserAutoAcceptGroupInvitations <$> A.decimal <* A.space <*> onOffP), + "/set accept group invitations " *> (SetUserAutoAcceptGroupInvitations <$> onOffP), "/_hide user " *> (APIHideUser <$> A.decimal <* A.space <*> jsonP), "/_unhide user " *> (APIUnhideUser <$> A.decimal <* A.space <*> jsonP), "/_mute user " *> (APIMuteUser <$> A.decimal), diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 6e798f7ab2..7791ee7631 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -2618,24 +2618,35 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let GroupMember {groupMemberId, memberId = membershipMemId} = membership - if sameGroupLinkId groupLinkId groupLinkId' - then do - subMode <- chatReadVar subscriptionMode - dm <- encodeConnInfo $ XGrpAcpt membershipMemId - connIds@(cmdId, acId) <- prepareAgentJoin user Nothing True connRequest - withStore' $ \db -> do - setViaGroupLinkUri db groupId connId - createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode - updateGroupMemberStatusById db userId hostId GSMemAccepted - updateGroupMemberStatus db userId membership GSMemAccepted - joinAgentConnectionAsync cmdId False acId True connRequest dm subMode - toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) - else do - let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole - (ci, cInfo) <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content - withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] - toView $ CEvtReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} + -- hostContact is only reported for group links, where the client replaces + -- the transient host connection view with the group and removes its chat + joinGroupAsync hostContact_ = do + subMode <- chatReadVar subscriptionMode + dm <- encodeConnInfo $ XGrpAcpt membershipMemId + connIds@(cmdId, acId) <- prepareAgentJoin user Nothing True connRequest + withStore' $ \db -> do + createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode + updateGroupMemberStatusById db userId hostId GSMemAccepted + updateGroupMemberStatus db userId membership GSMemAccepted + joinAgentConnectionAsync cmdId False acId True connRequest dm subMode + toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} hostContact_ + createInvitationItem invStatus = do + let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = invStatus}) memRole + (ci, cInfo) <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content + withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] + if + | sameGroupLinkId groupLinkId groupLinkId' -> do + withStore' $ \db -> setViaGroupLinkUri db groupId connId + joinGroupAsync (Just ct) + | autoAcceptGroupInvitations user -> + -- a resent invitation returns the existing group, so only join while still invited + when (memberStatus membership == GSMemInvited) $ do + joinGroupAsync Nothing + createInvitationItem CIGISAccepted + | otherwise -> do + createInvitationItem CIGISPending + toView $ CEvtReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} where GroupInvitation {groupProfile = GroupProfile {publicGroup}} = inv brokerTs = metaBrokerTs msgMeta diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 19c07edbf8..1d743880fc 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -45,6 +45,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260714_member_security_code import Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description import Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history import Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles +import Simplex.Chat.Store.Postgres.Migrations.M20260813_auto_accept_group_invitations import Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection import Simplex.Messaging.Agent.Store.Shared (Migration (..)) @@ -91,7 +92,8 @@ schemaMigrations = ("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description), ("20260716_signed_history", m20260716_signed_history, Just down_m20260716_signed_history), ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles), - ("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection) + ("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection), + ("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260813_auto_accept_group_invitations.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260813_auto_accept_group_invitations.hs new file mode 100644 index 0000000000..fa1e621619 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260813_auto_accept_group_invitations.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260813_auto_accept_group_invitations where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260813_auto_accept_group_invitations :: Text +m20260813_auto_accept_group_invitations = + [r| +ALTER TABLE users ADD COLUMN auto_accept_group_invitations SMALLINT NOT NULL DEFAULT 0; +|] + +down_m20260813_auto_accept_group_invitations :: Text +down_m20260813_auto_accept_group_invitations = + [r| +ALTER TABLE users DROP COLUMN auto_accept_group_invitations; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index ae695d3c37..0978a54115 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -1510,7 +1510,8 @@ CREATE TABLE test_chat_schema.users ( active_order bigint DEFAULT 0 NOT NULL, auto_accept_member_contacts smallint DEFAULT 0 NOT NULL, is_user_chat_relay smallint DEFAULT 0 NOT NULL, - client_service smallint DEFAULT 0 NOT NULL + client_service smallint DEFAULT 0 NOT NULL, + auto_accept_group_invitations smallint DEFAULT 0 NOT NULL ); diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index ce6a8c4c9f..b59a85502d 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -42,6 +42,7 @@ module Simplex.Chat.Store.Profiles updateUserContactReceipts, updateUserGroupReceipts, updateUserAutoAcceptMemberContacts, + updateUserAutoAcceptGroupInvitations, updateUserProfile, setUserBadge, setUserProfileContactLink, @@ -140,12 +141,13 @@ createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {di sendRcptsContacts = True sendRcptsSmallGroups = True autoAcceptMemberContacts = False + autoAcceptGroupInvitations = False order <- getNextActiveOrder db DB.execute db - "INSERT INTO users (agent_user_id, local_display_name, active_user, is_user_chat_relay, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, client_service, created_at, updated_at) VALUES (?,?,?,?,?,0,?,?,?,?,?,?,?)" + "INSERT INTO users (agent_user_id, local_display_name, active_user, is_user_chat_relay, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, auto_accept_group_invitations, client_service, created_at, updated_at) VALUES (?,?,?,?,?,0,?,?,?,?,?,?,?,?)" ( (auId, displayName, BI activeUser, BI userChatRelay, order) - :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, BI clientService, currentTs, currentTs) + :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, BI autoAcceptGroupInvitations, BI clientService, currentTs, currentTs) ) userId <- insertedRowId db DB.execute @@ -163,7 +165,7 @@ createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {di (profileId, displayName, userId, BI True, currentTs, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, description, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing :. (Nothing, Nothing, Nothing) + pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, description, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, BI autoAcceptGroupInvitations, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing :. (Nothing, Nothing, Nothing) -- TODO [mentions] getUsersInfo :: DB.Connection -> IO [UserInfo] @@ -328,6 +330,10 @@ updateUserAutoAcceptMemberContacts :: DB.Connection -> User -> Bool -> IO () updateUserAutoAcceptMemberContacts db User {userId} autoAccept = DB.execute db "UPDATE users SET auto_accept_member_contacts = ? WHERE user_id = ?" (BI autoAccept, userId) +updateUserAutoAcceptGroupInvitations :: DB.Connection -> User -> Bool -> IO () +updateUserAutoAcceptGroupInvitations db User {userId} autoAccept = + DB.execute db "UPDATE users SET auto_accept_group_invitations = ? WHERE user_id = ?" (BI autoAccept, userId) + updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO User updateUserProfile db user p' | displayName == newName = liftIO $ do diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index a4e7ab04a2..63f969b1ad 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -168,6 +168,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code import Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description import Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history import Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles +import Simplex.Chat.Store.SQLite.Migrations.M20260813_auto_accept_group_invitations import Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection import Simplex.Messaging.Agent.Store.Shared (Migration (..)) @@ -337,7 +338,8 @@ schemaMigrations = ("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description), ("20260716_signed_history", m20260716_signed_history, Just down_m20260716_signed_history), ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles), - ("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection) + ("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection), + ("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260813_auto_accept_group_invitations.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260813_auto_accept_group_invitations.hs new file mode 100644 index 0000000000..f0fab81d68 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260813_auto_accept_group_invitations.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260813_auto_accept_group_invitations where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260813_auto_accept_group_invitations :: Query +m20260813_auto_accept_group_invitations = + [sql| +ALTER TABLE users ADD COLUMN auto_accept_group_invitations INTEGER NOT NULL DEFAULT 0; +|] + +down_m20260813_auto_accept_group_invitations :: Query +down_m20260813_auto_accept_group_invitations = + [sql| +ALTER TABLE users DROP COLUMN auto_accept_group_invitations; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 6884bf7e04..bc41a87969 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -6185,7 +6185,7 @@ SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6198,7 +6198,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6212,7 +6212,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6226,7 +6226,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6241,7 +6241,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6255,7 +6255,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6269,7 +6269,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6283,7 +6283,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6297,7 +6297,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id @@ -6310,7 +6310,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index cfbcf70599..a49a6d0db7 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -53,7 +53,8 @@ CREATE TABLE users( active_order INTEGER NOT NULL DEFAULT 0, auto_accept_member_contacts INTEGER NOT NULL DEFAULT 0, is_user_chat_relay INTEGER NOT NULL DEFAULT 0, - client_service INTEGER NOT NULL DEFAULT 0, -- 1 for active user + client_service INTEGER NOT NULL DEFAULT 0, + auto_accept_group_invitations INTEGER NOT NULL DEFAULT 0, -- 1 for active user FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE RESTRICT diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 6516b3cc66..ae98479ed1 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -557,16 +557,16 @@ userQuery :: Query userQuery = [sql| SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.auto_accept_group_invitations, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow :. ContactDomainRow -> User -toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, description, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow :. domainRow) = - User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, userChatRelay = BoolDef userChatRelay, clientService = BoolDef clientService, uiThemes} +toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow :. ContactDomainRow -> User +toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, description, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, BI autoAcceptGroupInvitations, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow :. domainRow) = + User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts, autoAcceptGroupInvitations, viewPwdHash, userMemberProfileUpdatedAt, userChatRelay = BoolDef userChatRelay, clientService = BoolDef clientService, uiThemes} where profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""} fullPreferences = fullPreferences' userPreferences diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 7bccd04b71..473630e690 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -143,6 +143,7 @@ data User = User sendRcptsContacts :: Bool, sendRcptsSmallGroups :: Bool, autoAcceptMemberContacts :: Bool, + autoAcceptGroupInvitations :: Bool, userMemberProfileUpdatedAt :: Maybe UTCTime, userChatRelay :: BoolDef, clientService :: BoolDef, diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index c65306474c..ce62b300e4 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -52,6 +52,9 @@ chatProfileTests = do it "reject profile image that is too large" testSetProfileImageTooLarge it "set profile image from file" testSetProfileImageFromFile it "use multiword profile names" testMultiWordProfileNames + it "auto-accept group invitations" testAutoAcceptGroupInvitations + it "auto-accept group invitations on a second profile" testAutoAcceptGroupInvitationsSecondProfile + it "auto-accept group invitations on an inactive profile" testAutoAcceptGroupInvitationsInactiveProfile it "present supporter badge to contacts" testUserBadgeBroadcast it "supporter badge sent to contact connecting after attach" testUserBadgeOnConnect it "supporter badge sent to member joining via group link" testUserBadgeGroupLink @@ -539,6 +542,61 @@ testSetProfileImageFromFile ps = testChat aliceProfile test ps alice ##> ("/set profile image file " <> emptyPath) alice <##. "bad chat command: image file is empty" +testAutoAcceptGroupInvitations :: HasCallStack => TestParams -> IO () +testAutoAcceptGroupInvitations = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + bob ##> "/set accept group invitations on" + bob <## "ok" + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/a team bob admin" + alice <## "invitation to join the group #team sent to bob" + concurrently_ + (alice <## "#team: bob joined the group") + (bob <## "#team: you joined the group") + +testAutoAcceptGroupInvitationsSecondProfile :: HasCallStack => TestParams -> IO () +testAutoAcceptGroupInvitationsSecondProfile = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + bob ##> "/create user bob2" + showActiveUser bob "bob2" + bob ##> "/set accept group invitations on" + bob <## "ok" + connectUsers alice bob + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/a team bob2 admin" + alice <## "invitation to join the group #team sent to bob2" + concurrently_ + (alice <## "#team: bob2 joined the group") + (bob <## "#team: you joined the group") + +testAutoAcceptGroupInvitationsInactiveProfile :: HasCallStack => TestParams -> IO () +testAutoAcceptGroupInvitationsInactiveProfile = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + bob ##> "/create user bob2" + showActiveUser bob "bob2" + bob ##> "/set accept group invitations on" + bob <## "ok" + connectUsers alice bob + -- switch away: bob2 now has auto-accept on but is NOT the active profile + bob ##> "/user bob" + showActiveUser bob "bob (Bob)" + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/a team bob2 admin" + alice <## "invitation to join the group #team sent to bob2" + concurrently_ + (alice <## "#team: bob2 joined the group") + (bob <## "[user: bob2] #team: you joined the group") + testMultiWordProfileNames :: HasCallStack => TestParams -> IO () testMultiWordProfileNames = testChat3 aliceProfile' bobProfile' cathProfile' $ diff --git a/tests/JSONFixtures.hs b/tests/JSONFixtures.hs index 37fab0e4f0..bbdf8c14b0 100644 --- a/tests/JSONFixtures.hs +++ b/tests/JSONFixtures.hs @@ -17,10 +17,10 @@ activeUserExistsTagged :: LB.ByteString activeUserExistsTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}" activeUserSwift :: LB.ByteString -activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"userChatRelay\":false,\"clientService\":false}}}}" +activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"autoAcceptGroupInvitations\":false,\"userChatRelay\":false,\"clientService\":false}}}}" activeUserTagged :: LB.ByteString -activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"userChatRelay\":false,\"clientService\":false}}}" +activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"autoAcceptGroupInvitations\":false,\"userChatRelay\":false,\"clientService\":false}}}" chatStartedSwift :: LB.ByteString chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}" @@ -35,7 +35,7 @@ connectionsDiffTagged :: LB.ByteString connectionsDiffTagged = "{\"result\":{\"type\":\"connectionsDiff\",\"userIds\":{\"missingIds\":[],\"extraIds\":[]},\"connIds\":{\"missingIds\":[],\"extraIds\":[]}}}" userJSON :: LB.ByteString -userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"userChatRelay\":false}" +userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"autoAcceptGroupInvitations\":false,\"userChatRelay\":false}" parsedMarkdownSwift :: LB.ByteString parsedMarkdownSwift = "{\"formattedText\":[{\"format\":{\"_owsf\":true,\"bold\":{}},\"text\":\"hello\"}]}"