From 1872744543fb7a557cc341d337e6788ba4ebbabe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 29 Nov 2022 15:19:20 +0000 Subject: [PATCH] core, mobile: add group feature to allow direct messages (#1465) * core, mobile: split group features to a separate type (to add directAllowed later) * add directMessages group feature, update tests --- .../java/chat/simplex/app/model/ChatModel.kt | 19 ++-- .../java/chat/simplex/app/model/SimpleXAPI.kt | 101 +++++++++++++----- .../app/views/chat/ContactPreferences.kt | 4 +- .../views/chat/group/GroupMemberInfoView.kt | 8 +- .../app/views/chat/group/GroupPreferences.kt | 10 +- .../app/views/chat/item/ChatItemView.kt | 4 +- .../app/views/usersettings/Preferences.kt | 4 +- .../app/src/main/res/values/strings.xml | 9 +- .../Chat/ChatItem/CIChatFeatureView.swift | 2 +- apps/ios/Shared/Views/Chat/ChatItemView.swift | 1 + .../Views/Chat/ContactPreferencesView.swift | 2 +- .../Chat/Group/GroupMemberInfoView.swift | 2 +- .../Chat/Group/GroupPreferencesView.swift | 5 +- .../Views/UserSettings/PreferencesView.swift | 2 +- apps/ios/SimpleXChat/ChatTypes.swift | 87 ++++++++++++--- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 19 ++-- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Messages.hs | 41 ++++--- .../M20221129_delete_group_feature_items.hs | 13 +++ src/Simplex/Chat/Store.hs | 4 +- src/Simplex/Chat/Types.hs | 82 +++++++++----- src/Simplex/Chat/View.hs | 8 +- tests/ChatTests.hs | 66 ++++++------ tests/ProtocolTests.hs | 2 +- 25 files changed, 334 insertions(+), 164 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20221129_delete_group_feature_items.hs diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 8a5b9e18e..9a73ec4f5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1098,6 +1098,7 @@ data class ChatItem ( is CIContent.RcvGroupFeature -> false is CIContent.SndGroupFeature -> showNtfDir is CIContent.RcvChatFeatureRejected -> showNtfDir + is CIContent.RcvGroupFeatureRejected -> showNtfDir } fun withStatus(status: CIStatus): ChatItem = this.copy(meta = meta.copy(itemStatus = status)) @@ -1171,7 +1172,7 @@ data class ChatItem ( file = null ) - fun getChatFeatureSample(feature: Feature, enabled: FeatureEnabled): ChatItem { + fun getChatFeatureSample(feature: ChatFeature, enabled: FeatureEnabled): ChatItem { val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled) return ChatItem( chatDir = CIDirection.DirectRcv(), @@ -1277,11 +1278,12 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndConnEvent") class SndConnEventContent(val sndConnEvent: SndConnEvent): CIContent() { override val msgContent: MsgContent? get() = null } - @Serializable @SerialName("rcvChatFeature") class RcvChatFeature(val feature: Feature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null } - @Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: Feature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null } - @Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val feature: Feature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null } - @Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val feature: Feature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null } - @Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: Feature): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvChatFeature") class RcvChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null } override val text: String get() = when (this) { is SndMsgContent -> msgContent.text @@ -1299,9 +1301,10 @@ sealed class CIContent: ItemContent { is SndConnEventContent -> sndConnEvent.text is RcvChatFeature -> "${feature.text}: ${enabled.text}" is SndChatFeature -> "${feature.text}: ${enabled.text}" - is RcvGroupFeature -> "${feature.text}: ${preference.enable.text}" - is SndGroupFeature -> "${feature.text}: ${preference.enable.text}" + is RcvGroupFeature -> "${groupFeature.text}: ${preference.enable.text}" + is SndGroupFeature -> "${groupFeature.text}: ${preference.enable.text}" is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}" + is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}" } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 242bca677..0b2a5aa89 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -12,8 +12,7 @@ import android.util.Log import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DeleteForever -import androidx.compose.material.icons.filled.KeyboardVoice +import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -2048,12 +2047,18 @@ sealed class ContactUserPref { @Serializable @SerialName("user") data class User(val preference: ChatPreference): ContactUserPref() // global user default is used } +interface Feature { +// val icon: ImageVector + val text: String + val iconFilled: ImageVector +} + @Serializable -enum class Feature { +enum class ChatFeature: Feature { @SerialName("fullDelete") FullDelete, @SerialName("voice") Voice; - val text: String + override val text: String get() = when(this) { FullDelete -> generalGetString(R.string.full_deletion) Voice -> generalGetString(R.string.voice_messages) @@ -2065,7 +2070,7 @@ enum class Feature { Voice -> Icons.Outlined.KeyboardVoice } - val iconFilled: ImageVector + override val iconFilled: ImageVector get() = when(this) { FullDelete -> Icons.Filled.DeleteForever Voice -> Icons.Filled.KeyboardVoice @@ -2100,31 +2105,67 @@ enum class Feature { else -> generalGetString(R.string.voice_prohibited_in_this_chat) } } +} -fun enableGroupPrefDescription(enabled: GroupFeatureEnabled, canEdit: Boolean): String = - if (canEdit) { - when(this) { - FullDelete -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(R.string.allow_to_delete_messages) - GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_message_deletion) +@Serializable +enum class GroupFeature: Feature { + @SerialName("directMessages") DirectMessages, + @SerialName("fullDelete") FullDelete, + @SerialName("voice") Voice; + + override val text: String + get() = when(this) { + DirectMessages -> generalGetString(R.string.direct_messages) + FullDelete -> generalGetString(R.string.full_deletion) + Voice -> generalGetString(R.string.voice_messages) + } + + val icon: ImageVector + get() = when(this) { + DirectMessages -> Icons.Outlined.SwapHorizontalCircle + FullDelete -> Icons.Outlined.DeleteForever + Voice -> Icons.Outlined.KeyboardVoice + } + + override val iconFilled: ImageVector + get() = when(this) { + DirectMessages -> Icons.Filled.SwapHorizontalCircle + FullDelete -> Icons.Filled.DeleteForever + Voice -> Icons.Filled.KeyboardVoice + } + + fun enableDescription(enabled: GroupFeatureEnabled, canEdit: Boolean): String = + if (canEdit) { + when(this) { + DirectMessages -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(R.string.allow_direct_messages) + GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_direct_messages) + } + FullDelete -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(R.string.allow_to_delete_messages) + GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_message_deletion) + } + Voice -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(R.string.allow_to_send_voice) + GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_sending_voice) + } } - Voice -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(R.string.allow_to_send_voice) - GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_sending_voice) + } else { + when(this) { + DirectMessages -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_send_dms) + GroupFeatureEnabled.OFF -> generalGetString(R.string.direct_messages_are_prohibited_in_chat) + } + FullDelete -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_delete) + GroupFeatureEnabled.OFF -> generalGetString(R.string.message_deletion_prohibited_in_chat) + } + Voice -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_send_voice) + GroupFeatureEnabled.OFF -> generalGetString(R.string.voice_messages_are_prohibited) + } } } - } else { - when(this) { - FullDelete -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_delete) - GroupFeatureEnabled.OFF -> generalGetString(R.string.message_deletion_prohibited_in_chat) - } - Voice -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_send_voice) - GroupFeatureEnabled.OFF -> generalGetString(R.string.voice_messages_are_prohibited) - } - } - } } @Serializable @@ -2212,24 +2253,26 @@ enum class FeatureAllowed { @Serializable data class FullGroupPreferences( + val directMessages: GroupPreference, val fullDelete: GroupPreference, val voice: GroupPreference ) { fun toGroupPreferences(): GroupPreferences = - GroupPreferences(fullDelete = fullDelete, voice = voice) + GroupPreferences(directMessages = directMessages, fullDelete = fullDelete, voice = voice) companion object { - val sampleData = FullGroupPreferences(fullDelete = GroupPreference(enable = GroupFeatureEnabled.OFF), voice = GroupPreference(enable = GroupFeatureEnabled.ON)) + val sampleData = FullGroupPreferences(directMessages = GroupPreference(GroupFeatureEnabled.OFF), fullDelete = GroupPreference(GroupFeatureEnabled.OFF), voice = GroupPreference(GroupFeatureEnabled.ON)) } } @Serializable data class GroupPreferences( + val directMessages: GroupPreference?, val fullDelete: GroupPreference?, val voice: GroupPreference? ) { companion object { - val sampleData = GroupPreferences(fullDelete = GroupPreference(enable = GroupFeatureEnabled.OFF), voice = GroupPreference(enable = GroupFeatureEnabled.ON)) + val sampleData = GroupPreferences(directMessages = GroupPreference(GroupFeatureEnabled.OFF), fullDelete = GroupPreference(GroupFeatureEnabled.OFF), voice = GroupPreference(GroupFeatureEnabled.ON)) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt index fdc1c651c..65ed8861e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt @@ -78,7 +78,7 @@ private fun ContactPreferencesLayout( // } // SectionSpacer() val allowVoice: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) } - FeatureSection(Feature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) { + FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) { applyPrefs(featuresAllowed.copy(voice = it)) } SectionSpacer() @@ -92,7 +92,7 @@ private fun ContactPreferencesLayout( @Composable private fun FeatureSection( - feature: Feature, + feature: ChatFeature, userDefault: FeatureAllowed, pref: ContactUserPreference, allowFeature: State, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index 4afec7fba..b97ca1ff6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -133,10 +133,12 @@ fun GroupMemberInfoLayout( } SectionSpacer() - SectionView { - OpenChatButton(openDirectChat) + if (member.memberContactId != null && groupInfo.fullGroupPreferences.directMessages.enable == GroupFeatureEnabled.ON) { + SectionView { + OpenChatButton(openDirectChat) + } + SectionSpacer() } - SectionSpacer() SectionView(title = stringResource(R.string.member_info_section_title_member)) { InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt index a6da56449..fb2ffe867 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt @@ -62,13 +62,17 @@ private fun GroupPreferencesLayout( horizontalAlignment = Alignment.Start, ) { AppBarTitle(stringResource(R.string.group_preferences)) + val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) } + FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo) { + applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it))) + } // val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) } // FeatureSection(Feature.FullDelete, allowFullDeletion, groupInfo) { // applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it))) // } // SectionSpacer() val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) } - FeatureSection(Feature.Voice, allowVoice, groupInfo) { + FeatureSection(GroupFeature.Voice, allowVoice, groupInfo) { applyPrefs(preferences.copy(voice = GroupPreference(enable = it))) } if (groupInfo.canEdit) { @@ -83,7 +87,7 @@ private fun GroupPreferencesLayout( } @Composable -private fun FeatureSection(feature: Feature, enableFeature: State, groupInfo: GroupInfo, onSelected: (GroupFeatureEnabled) -> Unit) { +private fun FeatureSection(feature: GroupFeature, enableFeature: State, groupInfo: GroupInfo, onSelected: (GroupFeatureEnabled) -> Unit) { SectionView { if (groupInfo.canEdit) { SectionItemView { @@ -102,7 +106,7 @@ private fun FeatureSection(feature: Feature, enableFeature: State CIEventView(cItem) is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor) is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor) - is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.feature, c.preference.enable.iconColor) - is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.feature, c.preference.enable.iconColor) + is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor) + is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor) is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt index 3e7ae4dc1..e5ae5bcff 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt @@ -67,7 +67,7 @@ private fun PreferencesLayout( // } // SectionSpacer() val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) } - FeatureSection(Feature.Voice, allowVoice) { + FeatureSection(ChatFeature.Voice, allowVoice) { applyPrefs(preferences.copy(voice = ChatPreference(allow = it))) } SectionSpacer() @@ -80,7 +80,7 @@ private fun PreferencesLayout( } @Composable -private fun FeatureSection(feature: Feature, allowFeature: State, onSelected: (FeatureAllowed) -> Unit) { +private fun FeatureSection(feature: ChatFeature, allowFeature: State, onSelected: (FeatureAllowed) -> Unit) { SectionView { SectionItemView { ExposedDropDownSettingRow( diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index d2c912b42..6cb78aa3b 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -947,6 +947,7 @@ Contact preferences Group preferences Your preferences + Direct messages Full deletion Voice messages enabled @@ -968,13 +969,17 @@ Only you can send voice messages. Only your contact can send voice messages. Voice messages are prohibited in this chat. + Allow sending direct messages to members. + Prohibit sending direct messages to members. Allow to irreversibly delete sent messages. Prohibit irreversible message deletion. Allow to send voice messages. Prohibit sending voice messages. + Group members can send direct messages. + Direct messages between members are prohibited in this group. Group members can irreversibly delete sent messages. - Irreversible message deletion is prohibited in this chat. + Irreversible message deletion is prohibited in this group. Group members can send voice messages. - Voice messages are prohibited in this chat. + Voice messages are prohibited in this group. diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift index bdbb7c8ce..2033ec3c7 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -29,6 +29,6 @@ struct CIChatFeatureView: View { struct CIChatFeatureView_Previews: PreviewProvider { static var previews: some View { let enabled = FeatureEnabled(forUser: false, forContact: false) - CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: .fullDelete, iconColor: enabled.iconColor) + CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index b7bcd1d94..8b743f353 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -36,6 +36,7 @@ struct ChatItemView: View { case let .rcvGroupFeature(feature, preference): chatFeatureView(feature, preference.enable.iconColor) case let .sndGroupFeature(feature, preference): chatFeatureView(feature, preference.enable.iconColor) case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red) + case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red) } } diff --git a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift index f22e2f550..30f33a573 100644 --- a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift @@ -32,7 +32,7 @@ struct ContactPreferencesView: View { } } - private func featureSection(_ feature: Feature, _ userDefault: FeatureAllowed, _ pref: ContactUserPreference, _ allowFeature: Binding) -> some View { + private func featureSection(_ feature: ChatFeature, _ userDefault: FeatureAllowed, _ pref: ContactUserPreference, _ allowFeature: Binding) -> some View { let enabled = FeatureEnabled.enabled( user: Preference(allow: allowFeature.wrappedValue.allowed), contact: pref.contactPreference diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 9cf73f9a2..0095af6eb 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -42,7 +42,7 @@ struct GroupMemberInfoView: View { groupMemberInfoHeader(member) .listRowBackground(Color.clear) - if let contactId = member.memberContactId { + if let contactId = member.memberContactId, groupInfo.fullGroupPreferences.directMessages.enable == .on { Section { openDirectChatButton(contactId) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 71d02a772..73039a1bc 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -19,6 +19,7 @@ struct GroupPreferencesView: View { VStack { List { // featureSection(.fullDelete, $preferences.fullDelete.enable) + featureSection(.directMessages, $preferences.directMessages.enable) featureSection(.voice, $preferences.voice.enable) if groupInfo.canEdit { @@ -32,7 +33,7 @@ struct GroupPreferencesView: View { } } - private func featureSection(_ feature: Feature, _ enableFeature: Binding) -> some View { + private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding) -> some View { Section { if (groupInfo.canEdit) { settingsRow(feature.icon) { @@ -50,7 +51,7 @@ struct GroupPreferencesView: View { } } } footer: { - Text(feature.enableGroupPrefDescription(enableFeature.wrappedValue, groupInfo.canEdit)) + Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit)) .frame(height: 36, alignment: .topLeading) } } diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift index 01b766a62..6d24cf447 100644 --- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift +++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift @@ -30,7 +30,7 @@ struct PreferencesView: View { } } - private func featureSection(_ feature: Feature, _ allowFeature: Binding) -> some View { + private func featureSection(_ feature: ChatFeature, _ allowFeature: Binding) -> some View { Section { settingsRow(feature.icon) { Picker(feature.text, selection: allowFeature) { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 465ea071a..0e6dcf334 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -245,11 +245,15 @@ public enum ContactUserPref: Decodable { } } -public enum Feature: String, Decodable { +public protocol Feature { + var iconFilled: String { get } +} + +public enum ChatFeature: String, Decodable, Feature { case fullDelete case voice - public var values: [Feature] { [.fullDelete, .voice] } + public var values: [ChatFeature] { [.fullDelete, .voice] } public var id: Self { self } @@ -311,10 +315,49 @@ public enum Feature: String, Decodable { : "Voice messages are prohibited in this chat." } } +} - public func enableGroupPrefDescription(_ enabled: GroupFeatureEnabled, _ canEdit: Bool) -> LocalizedStringKey { +public enum GroupFeature: String, Decodable, Feature { + case fullDelete + case voice + case directMessages + + public var values: [GroupFeature] { [.directMessages, .fullDelete, .voice] } + + public var id: Self { self } + + public var text: String { + switch self { + case .directMessages: return NSLocalizedString("Direct messages", comment: "chat feature") + case .fullDelete: return NSLocalizedString("Full deletion", comment: "chat feature") + case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") + } + } + + public var icon: String { + switch self { + case .directMessages: return "arrow.left.and.right.circle" + case .fullDelete: return "trash.slash" + case .voice: return "mic" + } + } + + public var iconFilled: String { + switch self { + case .directMessages: return "arrow.left.and.right.circle.fill" + case .fullDelete: return "trash.slash.fill" + case .voice: return "mic.fill" + } + } + + public func enableDescription(_ enabled: GroupFeatureEnabled, _ canEdit: Bool) -> LocalizedStringKey { if canEdit { switch self { + case .directMessages: + switch enabled { + case .on: return "Allow sending direct messages to members." + case .off: return "Prohibit sending direct messages to members." + } case .fullDelete: switch enabled { case .on: return "Allow to irreversibly delete sent messages." @@ -328,15 +371,20 @@ public enum Feature: String, Decodable { } } else { switch self { + case .directMessages: + switch enabled { + case .on: return "Group members can send direct messages." + case .off: return "Direct messages between members are prohibited in this group." + } case .fullDelete: switch enabled { case .on: return "Group members can irreversibly delete sent messages." - case .off: return "Irreversible message deletion is prohibited in this chat." + case .off: return "Irreversible message deletion is prohibited in this group." } case .voice: switch enabled { case .on: return "Group members can send voice messages." - case .off: return "Voice messages are prohibited in this chat." + case .off: return "Voice messages are prohibited in this group." } } } @@ -443,31 +491,35 @@ public enum FeatureAllowed: String, Codable, Identifiable { } public struct FullGroupPreferences: Decodable, Equatable { + public var directMessages: GroupPreference public var fullDelete: GroupPreference public var voice: GroupPreference - public init(fullDelete: GroupPreference, voice: GroupPreference) { + public init(directMessages: GroupPreference, fullDelete: GroupPreference, voice: GroupPreference) { + self.directMessages = directMessages self.fullDelete = fullDelete self.voice = voice } - public static let sampleData = FullGroupPreferences(fullDelete: GroupPreference(enable: .off), voice: GroupPreference(enable: .on)) + public static let sampleData = FullGroupPreferences(directMessages: GroupPreference(enable: .off), fullDelete: GroupPreference(enable: .off), voice: GroupPreference(enable: .on)) } public struct GroupPreferences: Codable { + public var directMessages: GroupPreference? public var fullDelete: GroupPreference? public var voice: GroupPreference? - public init(fullDelete: GroupPreference?, voice: GroupPreference?) { + public init(directMessages: GroupPreference?, fullDelete: GroupPreference?, voice: GroupPreference?) { + self.directMessages = directMessages self.fullDelete = fullDelete self.voice = voice } - public static let sampleData = GroupPreferences(fullDelete: GroupPreference(enable: .off), voice: GroupPreference(enable: .on)) + public static let sampleData = GroupPreferences(directMessages: GroupPreference(enable: .off), fullDelete: GroupPreference(enable: .off), voice: GroupPreference(enable: .on)) } public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> GroupPreferences { - GroupPreferences(fullDelete: fullPreferences.fullDelete, voice: fullPreferences.voice) + GroupPreferences(directMessages: fullPreferences.directMessages, fullDelete: fullPreferences.fullDelete, voice: fullPreferences.voice) } public struct GroupPreference: Codable, Equatable { @@ -1371,6 +1423,7 @@ public struct ChatItem: Identifiable, Decodable { case .rcvGroupFeature: return false case .sndGroupFeature: return showNtfDir case .rcvChatFeatureRejected: return showNtfDir + case .rcvGroupFeatureRejected: return showNtfDir } } @@ -1462,7 +1515,7 @@ public struct ChatItem: Identifiable, Decodable { ) } - public static func getChatFeatureSample(_ feature: Feature, _ enabled: FeatureEnabled) -> ChatItem { + public static func getChatFeatureSample(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> ChatItem { let content = CIContent.rcvChatFeature(feature: feature, enabled: enabled) return ChatItem( chatDir: .directRcv, @@ -1573,11 +1626,12 @@ public enum CIContent: Decodable, ItemContent { case sndGroupEvent(sndGroupEvent: SndGroupEvent) case rcvConnEvent(rcvConnEvent: RcvConnEvent) case sndConnEvent(sndConnEvent: SndConnEvent) - case rcvChatFeature(feature: Feature, enabled: FeatureEnabled) - case sndChatFeature(feature: Feature, enabled: FeatureEnabled) - case rcvGroupFeature(feature: Feature, preference: GroupPreference) - case sndGroupFeature(feature: Feature, preference: GroupPreference) - case rcvChatFeatureRejected(feature: Feature) + case rcvChatFeature(feature: ChatFeature, enabled: FeatureEnabled) + case sndChatFeature(feature: ChatFeature, enabled: FeatureEnabled) + case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference) + case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference) + case rcvChatFeatureRejected(feature: ChatFeature) + case rcvGroupFeatureRejected(groupFeature: GroupFeature) public var text: String { get { @@ -1600,6 +1654,7 @@ public enum CIContent: Decodable, ItemContent { case let .rcvGroupFeature(feature, preference): return "\(feature.text): \(preference.enable.text)" case let .sndGroupFeature(feature, preference): return "\(feature.text): \(preference.enable.text)" case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text) + case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text) } } } diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7cf1861ba..a93fcd384 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -63,6 +63,7 @@ library Simplex.Chat.Migrations.M20221029_group_link_id Simplex.Chat.Migrations.M20221112_server_password Simplex.Chat.Migrations.M20221115_server_cfg + Simplex.Chat.Migrations.M20221129_delete_group_feature_items Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index b59d66c8b..f80a940c7 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -340,7 +340,7 @@ processChatCommand = \case Group gInfo@GroupInfo {membership, localDisplayName = gName} ms <- withStore $ \db -> getGroup db user chatId unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved case groupFeatureProhibited gInfo mc of - Just f -> pure $ chatCmdError $ "feature not allowed " <> T.unpack (chatFeatureToText f) + Just f -> pure $ chatCmdError $ "feature not allowed " <> T.unpack (groupFeatureToText f) _ -> do (fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length ms) (msgContainer, quotedItem_) <- prepareMsg fileInvitation_ membership @@ -2278,7 +2278,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = newGroupContentMessage gInfo@GroupInfo {chatSettings} m@GroupMember {localDisplayName = c} mc msg msgMeta = do let (ExtMsgContent content fInv_) = mcExtMsgContent mc case groupFeatureProhibited gInfo content of - Just f -> void $ newChatItem (CIRcvChatFeatureRejected f) Nothing + Just f -> void $ newChatItem (CIRcvGroupFeatureRejected f) Nothing _ -> do ciFile_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) ciFile_ @@ -2531,7 +2531,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = createGroupFeatureItems :: GroupInfo -> GroupMember -> m () createGroupFeatureItems g@GroupInfo {groupProfile} m = do let prefs = mergeGroupPreferences $ groupPreferences groupProfile - forM_ allChatFeatures $ \f -> do + forM_ allGroupFeatures $ \f -> do let p = getGroupPreference f prefs createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature f p) Nothing @@ -3113,9 +3113,9 @@ createFeatureChangedItems user Contact {mergedPreferences = cups} ct'@Contact {m unless (enabled == enabled') $ createInternalChatItem user (chatDir ct') (ciContent f enabled') Nothing -createGroupFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> ChatDirection 'CTGroup d -> (ChatFeature -> GroupPreference -> CIContent d) -> GroupProfile -> GroupProfile -> m () +createGroupFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> CIContent d) -> GroupProfile -> GroupProfile -> m () createGroupFeatureChangedItems user cd ciContent p p' = - forM_ allChatFeatures $ \f -> do + forM_ allGroupFeatures $ \f -> do let pref = getGroupPreference f $ groupPreferences p pref' = getGroupPreference f $ groupPreferences p' unless (pref == pref') $ @@ -3132,11 +3132,11 @@ featureProhibited forWhom Contact {mergedPreferences} = \case in if forWhom enabled then Nothing else Just CFVoice _ -> Nothing -groupFeatureProhibited :: GroupInfo -> MsgContent -> Maybe ChatFeature +groupFeatureProhibited :: GroupInfo -> MsgContent -> Maybe GroupFeature groupFeatureProhibited GroupInfo {fullGroupPreferences} = \case MCVoice {} -> - let GroupPreference {enable} = getGroupPreference CFVoice fullGroupPreferences - in case enable of FEOn -> Nothing; FEOff -> Just CFVoice + let GroupPreference {enable} = getGroupPreference GFVoice fullGroupPreferences + in case enable of FEOn -> Nothing; FEOff -> Just GFVoice _ -> Nothing createInternalChatItem :: forall c d m. (ChatTypeI c, MsgDirectionI d, ChatMonad m) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m () @@ -3399,9 +3399,10 @@ chatCommandP = "/profile_image" $> UpdateProfileImage Nothing, ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> userNames), ("/profile" <|> "/p") $> ShowProfile, - "/voice #" *> (SetGroupFeature CFVoice <$> displayName <*> (A.space *> strP)), + "/voice #" *> (SetGroupFeature GFVoice <$> displayName <*> (A.space *> strP)), "/voice @" *> (SetContactFeature CFVoice <$> displayName <*> optional (A.space *> strP)), "/voice " *> (SetUserFeature CFVoice <$> strP), + "/dms #" *> (SetGroupFeature GFDirectMessages <$> displayName <*> (A.space *> strP)), "/incognito " *> (SetIncognito <$> onOffP), ("/quit" <|> "/q" <|> "/exit") $> QuitChat, ("/version" <|> "/v") $> ShowVersion, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 7750d76a3..609471910 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -252,7 +252,7 @@ data ChatCommand | UpdateProfileImage (Maybe ImageData) | SetUserFeature ChatFeature FeatureAllowed | SetContactFeature ChatFeature ContactName (Maybe FeatureAllowed) - | SetGroupFeature ChatFeature GroupName GroupFeatureEnabled + | SetGroupFeature GroupFeature GroupName GroupFeatureEnabled | QuitChat | ShowVersion | DebugLocks diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 8e580783e..ed0a36bbc 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -560,9 +560,10 @@ data CIContent (d :: MsgDirection) where CISndConnEvent :: SndConnEvent -> CIContent 'MDSnd CIRcvChatFeature :: ChatFeature -> PrefEnabled -> CIContent 'MDRcv CISndChatFeature :: ChatFeature -> PrefEnabled -> CIContent 'MDSnd - CIRcvGroupFeature :: ChatFeature -> GroupPreference -> CIContent 'MDRcv - CISndGroupFeature :: ChatFeature -> GroupPreference -> CIContent 'MDSnd + CIRcvGroupFeature :: GroupFeature -> GroupPreference -> CIContent 'MDRcv + CISndGroupFeature :: GroupFeature -> GroupPreference -> CIContent 'MDSnd CIRcvChatFeatureRejected :: ChatFeature -> CIContent 'MDRcv + CIRcvGroupFeatureRejected :: GroupFeature -> CIContent 'MDRcv -- ^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API -- ! ^ Nested sum types also have to use different encodings for database and API -- ! ^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent @@ -589,6 +590,7 @@ ciCreateStatus = \case CIRcvGroupFeature {} -> CISRcvRead CISndGroupFeature {} -> ciStatusNew CIRcvChatFeatureRejected _ -> ciStatusNew + CIRcvGroupFeatureRejected _ -> ciStatusNew data RcvGroupEvent = RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting @@ -750,9 +752,10 @@ ciContentToText = \case CISndConnEvent event -> sndConnEventToText event CIRcvChatFeature feature enabled -> chatFeatureToText feature <> ": " <> prefEnabledToText enabled CISndChatFeature feature enabled -> chatFeatureToText feature <> ": " <> prefEnabledToText enabled - CIRcvGroupFeature feature pref -> chatFeatureToText feature <> ": " <> groupPrefToText pref - CISndGroupFeature feature pref -> chatFeatureToText feature <> ": " <> groupPrefToText pref + CIRcvGroupFeature feature pref -> groupFeatureToText feature <> ": " <> groupPrefToText pref + CISndGroupFeature feature pref -> groupFeatureToText feature <> ": " <> groupPrefToText pref CIRcvChatFeatureRejected feature -> chatFeatureToText feature <> ": received, prohibited" + CIRcvGroupFeatureRejected feature -> groupFeatureToText feature <> ": received, prohibited" msgIntegrityError :: MsgErrorType -> Text msgIntegrityError = \case @@ -805,9 +808,10 @@ data JSONCIContent | JCISndConnEvent {sndConnEvent :: SndConnEvent} | JCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled} | JCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled} - | JCIRcvGroupFeature {feature :: ChatFeature, preference :: GroupPreference} - | JCISndGroupFeature {feature :: ChatFeature, preference :: GroupPreference} + | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference} + | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference} | JCIRcvChatFeatureRejected {feature :: ChatFeature} + | JCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} deriving (Generic) instance FromJSON JSONCIContent where @@ -834,9 +838,10 @@ jsonCIContent = \case CISndConnEvent sndConnEvent -> JCISndConnEvent {sndConnEvent} CIRcvChatFeature feature enabled -> JCIRcvChatFeature {feature, enabled} CISndChatFeature feature enabled -> JCISndChatFeature {feature, enabled} - CIRcvGroupFeature feature preference -> JCIRcvGroupFeature {feature, preference} - CISndGroupFeature feature preference -> JCISndGroupFeature {feature, preference} + CIRcvGroupFeature groupFeature preference -> JCIRcvGroupFeature {groupFeature, preference} + CISndGroupFeature groupFeature preference -> JCISndGroupFeature {groupFeature, preference} CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature} + CIRcvGroupFeatureRejected groupFeature -> JCIRcvGroupFeatureRejected {groupFeature} aciContentJSON :: JSONCIContent -> ACIContent aciContentJSON = \case @@ -855,9 +860,10 @@ aciContentJSON = \case JCISndConnEvent {sndConnEvent} -> ACIContent SMDSnd $ CISndConnEvent sndConnEvent JCIRcvChatFeature {feature, enabled} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled JCISndChatFeature {feature, enabled} -> ACIContent SMDSnd $ CISndChatFeature feature enabled - JCIRcvGroupFeature {feature, preference} -> ACIContent SMDRcv $ CIRcvGroupFeature feature preference - JCISndGroupFeature {feature, preference} -> ACIContent SMDSnd $ CISndGroupFeature feature preference + JCIRcvGroupFeature {groupFeature, preference} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference + JCISndGroupFeature {groupFeature, preference} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature + JCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature -- platform independent data DBJSONCIContent @@ -876,9 +882,10 @@ data DBJSONCIContent | DBJCISndConnEvent {sndConnEvent :: DBSndConnEvent} | DBJCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled} | DBJCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled} - | DBJCIRcvGroupFeature {feature :: ChatFeature, preference :: GroupPreference} - | DBJCISndGroupFeature {feature :: ChatFeature, preference :: GroupPreference} + | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference} + | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference} | DBJCIRcvChatFeatureRejected {feature :: ChatFeature} + | DBJCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} deriving (Generic) instance FromJSON DBJSONCIContent where @@ -905,9 +912,10 @@ dbJsonCIContent = \case CISndConnEvent sce -> DBJCISndConnEvent $ SCE sce CIRcvChatFeature feature enabled -> DBJCIRcvChatFeature {feature, enabled} CISndChatFeature feature enabled -> DBJCISndChatFeature {feature, enabled} - CIRcvGroupFeature feature preference -> DBJCIRcvGroupFeature {feature, preference} - CISndGroupFeature feature preference -> DBJCISndGroupFeature {feature, preference} + CIRcvGroupFeature groupFeature preference -> DBJCIRcvGroupFeature {groupFeature, preference} + CISndGroupFeature groupFeature preference -> DBJCISndGroupFeature {groupFeature, preference} CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature} + CIRcvGroupFeatureRejected groupFeature -> DBJCIRcvGroupFeatureRejected {groupFeature} aciContentDBJSON :: DBJSONCIContent -> ACIContent aciContentDBJSON = \case @@ -926,9 +934,10 @@ aciContentDBJSON = \case DBJCISndConnEvent (SCE sce) -> ACIContent SMDSnd $ CISndConnEvent sce DBJCIRcvChatFeature {feature, enabled} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled DBJCISndChatFeature {feature, enabled} -> ACIContent SMDSnd $ CISndChatFeature feature enabled - DBJCIRcvGroupFeature {feature, preference} -> ACIContent SMDRcv $ CIRcvGroupFeature feature preference - DBJCISndGroupFeature {feature, preference} -> ACIContent SMDSnd $ CISndGroupFeature feature preference + DBJCIRcvGroupFeature {groupFeature, preference} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference + DBJCISndGroupFeature {groupFeature, preference} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature + DBJCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature data CICallStatus = CISCallPending diff --git a/src/Simplex/Chat/Migrations/M20221129_delete_group_feature_items.hs b/src/Simplex/Chat/Migrations/M20221129_delete_group_feature_items.hs new file mode 100644 index 000000000..7baf6b584 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20221129_delete_group_feature_items.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20221129_delete_group_feature_items where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20221129_delete_group_feature_items :: Query +m20221129_delete_group_feature_items = + [sql| +DELETE FROM chat_items WHERE item_content LIKE '%{"rcvGroupFeature":{%'; +DELETE FROM chat_items WHERE item_content LIKE '%{"sndGroupFeature":{%'; +|] diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 822d8316b..095a6ce1c 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -298,6 +298,7 @@ import Simplex.Chat.Migrations.M20221025_chat_settings import Simplex.Chat.Migrations.M20221029_group_link_id import Simplex.Chat.Migrations.M20221112_server_password import Simplex.Chat.Migrations.M20221115_server_cfg +import Simplex.Chat.Migrations.M20221129_delete_group_feature_items import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -346,7 +347,8 @@ schemaMigrations = ("20221025_chat_settings", m20221025_chat_settings), ("20221029_group_link_id", m20221029_group_link_id), ("20221112_server_password", m20221112_server_password), - ("20221115_server_cfg", m20221115_server_cfg) + ("20221115_server_cfg", m20221115_server_cfg), + ("20221129_delete_group_feature_items", m20221129_delete_group_feature_items) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index f36483791..9e4b45017 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -281,12 +281,6 @@ chatPrefSel = \case -- CFReceipts -> receipts CFVoice -> voice -chatPrefName :: ChatFeature -> Text -chatPrefName = \case - CFFullDelete -> "full message deletion" - -- CFReceipts -> "delivery receipts" - CFVoice -> "voice messages" - class PreferenceI p where getPreference :: ChatFeature -> p -> Preference @@ -329,14 +323,43 @@ instance ToField Preferences where instance FromField Preferences where fromField = fromTextField_ decodeJSON -groupPrefSel :: ChatFeature -> GroupPreferences -> Maybe GroupPreference +data GroupFeature + = GFDirectMessages + | GFFullDelete + | -- | GFReceipts + GFVoice + deriving (Show, Generic) + +groupFeatureToText :: GroupFeature -> Text +groupFeatureToText = \case + GFDirectMessages -> "Direct messages" + GFFullDelete -> "Full deletion" + GFVoice -> "Voice messages" + +instance ToJSON GroupFeature where + toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "GF" + toJSON = J.genericToJSON . enumJSON $ dropPrefix "GF" + +instance FromJSON GroupFeature where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "GF" + +allGroupFeatures :: [GroupFeature] +allGroupFeatures = + [ GFDirectMessages, + GFFullDelete, + -- GFReceipts, + GFVoice + ] + +groupPrefSel :: GroupFeature -> GroupPreferences -> Maybe GroupPreference groupPrefSel = \case - CFFullDelete -> fullDelete - -- CFReceipts -> receipts - CFVoice -> voice + GFDirectMessages -> directMessages + GFFullDelete -> fullDelete + -- GFReceipts -> receipts + GFVoice -> voice class GroupPreferenceI p where - getGroupPreference :: ChatFeature -> p -> GroupPreference + getGroupPreference :: GroupFeature -> p -> GroupPreference instance GroupPreferenceI GroupPreferences where getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt prefs) @@ -346,14 +369,16 @@ instance GroupPreferenceI (Maybe GroupPreferences) where instance GroupPreferenceI FullGroupPreferences where getGroupPreference = \case - CFFullDelete -> fullDelete - -- CFReceipts -> receipts - CFVoice -> voice + GFDirectMessages -> directMessages + GFFullDelete -> fullDelete + -- GFReceipts -> receipts + GFVoice -> voice {-# INLINE getGroupPreference #-} -- collection of optional group preferences data GroupPreferences = GroupPreferences - { fullDelete :: Maybe GroupPreference, + { directMessages :: Maybe GroupPreference, + fullDelete :: Maybe GroupPreference, -- receipts :: Maybe GroupPreference, voice :: Maybe GroupPreference } @@ -369,13 +394,14 @@ instance ToField GroupPreferences where instance FromField GroupPreferences where fromField = fromTextField_ decodeJSON -setGroupPreference :: ChatFeature -> GroupFeatureEnabled -> Maybe GroupPreferences -> GroupPreferences +setGroupPreference :: GroupFeature -> GroupFeatureEnabled -> Maybe GroupPreferences -> GroupPreferences setGroupPreference f enable prefs_ = let prefs = mergeGroupPreferences prefs_ pref = (getGroupPreference f prefs :: GroupPreference) {enable} in toGroupPreferences $ case f of - CFVoice -> prefs {voice = pref} - CFFullDelete -> prefs {fullDelete = pref} + GFDirectMessages -> prefs {directMessages = pref} + GFVoice -> prefs {voice = pref} + GFFullDelete -> prefs {fullDelete = pref} -- full collection of chat preferences defined in the app - it is used to ensure we include all preferences and to simplify processing -- if some of the preferences are not defined in Preferences, defaults from defaultChatPrefs are used here. @@ -391,7 +417,8 @@ instance ToJSON FullPreferences where toEncoding = J.genericToEncoding J.default -- full collection of group preferences defined in the app - it is used to ensure we include all preferences and to simplify processing -- if some of the preferences are not defined in GroupPreferences, defaults from defaultGroupPrefs are used here. data FullGroupPreferences = FullGroupPreferences - { fullDelete :: GroupPreference, + { directMessages :: GroupPreference, + fullDelete :: GroupPreference, -- receipts :: GroupPreference, voice :: GroupPreference } @@ -447,7 +474,8 @@ emptyChatPrefs = Preferences Nothing Nothing defaultGroupPrefs :: FullGroupPreferences defaultGroupPrefs = FullGroupPreferences - { fullDelete = GroupPreference {enable = FEOff}, + { directMessages = GroupPreference {enable = FEOff}, + fullDelete = GroupPreference {enable = FEOff}, -- receipts = GroupPreference {enable = FEOff}, voice = GroupPreference {enable = FEOn} } @@ -543,9 +571,10 @@ mergeUserChatPrefs' user connectedIncognito userPreferences = mergeGroupPreferences :: Maybe GroupPreferences -> FullGroupPreferences mergeGroupPreferences groupPreferences = FullGroupPreferences - { fullDelete = pref CFFullDelete, - -- receipts = pref CFReceipts, - voice = pref CFVoice + { directMessages = pref GFDirectMessages, + fullDelete = pref GFFullDelete, + -- receipts = pref GFReceipts, + voice = pref GFVoice } where pref pt = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPreferences >>= groupPrefSel pt) @@ -553,9 +582,10 @@ mergeGroupPreferences groupPreferences = toGroupPreferences :: FullGroupPreferences -> GroupPreferences toGroupPreferences groupPreferences = GroupPreferences - { fullDelete = pref CFFullDelete, - -- receipts = pref CFReceipts, - voice = pref CFVoice + { directMessages = pref GFDirectMessages, + fullDelete = pref GFFullDelete, + -- receipts = pref GFReceipts, + voice = pref GFVoice } where pref f = Just $ getGroupPreference f groupPreferences diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index be38b768c..8afc15fc8 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -745,7 +745,7 @@ viewContactPreferences user ct ct' cups = viewContactPref :: FullPreferences -> FullPreferences -> Maybe Preferences -> ContactUserPreferences -> ChatFeature -> Maybe StyledString viewContactPref userPrefs userPrefs' ctPrefs cups pt | userPref == userPref' && ctPref == contactPreference = Nothing - | otherwise = Just $ plain (chatPrefName pt) <> ": " <> plain (prefEnabledToText enabled) <> " (you allow: " <> viewCountactUserPref userPreference <> ", contact allows: " <> viewPreference contactPreference <> ")" + | otherwise = Just $ plain (chatFeatureToText pt) <> ": " <> plain (prefEnabledToText enabled) <> " (you allow: " <> viewCountactUserPref userPreference <> ", contact allows: " <> viewPreference contactPreference <> ")" where userPref = getPreference pt userPrefs userPref' = getPreference pt userPrefs' @@ -760,7 +760,7 @@ viewPrefsUpdated ps ps' prefs = mapMaybe viewPref allChatFeatures viewPref pt | pref ps == pref ps' = Nothing - | otherwise = Just $ plain (chatPrefName pt) <> " allowed: " <> viewPreference (pref ps') + | otherwise = Just $ plain (chatFeatureToText pt) <> " allowed: " <> viewPreference (pref ps') where pref pss = getPreference pt $ mergePreferences pss Nothing @@ -796,10 +796,10 @@ viewGroupUpdated | null prefs = [] | otherwise = "updated group preferences:" : prefs where - prefs = mapMaybe viewPref allChatFeatures + prefs = mapMaybe viewPref allGroupFeatures viewPref pt | pref gps == pref gps' = Nothing - | otherwise = Just $ plain (chatPrefName pt) <> " enabled: " <> plain (groupPrefToText $ pref gps') + | otherwise = Just $ plain (groupFeatureToText pt) <> " enabled: " <> plain (groupPrefToText $ pref gps') where pref pss = getGroupPreference pt $ mergeGroupPreferences pss diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index cd1ba9940..240dd8cd5 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -1266,7 +1266,7 @@ testGroupMessageDelete = cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))]) -- alice: msg id 5 - bob #$> ("/_update item #1 " <> groupItemId 2 6 <> " text hi alice", id, "message updated") + bob #$> ("/_update item #1 " <> groupItemId 2 7 <> " text hi alice", id, "message updated") concurrently_ (alice <# "#team bob> [edited] hi alice") ( do @@ -1285,7 +1285,7 @@ testGroupMessageDelete = (alice <# "#team cath> how are you?") (bob <# "#team cath> how are you?") - cath #$> ("/_delete item #1 " <> groupItemId 2 6 <> " broadcast", id, "message deleted") + cath #$> ("/_delete item #1 " <> groupItemId 2 7 <> " broadcast", id, "message deleted") concurrently_ (alice <# "#team cath> [deleted] how are you?") (bob <# "#team cath> [deleted] how are you?") @@ -2610,17 +2610,17 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi (bob "/_set prefs @2 {\"fullDelete\": {\"allow\": \"always\"}}" alice <## ("you updated preferences for " <> bobIncognito <> ":") - alice <## "full message deletion: enabled for contact (you allow: always, contact allows: no)" + alice <## "Full deletion: enabled for contact (you allow: always, contact allows: no)" bob <## (aliceIncognito <> " updated preferences for you:") - bob <## "full message deletion: enabled for you (you allow: no, contact allows: always)" + bob <## "Full deletion: enabled for you (you allow: no, contact allows: always)" bob ##> "/_set prefs @2 {}" bob <## ("your preferences for " <> aliceIncognito <> " did not change") (alice "/_set prefs @2 {\"fullDelete\": {\"allow\": \"no\"}}" alice <## ("you updated preferences for " <> bobIncognito <> ":") - alice <## "full message deletion: off (you allow: no, contact allows: no)" + alice <## "Full deletion: off (you allow: no, contact allows: no)" bob <## (aliceIncognito <> " updated preferences for you:") - bob <## "full message deletion: off (you allow: no, contact allows: no)" + bob <## "Full deletion: off (you allow: no, contact allows: no)" testConnectIncognitoContactAddress :: IO () testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ @@ -2919,32 +2919,32 @@ testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathPr alice ##> "/_profile {\"displayName\": \"alice\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}}}" alice <## "user full name removed (your contacts are notified)" alice <## "updated preferences:" - alice <## "full message deletion allowed: always" + alice <## "Full deletion allowed: always" (alice "/_set prefs @2 {\"fullDelete\": {\"allow\": \"always\"}}" bob <## ("you updated preferences for " <> aliceIncognito <> ":") - bob <## "full message deletion: enabled for contact (you allow: always, contact allows: no)" + bob <## "Full deletion: enabled for contact (you allow: always, contact allows: no)" alice <## "bob updated preferences for you:" - alice <## "full message deletion: enabled for you (you allow: no, contact allows: always)" + alice <## "Full deletion: enabled for you (you allow: no, contact allows: always)" alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"yes\"}}" alice <## "you updated preferences for bob:" - alice <## "full message deletion: enabled (you allow: yes, contact allows: always)" + alice <## "Full deletion: enabled (you allow: yes, contact allows: always)" bob <## (aliceIncognito <> " updated preferences for you:") - bob <## "full message deletion: enabled (you allow: always, contact allows: yes)" + bob <## "Full deletion: enabled (you allow: always, contact allows: yes)" (cath "/_set prefs @3 {\"fullDelete\": {\"allow\": \"always\"}}" alice <## "your preferences for cath did not change" alice ##> "/_set prefs @3 {\"fullDelete\": {\"allow\": \"yes\"}}" alice <## "you updated preferences for cath:" - alice <## "full message deletion: off (you allow: yes, contact allows: no)" + alice <## "Full deletion: off (you allow: yes, contact allows: no)" cath <## "alice updated preferences for you:" - cath <## "full message deletion: off (you allow: default (no), contact allows: yes)" + cath <## "Full deletion: off (you allow: default (no), contact allows: yes)" testSetAlias :: IO () testSetAlias = testChat2 aliceProfile bobProfile $ @@ -2986,7 +2986,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ bob ##> "/_profile {\"displayName\": \"bob\", \"fullName\": \"Bob\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}" bob <## "profile image removed" bob <## "updated preferences:" - bob <## "voice messages allowed: no" + bob <## "Voice messages allowed: no" (bob "/_set prefs @2 {}" @@ -3004,10 +3004,10 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ -- alice ##> "/_set prefs @2 {\"voice\": {\"allow\": \"always\"}}" alice ##> "/voice @bob always" alice <## "you updated preferences for bob:" - alice <## "voice messages: enabled for contact (you allow: always, contact allows: no)" + alice <## "Voice messages: enabled for contact (you allow: always, contact allows: no)" alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact")]) bob <## "alice updated preferences for you:" - bob <## "voice messages: enabled for you (you allow: default (no), contact allows: always)" + bob <## "Voice messages: enabled for you (you allow: default (no), contact allows: always)" bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you")]) alice ##> sendVoice alice <## voiceNotAllowed @@ -3023,25 +3023,25 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ -- alice ##> "/_profile {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}" alice ##> "/voice no" alice <## "updated preferences:" - alice <## "voice messages allowed: no" + alice <## "Voice messages allowed: no" (alice "/_set prefs @2 {\"voice\": {\"allow\": \"yes\"}}" alice <## "you updated preferences for bob:" - alice <## "voice messages: off (you allow: yes, contact allows: no)" + alice <## "Voice messages: off (you allow: yes, contact allows: no)" alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off")]) bob <## "alice updated preferences for you:" - bob <## "voice messages: off (you allow: default (no), contact allows: yes)" + bob <## "Voice messages: off (you allow: default (no), contact allows: yes)" bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off")]) (bob "/_profile {\"displayName\": \"bob\", \"fullName\": \"\", \"preferences\": {\"voice\": {\"allow\": \"yes\"}}}" bob <## "user full name removed (your contacts are notified)" bob <## "updated preferences:" - bob <## "voice messages allowed: yes" + bob <## "Voice messages allowed: yes" bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled")]) (bob ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "Voice messages: enabled")]) (alice "/_set prefs @2 {}" @@ -3052,10 +3052,10 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ (alice "/_set prefs @2 {\"voice\": {\"allow\": \"no\"}}" alice <## "you updated preferences for bob:" - alice <## "voice messages: off (you allow: no, contact allows: yes)" + alice <## "Voice messages: off (you allow: no, contact allows: yes)" alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "Voice messages: enabled"), (1, "Voice messages: off")]) bob <## "alice updated preferences for you:" - bob <## "voice messages: off (you allow: default (yes), contact allows: no)" + bob <## "Voice messages: off (you allow: default (yes), contact allows: no)" bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled"), (0, "Voice messages: off")]) testUpdateGroupPrefs :: IO () @@ -3068,32 +3068,32 @@ testUpdateGroupPrefs = threadDelay 1000000 alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}}}" alice <## "updated group preferences:" - alice <## "full message deletion enabled: on" + alice <## "Full deletion enabled: on" alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" - bob <## "full message deletion enabled: on" + bob <## "Full deletion enabled: on" bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on")]) threadDelay 1000000 alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}}}" alice <## "updated group preferences:" - alice <## "full message deletion enabled: off" - alice <## "voice messages enabled: off" + alice <## "Full deletion enabled: off" + alice <## "Voice messages enabled: off" alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" - bob <## "full message deletion enabled: off" - bob <## "voice messages enabled: off" + bob <## "Full deletion enabled: off" + bob <## "Voice messages enabled: off" bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off")]) threadDelay 1000000 -- alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}" alice ##> "/voice #team on" alice <## "updated group preferences:" - alice <## "voice messages enabled: on" + alice <## "Voice messages enabled: on" alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" - bob <## "voice messages enabled: on" + bob <## "Voice messages enabled: on" bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on")]) threadDelay 1000000 alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}" @@ -4243,7 +4243,7 @@ groupFeatures :: [(Int, String)] groupFeatures = map (\(a, _, _) -> a) groupFeatures'' groupFeatures'' :: [((Int, String), Maybe (Int, String), Maybe String)] -groupFeatures'' = [((0, "Full deletion: off"), Nothing, Nothing), ((0, "Voice messages: on"), Nothing, Nothing)] +groupFeatures'' = [((0, "Direct messages: off"), Nothing, Nothing), ((0, "Full deletion: off"), Nothing, Nothing), ((0, "Voice messages: on"), Nothing, Nothing)] itemId :: Int -> String itemId i = show $ length chatFeatures + i diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 8295b37ee..753fe9125 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -83,7 +83,7 @@ testChatPreferences :: Maybe Preferences testChatPreferences = Just Preferences {voice = Just Preference {allow = FAYes}, fullDelete = Nothing} testGroupPreferences :: Maybe GroupPreferences -testGroupPreferences = Just GroupPreferences {voice = Just GroupPreference {enable = FEOn}, fullDelete = Nothing} +testGroupPreferences = Just GroupPreferences {directMessages = Nothing, voice = Just GroupPreference {enable = FEOn}, fullDelete = Nothing} testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), preferences = testChatPreferences}