core: support chats in channels, send as owner in support chats (#6870)

* core: test support chats in channels, CLI defaults to sending as member in support chat

* ui: enable support chats in channels

* use correct scope when sending from UI

* more readable

* remove test output

* show member support chat in channels

* preference for support chats

* ios: types for support preference

* mp: support preference types

* show support preference in UI

* fix ios

* make support preference optional in JSON parser

* update string

* change strings, pass parameters to prefs

* refactor kotlin

* take support preference into account

* refactor core

* do not show broadcast placeholder in support scope

* move role check, add pref check on update

* support preference test (failing)

* fix version

* fix tests

* warning alert when enabling chats with admins

* revert on dismiss

* update text and icons

* query plans

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-04-26 14:37:16 +01:00
committed by GitHub
parent 504ef253cb
commit 63c278818e
34 changed files with 503 additions and 93 deletions
@@ -515,7 +515,7 @@ struct ComposeView: View {
sendMessageView(
disableSendButton,
placeholder: chat.chatInfo.groupInfo.map { gi in
gi.useRelays && gi.membership.memberRole >= .owner
gi.useRelays && gi.membership.memberRole >= .owner && chat.chatInfo.groupChatScope() == nil
? NSLocalizedString("Broadcast", comment: "compose placeholder for channel owner")
: nil
} ?? nil
@@ -1659,7 +1659,7 @@ struct ComposeView: View {
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
scope: chat.chatInfo.groupChatScope(),
sendAsGroup: chat.chatInfo.groupInfo.map { $0.useRelays && $0.membership.memberRole >= .owner } ?? false,
sendAsGroup: chat.chatInfo.sendAsGroup,
live: live,
ttl: ttl,
composedMessages: msgs
@@ -103,6 +103,10 @@ struct GroupChatInfoView: View {
}
}
let showUserSupportChat = groupInfo.membership.memberActive
&& ((groupInfo.fullGroupPreferences.support.on && groupInfo.membership.memberRole < .moderator)
|| groupInfo.membership.supportChat != nil)
if groupInfo.useRelays {
Section {
// TODO [relays] allow other owners to manage channel link (requires protocol changes to share link ownership)
@@ -124,6 +128,12 @@ struct GroupChatInfoView: View {
if groupInfo.isOwner || members.contains(where: { $0.wrapped.memberRole >= .owner }) {
channelMembersButton()
}
if groupInfo.membership.memberRole >= .moderator {
memberSupportButton()
}
if showUserSupportChat {
UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
}
} footer: {
if !groupInfo.isOwner && groupInfo.groupProfile.publicGroup?.groupLink != nil {
Text("You can share a link or a QR code - anybody will be able to join the channel.")
@@ -141,8 +151,7 @@ struct GroupChatInfoView: View {
if groupInfo.canModerate {
GroupReportsChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
}
if groupInfo.membership.memberActive
&& (groupInfo.membership.memberRole < .moderator || groupInfo.membership.supportChat != nil) {
if showUserSupportChat {
UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
}
} header: {
@@ -121,13 +121,15 @@ struct GroupMemberInfoView: View {
}
if connectionLoaded {
let showMemberSupportChat = !openedFromSupportChat
&& groupInfo.membership.memberRole >= .moderator
&& member.memberRole != .relay
&& ((groupInfo.fullGroupPreferences.support.on && member.memberRole < .moderator)
|| member.supportChat != nil)
if member.memberActive {
Section {
if !openedFromSupportChat
&& groupInfo.membership.memberRole >= .moderator
&& member.memberRole != .relay
&& (member.memberRole < .moderator || member.supportChat != nil) {
if showMemberSupportChat {
MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId)
}
if let code = connectionCode,
@@ -142,6 +144,10 @@ struct GroupMemberInfoView: View {
// synchronizeConnectionButtonForce()
// }
}
} else if groupInfo.useRelays && member.memberCurrent && showMemberSupportChat {
Section {
MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId)
}
}
if let contactLink = member.contactLink {
@@ -46,13 +46,30 @@ struct GroupPreferencesView: View {
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
featureSection(.files, $preferences.files.enable, $preferences.files.role)
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
featureSection(.reports, $preferences.reports.enable)
featureSection(.reports, $preferences.reports.enable, disabled: true) // enable reports in 7.0 once directory support added
featureSection(.history, $preferences.history.enable)
featureSection(.support, $preferences.support.enable, disabled: true)
} else {
featureSection(.timedMessages, $preferences.timedMessages.enable)
featureSection(.fullDelete, $preferences.fullDelete.enable)
featureSection(.reactions, $preferences.reactions.enable)
featureSection(.history, $preferences.history.enable)
let supportNotice = NSLocalizedString("Chats with admins in public channels have no E2E encryption - use only with trusted chat relays.", comment: "alert message")
featureSection(.support, $preferences.support.enable, notice: supportNotice)
.onChange(of: preferences.support.enable) { enable in
if enable == .on {
showAlert(
NSLocalizedString("Enable chats with admins?", comment: "alert title"),
message: supportNotice,
actions: {[
UIAlertAction(title: NSLocalizedString("Enable", comment: "alert button"), style: .destructive) { _ in },
UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) { _ in
preferences.support.enable = .off
}
]}
)
}
}
}
if groupInfo.isOwner {
@@ -92,7 +109,7 @@ struct GroupPreferencesView: View {
}
}
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil) -> some View {
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil, disabled: Bool = false, notice: String? = nil) -> some View {
Section {
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
@@ -105,7 +122,7 @@ struct GroupPreferencesView: View {
settingsRow(icon, color: color) {
Toggle(feature.text, isOn: enable)
}
.disabled(feature == .reports) // remove in 6.4
.disabled(disabled)
if timedOn {
DropdownCustomTimePicker(
selection: $preferences.timedMessages.ttl,
@@ -144,8 +161,11 @@ struct GroupPreferencesView: View {
}
}
} footer: {
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner))
.foregroundColor(theme.colors.secondary)
VStack(alignment: .leading) {
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner))
if let notice { Text(notice) }
}
.foregroundColor(theme.colors.secondary)
}
.onChange(of: enableFeature.wrappedValue) { enabled in
if case .off = enabled {
@@ -45,7 +45,7 @@ struct MemberSupportView: View {
: membersWithChats.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
if membersWithChats.isEmpty {
Text("No chats with members")
Text(groupInfo.fullGroupPreferences.support.on ? "No chats with members" : "Chats with members are disabled")
.foregroundColor(.secondary)
} else {
List {
@@ -161,7 +161,10 @@ struct AddChannelView: View {
private func createChannel() {
focusDisplayName = false
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
profile.groupPreferences = GroupPreferences(history: GroupPreference(enable: .on))
profile.groupPreferences = GroupPreferences(
history: GroupPreference(enable: .on),
support: GroupPreference(enable: .off)
)
creationInProgress = true
Task {
do {
+1 -1
View File
@@ -68,7 +68,7 @@ func apiSendMessages(
type: chatInfo.chatType,
id: chatInfo.apiId,
scope: chatInfo.groupChatScope(),
sendAsGroup: chatInfo.groupInfo.map { $0.useRelays && $0.membership.memberRole >= .owner } ?? false,
sendAsGroup: chatInfo.sendAsGroup,
live: false,
ttl: nil,
composedMessages: composedMessages
+35
View File
@@ -867,6 +867,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable {
case simplexLinks
case reports
case history
case support
public var id: Self { self }
@@ -888,6 +889,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable {
case .simplexLinks: true
case .reports: false
case .history: false
case .support: false
}
}
@@ -902,6 +904,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable {
case .simplexLinks: return NSLocalizedString("SimpleX links", comment: "chat feature")
case .reports: return NSLocalizedString("Member reports", comment: "chat feature")
case .history: return NSLocalizedString("Visible history", comment: "chat feature")
case .support: return NSLocalizedString("Chat with admins", comment: "chat feature")
}
}
@@ -916,6 +919,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable {
case .simplexLinks: return "link.circle"
case .reports: return "flag"
case .history: return "clock"
case .support: return "questionmark.circle"
}
}
@@ -930,6 +934,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable {
case .simplexLinks: return "link.circle.fill"
case .reports: return "flag.fill"
case .history: return "clock.fill"
case .support: return "questionmark.circle.fill"
}
}
@@ -988,6 +993,11 @@ public enum GroupFeature: String, Decodable, Feature, Hashable {
case .on: return "Send up to 100 last messages to new members."
case .off: return "Do not send history to new members."
}
case .support:
switch enabled {
case .on: return "Allow members to chat with admins."
case .off: return "Prohibit chats with admins."
}
}
} else {
switch self {
@@ -1036,6 +1046,11 @@ public enum GroupFeature: String, Decodable, Feature, Hashable {
case .on: return "Up to 100 last messages are sent to new members."
case .off: return "History is not sent to new members."
}
case .support:
switch enabled {
case .on: return "Members can chat with admins."
case .off: return "Chats with admins are prohibited."
}
}
}
}
@@ -1190,6 +1205,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable {
public var simplexLinks: RoleGroupPreference
public var reports: GroupPreference
public var history: GroupPreference
public var support: GroupPreference
public var commands: [ChatBotCommand]
public init(
@@ -1202,6 +1218,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable {
simplexLinks: RoleGroupPreference,
reports: GroupPreference,
history: GroupPreference,
support: GroupPreference,
commands: [ChatBotCommand]
) {
self.timedMessages = timedMessages
@@ -1213,6 +1230,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable {
self.simplexLinks = simplexLinks
self.reports = reports
self.history = history
self.support = support
self.commands = commands
}
@@ -1226,6 +1244,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable {
simplexLinks: RoleGroupPreference(enable: .on, role: nil),
reports: GroupPreference(enable: .on),
history: GroupPreference(enable: .on),
support: GroupPreference(enable: .on),
commands: []
)
}
@@ -1240,6 +1259,7 @@ public struct GroupPreferences: Codable, Hashable {
public var simplexLinks: RoleGroupPreference?
public var reports: GroupPreference?
public var history: GroupPreference?
public var support: GroupPreference?
public var commands: [ChatBotCommand]?
public init(
@@ -1252,6 +1272,7 @@ public struct GroupPreferences: Codable, Hashable {
simplexLinks: RoleGroupPreference? = nil,
reports: GroupPreference? = nil,
history: GroupPreference? = nil,
support: GroupPreference? = nil,
commands: [ChatBotCommand]? = nil
) {
self.timedMessages = timedMessages
@@ -1263,6 +1284,7 @@ public struct GroupPreferences: Codable, Hashable {
self.simplexLinks = simplexLinks
self.reports = reports
self.history = history
self.support = support
self.commands = commands
}
@@ -1276,6 +1298,7 @@ public struct GroupPreferences: Codable, Hashable {
simplexLinks: RoleGroupPreference(enable: .on, role: nil),
reports: GroupPreference(enable: .on),
history: GroupPreference(enable: .on),
support: GroupPreference(enable: .on),
commands: nil
)
}
@@ -1760,6 +1783,18 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
}
}
public var sendAsGroup: Bool {
if let g = groupInfo, g.useRelays && g.membership.memberRole >= .owner {
switch groupChatScope() {
case .none: true
case .memberSupport: false
case .reports: false
}
} else {
false
}
}
public func ntfsEnabled(chatItem: ChatItem) -> Bool {
ntfsEnabled(chatItem.meta.userMention)
}
+1
View File
@@ -25,6 +25,7 @@ extension ChatLike {
case .files: p.files.on(for: groupInfo.membership)
case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership)
case .history: p.history.on
case .support: p.support.on
case .reports: p.reports.on
}
} else {
@@ -1687,6 +1687,18 @@ sealed class ChatInfo: SomeChat, NamedChat {
else -> null
}
val sendAsGroup: Boolean get() {
val g = (this as? Group)?.groupInfo
return if (g != null && g.useRelays && g.membership.memberRole >= GroupMemberRole.Owner) {
when (groupChatScope()) {
null -> true
is GroupChatScope.MemberSupport -> false
}
} else {
false
}
}
fun ntfsEnabled(ci: ChatItem): Boolean =
ntfsEnabled(ci.meta.userMention)
@@ -2133,6 +2145,7 @@ data class GroupInfo (
GroupFeature.SimplexLinks -> p.simplexLinks.on(membership)
GroupFeature.Reports -> p.reports.on
GroupFeature.History -> p.history.on
GroupFeature.Support -> p.support.on
}
}
@@ -5693,7 +5693,8 @@ enum class GroupFeature: Feature {
@SerialName("files") Files,
@SerialName("simplexLinks") SimplexLinks,
@SerialName("reports") Reports,
@SerialName("history") History;
@SerialName("history") History,
@SerialName("support") Support;
override val hasParam: Boolean get() = when(this) {
TimedMessages -> true
@@ -5711,6 +5712,7 @@ enum class GroupFeature: Feature {
SimplexLinks -> true
Reports -> false
History -> false
Support -> false
}
override val text: String
@@ -5724,6 +5726,7 @@ enum class GroupFeature: Feature {
SimplexLinks -> generalGetString(MR.strings.simplex_links)
Reports -> generalGetString(MR.strings.group_reports_member_reports)
History -> generalGetString(MR.strings.recent_history)
Support -> generalGetString(MR.strings.chat_with_admins)
}
val icon: Painter
@@ -5737,6 +5740,7 @@ enum class GroupFeature: Feature {
SimplexLinks -> painterResource(MR.images.ic_link)
Reports -> painterResource(MR.images.ic_flag)
History -> painterResource(MR.images.ic_schedule)
Support -> painterResource(MR.images.ic_help)
}
@Composable
@@ -5750,6 +5754,7 @@ enum class GroupFeature: Feature {
SimplexLinks -> painterResource(MR.images.ic_link)
Reports -> painterResource(MR.images.ic_flag_filled)
History -> painterResource(MR.images.ic_schedule_filled)
Support -> painterResource(MR.images.ic_help_filled)
}
fun enableDescription(enabled: GroupFeatureEnabled, canEdit: Boolean): String =
@@ -5791,6 +5796,10 @@ enum class GroupFeature: Feature {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.enable_sending_recent_history)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.disable_sending_recent_history)
}
Support -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_chat_with_admins)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_chat_with_admins)
}
}
} else {
when(this) {
@@ -5830,6 +5839,10 @@ enum class GroupFeature: Feature {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.recent_history_is_sent_to_new_members)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.recent_history_is_not_sent_to_new_members)
}
Support -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.members_can_chat_with_admins)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.chat_with_admins_is_prohibited)
}
}
}
}
@@ -5955,6 +5968,7 @@ data class FullGroupPreferences(
val simplexLinks: RoleGroupPreference,
val reports: GroupPreference,
val history: GroupPreference,
val support: GroupPreference,
val commands: List<ChatBotCommand>,
) {
fun toGroupPreferences(): GroupPreferences =
@@ -5968,6 +5982,7 @@ data class FullGroupPreferences(
simplexLinks = simplexLinks,
reports = reports,
history = history,
support = support,
commands = commands,
)
@@ -5982,6 +5997,7 @@ data class FullGroupPreferences(
simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
reports = GroupPreference(GroupFeatureEnabled.ON),
history = GroupPreference(GroupFeatureEnabled.ON),
support = GroupPreference(GroupFeatureEnabled.ON),
commands = listOf()
)
}
@@ -5998,6 +6014,7 @@ data class GroupPreferences(
val simplexLinks: RoleGroupPreference? = null,
val reports: GroupPreference? = null,
val history: GroupPreference? = null,
val support: GroupPreference? = null,
val commands: List<ChatBotCommand>? = null
) {
companion object {
@@ -534,7 +534,7 @@ fun ComposeView(
type = cInfo.chatType,
id = cInfo.apiId,
scope = cInfo.groupChatScope(),
sendAsGroup = (cInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false,
sendAsGroup = cInfo.sendAsGroup,
live = live,
ttl = ttl,
composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions))
@@ -665,7 +665,7 @@ fun ComposeView(
toChatType = chat.chatInfo.chatType,
toChatId = chat.chatInfo.apiId,
toScope = chat.chatInfo.groupChatScope(),
sendAsGroup = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false,
sendAsGroup = chat.chatInfo.sendAsGroup,
fromChatType = fromChatInfo.chatType,
fromChatId = fromChatInfo.apiId,
fromScope = fromChatInfo.groupChatScope(),
@@ -1496,7 +1496,7 @@ fun ComposeView(
)
is SharedContent.ChatLink -> {
val cInfo = chat.chatInfo
val sendAsGroup = (cInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false
val sendAsGroup = cInfo.sendAsGroup
withBGApi {
val mc = chatModel.controller.apiShareChatMsgContent(
chat.remoteHostId, ChatType.Group, shared.groupInfo.groupId,
@@ -1693,7 +1693,7 @@ fun ComposeView(
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
AttachmentAndCommandsButtons()
val broadcastPlaceholder = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.let { gi ->
if (gi.useRelays && gi.membership.memberRole >= GroupMemberRole.Owner) generalGetString(MR.strings.compose_view_broadcast)
if (gi.useRelays && gi.membership.memberRole >= GroupMemberRole.Owner && chat.chatInfo.groupChatScope() == null) generalGetString(MR.strings.compose_view_broadcast)
else null
}
SendMsgView_(disableSendButton = disableSendButton, placeholder = broadcastPlaceholder)
@@ -546,6 +546,10 @@ fun ModalData.GroupChatInfoLayout(
var anyTopSectionRowShow = false
val channelLink = groupInfo.groupProfile.publicGroup?.groupLink
val showUserSupportChat = groupInfo.membership.memberActive &&
((groupInfo.fullGroupPreferences.support.on && groupInfo.membership.memberRole < GroupMemberRole.Moderator)
|| groupInfo.membership.supportChat != null)
if (groupInfo.useRelays) {
SectionView {
if (groupInfo.isOwner && groupLink != null) {
@@ -564,6 +568,14 @@ fun ModalData.GroupChatInfoLayout(
anyTopSectionRowShow = true
ChannelMembersButton(chat.remoteHostId, groupInfo, showMemberInfo)
}
if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
anyTopSectionRowShow = true
MemberSupportButton(chat, openMemberSupport)
}
if (showUserSupportChat) {
anyTopSectionRowShow = true
UserSupportChatButton(chat, groupInfo, scrollToItemId)
}
}
if (!groupInfo.isOwner && channelLink != null) {
SectionTextFooter(stringResource(MR.strings.you_can_share_channel_link_anybody_will_be_able_to_connect))
@@ -590,10 +602,7 @@ fun ModalData.GroupChatInfoLayout(
}
}
}
if (
groupInfo.membership.memberActive &&
(groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null)
) {
if (showUserSupportChat) {
anyTopSectionRowShow = true
UserSupportChatButton(chat, groupInfo, scrollToItemId)
}
@@ -483,14 +483,15 @@ fun GroupMemberInfoLayout(
SectionSpacer()
}
val showMemberSupportChat = !openedFromSupportChat &&
groupInfo.membership.memberRole >= GroupMemberRole.Moderator &&
member.memberRole != GroupMemberRole.Relay &&
((groupInfo.fullGroupPreferences.support.on && member.memberRole < GroupMemberRole.Moderator)
|| member.supportChat != null)
if (member.memberActive) {
SectionView {
if (
!openedFromSupportChat &&
groupInfo.membership.memberRole >= GroupMemberRole.Moderator &&
member.memberRole != GroupMemberRole.Relay &&
(member.memberRole < GroupMemberRole.Moderator || member.supportChat != null)
) {
if (showMemberSupportChat) {
SupportChatButton()
}
if (connectionCode != null && !(groupInfo.useRelays && member.memberRole == GroupMemberRole.Relay)) {
@@ -504,6 +505,11 @@ fun GroupMemberInfoLayout(
// }
}
SectionDividerSpaced()
} else if (groupInfo.useRelays && member.memberCurrent && showMemberSupportChat) {
SectionView {
SupportChatButton()
}
SectionDividerSpaced()
}
if (member.contactLink != null) {
@@ -155,7 +155,7 @@ private fun GroupPreferencesLayout(
}
@Composable fun ReportsPreference() {
val enableReports = remember(preferences) { mutableStateOf(preferences.reports.enable) }
FeatureSection(GroupFeature.Reports, enableReports, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
FeatureSection(GroupFeature.Reports, enableReports, null, groupInfo, preferences, onTTLUpdated, disabled = true) { enable, _ -> // enable reports in 7.0 once directory support added
applyPrefs(preferences.copy(reports = GroupPreference(enable = enable)))
}
}
@@ -165,6 +165,16 @@ private fun GroupPreferencesLayout(
applyPrefs(preferences.copy(history = GroupPreference(enable = enable)))
}
}
@Composable fun SupportPreference(disabled: Boolean = false, notice: String? = null, onEnable: ((() -> Unit) -> Unit)? = null) {
val enableSupport = remember(preferences) { mutableStateOf(preferences.support.enable) }
FeatureSection(GroupFeature.Support, enableSupport, null, groupInfo, preferences, onTTLUpdated, disabled = disabled, notice = notice) { enable, _ ->
applyPrefs(preferences.copy(support = GroupPreference(enable = enable)))
if (enable == GroupFeatureEnabled.ON) onEnable?.invoke {
enableSupport.value = GroupFeatureEnabled.OFF
applyPrefs(preferences.copy(support = GroupPreference(enable = GroupFeatureEnabled.OFF)))
}
}
}
ColumnWithScrollBar {
val titleId = if (groupInfo.useRelays) MR.strings.channel_preferences
else if (groupInfo.businessChat == null) MR.strings.group_preferences
@@ -192,6 +202,8 @@ private fun GroupPreferencesLayout(
ReportsPreference()
SectionDividerSpaced(true, maxBottomPadding = false)
HistoryPreference()
SectionDividerSpaced(true, maxBottomPadding = false)
SupportPreference(disabled = true)
} else {
TimedMessagesPreference()
SectionDividerSpaced(true, maxBottomPadding = false)
@@ -200,6 +212,17 @@ private fun GroupPreferencesLayout(
ReactionsPreference()
SectionDividerSpaced(true, maxBottomPadding = false)
HistoryPreference()
SectionDividerSpaced(true, maxBottomPadding = false)
SupportPreference(notice = generalGetString(MR.strings.chat_with_admins_relay_note), onEnable = { revert ->
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.enable_chats_with_admins_question),
text = generalGetString(MR.strings.chat_with_admins_relay_note),
confirmText = generalGetString(MR.strings.enable_chats_with_admins),
destructive = true,
onDismiss = revert,
onDismissRequest = revert,
)
})
}
if (groupInfo.isOwner) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
@@ -233,6 +256,8 @@ private fun FeatureSection(
groupInfo: GroupInfo,
preferences: FullGroupPreferences,
onTTLUpdated: (Int?) -> Unit,
disabled: Boolean = false,
notice: String? = null,
onSelected: (GroupFeatureEnabled, GroupMemberRole?) -> Unit
) {
SectionView {
@@ -245,7 +270,7 @@ private fun FeatureSection(
feature.text,
icon,
iconTint,
disabled = feature == GroupFeature.Reports, // remove in 6.4
disabled = disabled,
checked = enableFeature.value == GroupFeatureEnabled.ON,
) { checked ->
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF, enableForRole?.value)
@@ -293,6 +318,9 @@ private fun FeatureSection(
}
}
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.isOwner))
if (notice != null) {
SectionTextFooter(notice)
}
}
@Composable
@@ -116,7 +116,10 @@ private fun ModalData.MemberSupportViewLayout(
if (membersWithChats.isEmpty()) {
item {
Box(Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
Text(generalGetString(MR.strings.no_support_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
Text(
generalGetString(if (groupInfo.fullGroupPreferences.support.on) MR.strings.no_support_chats else MR.strings.support_chats_disabled),
color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center
)
}
}
} else {
@@ -110,7 +110,10 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
fullName = "",
shortDescr = null,
image = profileImage.value,
groupPreferences = GroupPreferences(history = GroupPreference(GroupFeatureEnabled.ON))
groupPreferences = GroupPreferences(
history = GroupPreference(GroupFeatureEnabled.ON),
support = GroupPreference(GroupFeatureEnabled.OFF)
)
)
creationInProgress.value = true
withBGApi {
@@ -2337,6 +2337,14 @@
<string name="recent_history_is_not_sent_to_new_members">History is not sent to new members.</string>
<string name="group_members_can_send_reports">Members can report messsages to moderators.</string>
<string name="member_reports_are_prohibited">Reporting messages is prohibited in this group.</string>
<string name="chat_with_admins">Chat with admins</string>
<string name="allow_chat_with_admins">Allow members to chat with admins.</string>
<string name="prohibit_chat_with_admins">Prohibit chats with admins.</string>
<string name="members_can_chat_with_admins">Members can chat with admins.</string>
<string name="chat_with_admins_is_prohibited">Chats with admins are prohibited.</string>
<string name="chat_with_admins_relay_note">Chats with admins in public channels have no E2E encryption - use only with trusted chat relays.</string>
<string name="enable_chats_with_admins_question">Enable chats with admins?</string>
<string name="enable_chats_with_admins">Enable</string>
<string name="delete_after">Delete after</string>
<string name="ttl_sec">%d sec</string>
<string name="ttl_s">%ds</string>
@@ -2373,6 +2381,7 @@
<!-- MemberSupportView.kt -->
<string name="member_support">Chats with members</string>
<string name="no_support_chats">No chats with members</string>
<string name="support_chats_disabled">Chats with members are disabled</string>
<string name="delete_member_support_chat_button">Delete chat</string>
<string name="delete_member_support_chat_alert_title">Delete chat with member?</string>
+12
View File
@@ -171,6 +171,7 @@ This file is generated automatically.
- [SrvError](#srverror)
- [StoreError](#storeerror)
- [SubscriptionStatus](#subscriptionstatus)
- [SupportGroupPreference](#supportgrouppreference)
- [SwitchPhase](#switchphase)
- [TimedMessagesGroupPreference](#timedmessagesgrouppreference)
- [TimedMessagesPreference](#timedmessagespreference)
@@ -2109,6 +2110,7 @@ Phone:
- simplexLinks: [RoleGroupPreference](#rolegrouppreference)
- reports: [GroupPreference](#grouppreference)
- history: [GroupPreference](#grouppreference)
- support: [SupportGroupPreference](#supportgrouppreference)
- sessions: [RoleGroupPreference](#rolegrouppreference)
- comments: [CommentsGroupPreference](#commentsgrouppreference)
- commands: [[ChatBotCommand](#chatbotcommand)]
@@ -2200,6 +2202,7 @@ MemberSupport:
- "simplexLinks"
- "reports"
- "history"
- "support"
- "sessions"
- "comments"
@@ -2434,6 +2437,7 @@ NoRelays:
- simplexLinks: [RoleGroupPreference](#rolegrouppreference)?
- reports: [GroupPreference](#grouppreference)?
- history: [GroupPreference](#grouppreference)?
- support: [SupportGroupPreference](#supportgrouppreference)?
- sessions: [RoleGroupPreference](#rolegrouppreference)?
- comments: [CommentsGroupPreference](#commentsgrouppreference)?
- commands: [[ChatBotCommand](#chatbotcommand)]?
@@ -3899,6 +3903,14 @@ NoSub:
- type: "noSub"
---
## SupportGroupPreference
**Record type**:
- enable: [GroupFeatureEnabled](#groupfeatureenabled)
---
## SwitchPhase
+2
View File
@@ -354,6 +354,7 @@ chatTypesDocsData =
(sti @SrvError, STUnion, "SrvErr", [], "", ""),
(sti @StoreError, STUnion, "SE", [], "", ""),
(sti @SubscriptionStatus, STUnion, "SS", [], "", ""),
(sti @SupportGroupPreference, STRecord, "", [], "", ""),
(sti @SwitchPhase, STEnum, "SP", [], "", ""),
(sti @TimedMessagesGroupPreference, STRecord, "", [], "", ""),
(sti @TimedMessagesPreference, STRecord, "", [], "", ""),
@@ -563,6 +564,7 @@ deriving instance Generic SndGroupEvent
deriving instance Generic SrvError
deriving instance Generic StoreError
deriving instance Generic SubscriptionStatus
deriving instance Generic SupportGroupPreference
deriving instance Generic SwitchPhase
deriving instance Generic TimedMessagesGroupPreference
deriving instance Generic TimedMessagesPreference
@@ -2443,6 +2443,7 @@ export interface FullGroupPreferences {
simplexLinks: RoleGroupPreference
reports: GroupPreference
history: GroupPreference
support: SupportGroupPreference
sessions: RoleGroupPreference
comments: CommentsGroupPreference
commands: ChatBotCommand[]
@@ -2516,6 +2517,7 @@ export enum GroupFeature {
SimplexLinks = "simplexLinks",
Reports = "reports",
History = "history",
Support = "support",
Sessions = "sessions",
Comments = "comments",
}
@@ -2715,6 +2717,7 @@ export interface GroupPreferences {
simplexLinks?: RoleGroupPreference
reports?: GroupPreference
history?: GroupPreference
support?: SupportGroupPreference
sessions?: RoleGroupPreference
comments?: CommentsGroupPreference
commands?: ChatBotCommand[]
@@ -4637,6 +4640,10 @@ export namespace SubscriptionStatus {
}
}
export interface SupportGroupPreference {
enable: GroupFeatureEnabled
}
export enum SwitchPhase {
Started = "started",
Confirmed = "confirmed",
+14 -6
View File
@@ -625,7 +625,10 @@ processChatCommand vr nm = \case
mapM_ assertNoMentions cms
withContactLock "sendMessage" chatId $
sendContactContentMessages user chatId live itemTTL (L.map composedMessageReq cms)
SRGroup chatId gsScope asGroup ->
SRGroup chatId gsScope asGroup -> do
case gsScope of
Just (GCSMemberSupport _) -> when asGroup $ throwCmdError "cannot send as group in support scope"
Nothing -> pure ()
withGroupLock "sendMessage" chatId $ do
(gInfo, cmrs) <- withFastStore $ \db -> do
g <- getGroupInfo db vr user chatId
@@ -2375,7 +2378,7 @@ processChatCommand vr nm = \case
forM scope_ $ \(GSNMemberSupport mName_) ->
GCSMemberSupport <$> mapM (getGroupMemberIdByName db user gId) mName_
(gInfo, cScope_,) <$> liftIO (getMessageMentions db user gId msg)
let sendRef = SRGroup (groupId' gInfo) cScope_ (sendAsGroup' gInfo)
let sendRef = SRGroup (groupId' gInfo) cScope_ (sendAsGroup' gInfo cScope_)
processChatCommand vr nm $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions]
SNLocal -> do
folderId <- withFastStore (`getUserNoteFolderId` user)
@@ -3128,7 +3131,7 @@ processChatCommand vr nm = \case
qiId <- getGroupChatItemIdByText db user gId cName quotedMsg
(gInfo, qiId,) <$> liftIO (getMessageMentions db user gId msg)
let mc = MCText msg
processChatCommand vr nm $ APISendMessages (SRGroup (groupId' gInfo) Nothing (sendAsGroup' gInfo)) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions]
processChatCommand vr nm $ APISendMessages (SRGroup (groupId' gInfo) Nothing (sendAsGroup' gInfo Nothing)) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions]
ClearNoteFolder -> withUser $ \user -> do
folderId <- withFastStore (`getUserNoteFolderId` user)
processChatCommand vr nm $ APIClearChat (ChatRef CTLocal folderId Nothing)
@@ -3404,7 +3407,7 @@ processChatCommand vr nm = \case
_ -> throwCmdError "not supported"
pure $ ChatRef cType chatId Nothing
getSendAsGroup :: User -> ChatRef -> CM ShowGroupAsSender
getSendAsGroup user' (ChatRef CTGroup chatId _) = sendAsGroup' <$> withFastStore (\db -> getGroupInfo db vr user' chatId)
getSendAsGroup user' (ChatRef CTGroup chatId scope) = (`sendAsGroup'` scope) <$> withFastStore (\db -> getGroupInfo db vr user' chatId)
getSendAsGroup _ _ = pure False
getChatRefAndMentions :: User -> ChatName -> Text -> CM (ChatRef, Map MemberName GroupMemberId)
getChatRefAndMentions user cName msg = do
@@ -4490,7 +4493,7 @@ processChatCommand vr nm = \case
ChatRef CTDirect cId _ -> a $ SRDirect cId
ChatRef CTGroup gId scope -> do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId
a $ SRGroup gId scope (sendAsGroup' gInfo)
a $ SRGroup gId scope (sendAsGroup' gInfo scope)
_ -> throwCmdError "not supported"
getSharedMsgId :: CM SharedMsgId
getSharedMsgId = do
@@ -5020,7 +5023,7 @@ chatCommandP =
("/help" <|> "/h") $> ChatHelp HSMain,
("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile),
"/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP),
("/public group" <|> "/pg") *> (NewPublicGroup <$> incognitoP <* " relays=" <*> strP <* A.space <* char_ '#' <*> groupProfile),
("/public group" <|> "/pg") *> (NewPublicGroup <$> incognitoP <* " relays=" <*> strP <* A.space <* char_ '#' <*> channelProfile),
"/_public group " *> (APINewPublicGroup <$> A.decimal <*> incognitoOnOffP <*> _strP <* A.space <*> jsonP),
"/_get relays #" *> (APIGetGroupRelays <$> A.decimal),
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)),
@@ -5150,6 +5153,7 @@ chatCommandP =
"/set disappear @" *> (SetContactTimedMessages <$> displayNameP <*> optional (A.space *> timedMessagesEnabledP)),
"/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))),
"/set reports #" *> (SetGroupFeature (AGFNR SGFReports) <$> displayNameP <*> _strP),
"/set support #" *> (SetGroupFeature (AGFNR SGFSupport) <$> displayNameP <*> (A.space *> strP)),
"/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayNameP <*> _strP <*> optional memberRole),
"/set admission review #" *> (SetGroupMemberAdmissionReview <$> displayNameP <*> (A.space *> memberCriteriaP)),
("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito,
@@ -5287,6 +5291,10 @@ chatCommandP =
history = Just HistoryGroupPreference {enable = FEOn}
}
pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences, memberAdmission = Nothing}
channelProfile = do
p@GroupProfile {groupPreferences = prefs_} <- groupProfile
let prefs = (fromMaybe emptyGroupPrefs prefs_) {support = Just SupportGroupPreference {enable = FEOff}} :: GroupPreferences
pure p {groupPreferences = Just prefs}
memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing)
shortDescrP = do
descr <- A.takeWhile1 isSpace *> (T.dropWhileEnd isSpace <$> textP) <|> pure ""
+5
View File
@@ -338,12 +338,17 @@ quoteContent mc qmc ciFile_
prohibitedGroupContent :: GroupInfo -> GroupMember -> Maybe GroupChatScopeInfo -> MsgContent -> Maybe MarkdownList -> Maybe f -> Bool -> Maybe GroupFeature
prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole = userRole}} m scopeInfo mc ft file_ sent
| not supportAllowed = Just GFSupport
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) && not hostApprovalVoice = Just GFVoice
| isNothing scopeInfo && not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles
| isNothing scopeInfo && isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports
| isNothing scopeInfo && prohibitedSimplexLinks gInfo m mc ft = Just GFSimplexLinks
| otherwise = Nothing
where
supportAllowed = case scopeInfo of
Just (GCSIMemberSupport scopeMem_) ->
groupFeatureAllowed SGFSupport gInfo || isJust (supportChat $ fromMaybe mem scopeMem_)
Nothing -> True
hostApprovalVoice
| sent = userRole >= GRAdmin && sendApprovalPhase
| otherwise = memberCategory m == GCHostMember && hostApprovalPhase
+32 -17
View File
@@ -1535,7 +1535,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
memberCanSend :: Maybe GroupMember -> Maybe MsgScope -> CM (Maybe DeliveryTaskContext) -> CM (Maybe DeliveryTaskContext)
memberCanSend Nothing _ a = a -- channel message - was previously checked and allowed by relay
memberCanSend (Just m@GroupMember {memberRole}) msgScope a = case msgScope of
Just MSMember {} -> a
Just (MSMember mId)
| sameMemberId mId m || memberRole >= GRModerator -> a
| otherwise -> messageError "member is not allowed to send to this support chat" $> Nothing
Nothing
| memberRole > GRObserver || memberPending m -> a
| otherwise -> messageError "member is not allowed to send messages" $> Nothing
@@ -1837,13 +1839,19 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- This patches initial sharedMsgId into chat item when locally deleted chat item
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
-- Chat item and update message which created it will have different sharedMsgId in this case...
let timed_ = rcvContactCITimed ct ttl
ts = ciContentTexts content
(ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live M.empty
ci' <- withStore' $ \db -> do
createChatItemVersion db (chatItemId' ci) brokerTs mc
updateDirectChatItem' db user contactId ci content True live Nothing Nothing
toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv cInfo ci')
if isVoice mc && not (featureAllowed SCFVoice forContact ct)
then do
let ciContent = ciContentNoParse $ CIRcvChatFeatureRejected CFVoice
(ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs ciContent Nothing Nothing False M.empty
toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv cInfo ci)
else do
let timed_ = rcvContactCITimed ct ttl
ts = ciContentTexts content
(ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live M.empty
ci' <- withStore' $ \db -> do
createChatItemVersion db (chatItemId' ci) brokerTs mc
updateDirectChatItem' db user contactId ci content True live Nothing Nothing
toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv cInfo ci')
where
brokerTs = metaBrokerTs msgMeta
content = CIRcvMsgContent mc
@@ -2073,15 +2081,22 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
(gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m mc msgScope_
pure (gInfo', CDGroupRcv gInfo' scopeInfo m', mentions', scopeInfo)
Nothing -> pure (gInfo, CDChannelRcv gInfo Nothing, mentions, Nothing)
(ci, cInfo) <- saveRcvChatItem' user chatDir msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions'
ci' <- withStore' $ \db -> do
createChatItemVersion db (chatItemId' ci) brokerTs mc
updateGroupChatItem db user groupId ci content True live Nothing
ci'' <- case chatDir of
CDGroupRcv gi' _ m' -> blockedMemberCI gi' m' ci'
CDChannelRcv {} -> pure ci'
toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci'')
pure $ Just $ infoToDeliveryContext gInfo' scopeInfo showGroupAsSender
case m_ >>= \m -> prohibitedGroupContent gInfo' m scopeInfo mc ft_ (Nothing :: Maybe String) False of
Just f -> do
let ciContent = ciContentNoParse $ CIRcvGroupFeatureRejected f
(ci, cInfo) <- saveRcvChatItem' user chatDir msg (Just sharedMsgId) brokerTs ciContent Nothing timed_ False M.empty
groupMsgToView cInfo ci
pure Nothing
Nothing -> do
(ci, cInfo) <- saveRcvChatItem' user chatDir msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions'
ci' <- withStore' $ \db -> do
createChatItemVersion db (chatItemId' ci) brokerTs mc
updateGroupChatItem db user groupId ci content True live Nothing
ci'' <- case chatDir of
CDGroupRcv gi' _ m' -> blockedMemberCI gi' m' ci'
CDChannelRcv {} -> pure ci'
toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci'')
pure $ Just $ infoToDeliveryContext gInfo' scopeInfo showGroupAsSender
where
content = CIRcvMsgContent mc
ts@(_, ft_) = msgContentTexts mc
+5
View File
@@ -119,6 +119,11 @@ checkChatType x = case testEquality (chatTypeI @c) (chatTypeI @c') of
data GroupChatScope = GCSMemberSupport {groupMemberId_ :: Maybe GroupMemberId} -- Nothing means own conversation with support
deriving (Eq, Show, Ord)
sendAsGroup' :: GroupInfo -> Maybe GroupChatScope -> Bool
sendAsGroup' gInfo@GroupInfo {membership} scope = case scope of
Nothing -> useRelays' gInfo && memberRole' membership == GROwner
Just (GCSMemberSupport _) -> False
data GroupChatScopeTag
= GCSTMemberSupport_
deriving (Eq, Show)
+5 -2
View File
@@ -1555,6 +1555,7 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe
(Binary invId, groupLink, minVersion reqChatVRange, maxVersion reqChatVRange, groupId)
insertOwner_ currentTs groupId = do
let MemberIdRole {memberId, memberRole} = fromMember
VersionRange minV maxV = reqChatVRange
(localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs
indexInGroup <- getUpdateNextIndexInGroup_ db groupId
liftIO $ do
@@ -1563,11 +1564,13 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe
[sql|
INSERT INTO group_members
( group_id, index_in_group, member_id, member_role, member_category, member_status,
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
peer_chat_min_version, peer_chat_max_version)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (groupId, indexInGroup, memberId, memberRole, GCHostMember, GSMemAccepted)
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
:. (minV, maxV)
)
insertedRowId db
@@ -523,8 +523,9 @@ SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?)
Query:
INSERT INTO group_members
( group_id, index_in_group, member_id, member_role, member_category, member_status,
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
peer_chat_min_version, peer_chat_max_version)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?)
-3
View File
@@ -494,9 +494,6 @@ data GroupInfo = GroupInfo
useRelays' :: GroupInfo -> Bool
useRelays' GroupInfo {useRelays} = isTrue useRelays
sendAsGroup' :: GroupInfo -> Bool
sendAsGroup' gInfo@GroupInfo {membership} = useRelays' gInfo && memberRole' membership == GROwner
groupId' :: GroupInfo -> GroupId
groupId' GroupInfo {groupId} = groupId
+39 -8
View File
@@ -176,6 +176,7 @@ data GroupFeature
| GFSimplexLinks
| GFReports
| GFHistory
| GFSupport
| GFSessions
| GFComments
deriving (Show)
@@ -190,6 +191,7 @@ data SGroupFeature (f :: GroupFeature) where
SGFSimplexLinks :: SGroupFeature 'GFSimplexLinks
SGFReports :: SGroupFeature 'GFReports
SGFHistory :: SGroupFeature 'GFHistory
SGFSupport :: SGroupFeature 'GFSupport
SGFSessions :: SGroupFeature 'GFSessions
SGFComments :: SGroupFeature 'GFComments
@@ -218,6 +220,7 @@ groupFeatureNameText = \case
GFSimplexLinks -> "SimpleX links"
GFReports -> "Member reports"
GFHistory -> "Recent history"
GFSupport -> "Chat with admins"
GFSessions -> "Chat sessions"
GFComments -> "Comments"
@@ -248,11 +251,12 @@ allGroupFeatures =
AGF SGFFiles,
AGF SGFSimplexLinks,
AGF SGFReports,
AGF SGFHistory
AGF SGFHistory,
AGF SGFSupport
]
groupPrefSel :: SGroupFeature f -> GroupPreferences -> Maybe (GroupFeaturePreference f)
groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, sessions, comments} = case f of
groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, support, sessions, comments} = case f of
SGFTimedMessages -> timedMessages
SGFDirectMessages -> directMessages
SGFFullDelete -> fullDelete
@@ -262,6 +266,7 @@ groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reac
SGFSimplexLinks -> simplexLinks
SGFReports -> reports
SGFHistory -> history
SGFSupport -> support
SGFSessions -> sessions
SGFComments -> comments
@@ -276,6 +281,7 @@ toGroupFeature = \case
SGFSimplexLinks -> GFSimplexLinks
SGFReports -> GFReports
SGFHistory -> GFHistory
SGFSupport -> GFSupport
SGFSessions -> GFSessions
SGFComments -> GFComments
@@ -289,7 +295,7 @@ instance GroupPreferenceI (Maybe GroupPreferences) where
getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt =<< prefs)
instance GroupPreferenceI FullGroupPreferences where
getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, sessions, comments} = case f of
getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, support, sessions, comments} = case f of
SGFTimedMessages -> timedMessages
SGFDirectMessages -> directMessages
SGFFullDelete -> fullDelete
@@ -299,6 +305,7 @@ instance GroupPreferenceI FullGroupPreferences where
SGFSimplexLinks -> simplexLinks
SGFReports -> reports
SGFHistory -> history
SGFSupport -> support
SGFSessions -> sessions
SGFComments -> comments
{-# INLINE getGroupPreference #-}
@@ -314,6 +321,7 @@ data GroupPreferences = GroupPreferences
simplexLinks :: Maybe SimplexLinksGroupPreference,
reports :: Maybe ReportsGroupPreference,
history :: Maybe HistoryGroupPreference,
support :: Maybe SupportGroupPreference,
sessions :: Maybe SessionsGroupPreference,
comments :: Maybe CommentsGroupPreference,
commands :: Maybe [ChatBotCommand]
@@ -365,6 +373,7 @@ setGroupPreference_ f pref prefs =
SGFSimplexLinks -> prefs {simplexLinks = pref}
SGFReports -> prefs {reports = pref}
SGFHistory -> prefs {history = pref}
SGFSupport -> prefs {support = pref}
SGFSessions -> prefs {sessions = pref}
SGFComments -> prefs {comments = pref}
@@ -408,6 +417,7 @@ data FullGroupPreferences = FullGroupPreferences
simplexLinks :: SimplexLinksGroupPreference,
reports :: ReportsGroupPreference,
history :: HistoryGroupPreference,
support :: SupportGroupPreference,
sessions :: SessionsGroupPreference,
comments :: CommentsGroupPreference,
commands :: ListDef ChatBotCommand
@@ -478,13 +488,14 @@ defaultGroupPrefs =
simplexLinks = SimplexLinksGroupPreference {enable = FEOn, role = Nothing},
reports = ReportsGroupPreference {enable = FEOn},
history = HistoryGroupPreference {enable = FEOff},
support = SupportGroupPreference {enable = FEOn},
sessions = SessionsGroupPreference {enable = FEOff, role = Nothing},
comments = CommentsGroupPreference {enable = FEOff, duration = Nothing},
commands = ListDef []
}
emptyGroupPrefs :: GroupPreferences
emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
businessGroupPrefs :: Preferences -> GroupPreferences
businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice, files, sessions, commands} =
@@ -515,6 +526,7 @@ defaultBusinessGroupPrefs =
simplexLinks = Just $ SimplexLinksGroupPreference FEOn Nothing,
reports = Just $ ReportsGroupPreference FEOff,
history = Just $ HistoryGroupPreference FEOn,
support = Just $ SupportGroupPreference FEOn,
sessions = Just $ SessionsGroupPreference FEOn Nothing,
comments = Just $ CommentsGroupPreference FEOff Nothing,
commands = Nothing
@@ -647,6 +659,10 @@ data HistoryGroupPreference = HistoryGroupPreference
{enable :: GroupFeatureEnabled}
deriving (Eq, Show)
data SupportGroupPreference = SupportGroupPreference
{enable :: GroupFeatureEnabled}
deriving (Eq, Show)
data SessionsGroupPreference = SessionsGroupPreference
{enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole}
deriving (Eq, Show)
@@ -699,6 +715,9 @@ instance HasField "enable" ReportsGroupPreference GroupFeatureEnabled where
instance HasField "enable" HistoryGroupPreference GroupFeatureEnabled where
hasField p@HistoryGroupPreference {enable} = (\e -> p {enable = e}, enable)
instance HasField "enable" SupportGroupPreference GroupFeatureEnabled where
hasField p@SupportGroupPreference {enable} = (\e -> p {enable = e}, enable)
instance HasField "enable" SessionsGroupPreference GroupFeatureEnabled where
hasField p@SessionsGroupPreference {enable} = (\e -> p {enable = e}, enable)
@@ -759,6 +778,12 @@ instance GroupFeatureI 'GFHistory where
groupPrefParam _ = Nothing
groupPrefRole _ = Nothing
instance GroupFeatureI 'GFSupport where
type GroupFeaturePreference 'GFSupport = SupportGroupPreference
sGroupFeature = SGFSupport
groupPrefParam _ = Nothing
groupPrefRole _ = Nothing
instance GroupFeatureI 'GFSessions where
type GroupFeaturePreference 'GFSessions = SessionsGroupPreference
sGroupFeature = SGFSessions
@@ -781,6 +806,8 @@ instance GroupFeatureNoRoleI 'GFReports
instance GroupFeatureNoRoleI 'GFHistory
instance GroupFeatureNoRoleI 'GFSupport
instance GroupFeatureNoRoleI 'GFComments
instance HasField "role" DirectMessagesGroupPreference (Maybe GroupMemberRole) where
@@ -973,6 +1000,7 @@ mergeGroupPreferences groupPreferences =
simplexLinks = pref SGFSimplexLinks,
reports = pref SGFReports,
history = pref SGFHistory,
support = pref SGFSupport,
sessions = pref SGFSessions,
comments = pref SGFComments,
commands = ListDef $ fromMaybe [] $ groupPreferences >>= commands_
@@ -993,6 +1021,7 @@ toGroupPreferences groupPreferences@FullGroupPreferences {commands = ListDef cmd
simplexLinks = pref SGFSimplexLinks,
reports = pref SGFReports,
history = pref SGFHistory,
support = pref SGFSupport,
sessions = pref SGFSessions,
comments = pref SGFComments,
commands = Just cmds
@@ -1123,11 +1152,13 @@ $(J.deriveJSON defaultJSON ''ReportsGroupPreference)
$(J.deriveJSON defaultJSON ''HistoryGroupPreference)
$(J.deriveToJSON defaultJSON ''SessionsGroupPreference)
$(J.deriveToJSON defaultJSON ''SupportGroupPreference)
instance FromJSON SessionsGroupPreference where
parseJSON v = $(J.mkParseJSON defaultJSON ''SessionsGroupPreference) v
omittedField = Just SessionsGroupPreference {enable = FEOff, role = Nothing}
instance FromJSON SupportGroupPreference where
parseJSON v = $(J.mkParseJSON defaultJSON ''SupportGroupPreference) v
omittedField = Just SupportGroupPreference {enable = FEOn}
$(J.deriveJSON defaultJSON ''SessionsGroupPreference)
$(J.deriveToJSON defaultJSON ''CommentsGroupPreference)
+1 -1
View File
@@ -170,7 +170,7 @@ termSettings :: VirtualTerminalSettings
termSettings =
VirtualTerminalSettings
{ virtualType = "xterm",
virtualWindowSize = pure C.Size {height = 20, width = 6000},
virtualWindowSize = pure C.Size {height = 24, width = 6000},
virtualEvent = retry,
virtualInterrupt = retry
}
+9 -9
View File
@@ -200,14 +200,14 @@ testPaginationAllChatTypes =
ts7 <- iso8601Show <$> getCurrentTime
getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")]
getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on")]
getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")]
getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on")]
getChats_ alice ("after=" <> ts2 <> " count=2") [(":3", ""), ("@cath", "Audio/video calls: enabled")]
getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", "Recent history: on"), (":3", "")]
getChats_ alice ("after=" <> ts3 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", "")]
getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", "Chat with admins: on"), (":3", "")]
getChats_ alice ("after=" <> ts3 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on"), (":3", "")]
getChats_ alice ("before=" <> ts4 <> " count=10") [(":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")]
getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")]
getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")]
getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")]
getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")]
getChats_ alice ("after=" <> ts7 <> " count=10") []
getChats_ alice ("before=" <> ts1 <> " count=10") []
@@ -219,11 +219,11 @@ testPaginationAllChatTypes =
alice ##> "/_settings #1 {\"enableNtfs\":\"all\",\"favorite\":true}"
alice <## "ok"
getChats_ alice queryFavorite [("#team", "Recent history: on"), ("@bob", "hey")]
getChats_ alice queryFavorite [("#team", "Chat with admins: on"), ("@bob", "hey")]
getChats_ alice ("before=" <> ts4 <> " count=1 " <> queryFavorite) [("@bob", "hey")]
getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", "Recent history: on")]
getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", "Chat with admins: on")]
getChats_ alice ("after=" <> ts1 <> " count=1 " <> queryFavorite) [("@bob", "hey")]
getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", "Recent history: on")]
getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", "Chat with admins: on")]
let queryUnread = "{\"type\": \"filters\", \"favorite\": false, \"unread\": true}"
+160 -3
View File
@@ -231,6 +231,8 @@ chatGroupTests = do
it "should remove support chat with member when member is removed" testScopedSupportMemberRemoved
it "should remove support chat with member when user removes member" testScopedSupportUserRemovesMember
it "should remove support chat with member when member leaves" testScopedSupportMemberLeaves
it "should respect support preference in group" testSupportPreferenceGroup
it "should respect support preference in channel" testSupportPreferenceChannel
-- TODO [relays] add tests for channels
-- TODO - tests with delivery loop over members restored after restart
-- TODO - delivery in support scopes inside channels
@@ -267,6 +269,7 @@ chatGroupTests = do
it "owner should update profile in channel (signed)" testChannelOwnerProfileUpdate
it "subscriber should update profile in channel (signed)" testChannelSubscriberProfileUpdate
it "should report relay results when one relay deleted its address" testChannelCreateDeletedRelay
it "should deliver support scope messages via relay" testChannelSupportScope
describe "channel message operations" $ do
it "should update channel message" testChannelMessageUpdate
it "should delete channel message" testChannelMessageDelete
@@ -457,7 +460,7 @@ testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice
forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("#team alice> " <> show n)
-- All messages are unread for bob, should return area around unread
bob #$> ("/_get chat #1 initial=2", chat, [(0, "Recent history: on"), (0, "connected"), (0, "1"), (0, "2"), (0, "3")])
bob #$> ("/_get chat #1 initial=2", chat, [(0, "Chat with admins: on"), (0, "connected"), (0, "1"), (0, "2"), (0, "3")])
-- Read next 2 items
let itemIds = intercalate "," $ map groupItemId [1 .. 2]
@@ -646,7 +649,7 @@ testGroup2 =
]
dan <##> alice
-- show last messages
alice ##> "/t #club 20"
alice ##> "/t #club 21"
alice -- these strings are expected in any order because of sorting by time and rounding of time for sent
<##?
( map (ConsoleString . ("#club " <> )) groupFeatureStrs
@@ -1667,6 +1670,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile
alice <## "SimpleX links: on"
alice <## "Member reports: on"
alice <## "Recent history: on"
alice <## "Chat with admins: on"
bobAddedDan :: HasCallStack => TestCC -> IO ()
bobAddedDan cc = do
cc <## "#team: bob added dan (Daniel) to the group (connecting...)"
@@ -8400,6 +8404,120 @@ testScopedSupportMemberLeaves =
{ markRead = False
}
testSupportPreferenceGroup :: HasCallStack => TestParams -> IO ()
testSupportPreferenceGroup =
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
createGroup3' "team" alice (bob, GRMember) (cath, GRMember)
threadDelay 1000000
-- support enabled by default, bob sends to support
bob #> "#team (support) hello"
alice <# "#team (support: bob) bob> hello"
-- alice replies
alice #> "#team (support: bob) hi"
bob <# "#team (support) alice> hi"
-- alice disables support
alice ##> "/set support #team off"
alice <## "updated group preferences:"
alice <## "Chat with admins: off"
concurrentlyN_
[ do
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Chat with admins: off",
do
cath <## "alice updated group #team:"
cath <## "updated group preferences:"
cath <## "Chat with admins: off"
]
threadDelay 500000
-- cath can't send support (no existing chat)
cath ##> "#team (support) hey"
cath <## "bad chat command: feature not allowed Chat with admins"
-- alice can't send to cath's support (no existing chat)
alice ##> "#team (support: cath) hey"
alice <## "bad chat command: feature not allowed Chat with admins"
-- bob can still send (existing chat)
bob #> "#team (support) still here"
alice <# "#team (support: bob) bob> still here"
-- alice can still send to bob (existing chat)
alice #> "#team (support: bob) yes"
bob <# "#team (support) alice> yes"
testSupportPreferenceChannel :: HasCallStack => TestParams -> IO ()
testSupportPreferenceChannel ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "relay" relayProfile $ \relay ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
(shortLink, fullLink) <- prepareChannel1Relay "team" alice relay
memberJoinChannel "team" [relay] [alice] shortLink fullLink bob
memberJoinChannel "team" [relay] [alice] shortLink fullLink cath
threadDelay 1000000
alice ##> "/set support #team on"
alice <## "updated group preferences:"
alice <## "Chat with admins: on"
toggledSupport relay "alice" "team" "on"
concurrentlyN_
[ toggledSupport bob "alice" "team" "on",
toggledSupport cath "alice" "team" "on"
]
-- support enabled by default, bob sends to support
bob #> "#team (support) hello"
relay <# "#team (support: bob) bob> hello"
alice <# "#team (support: bob) bob> hello [>>]"
-- alice replies
alice #> "#team (support: bob) hi"
relay <# "#team (support: bob) alice> hi"
bob <# "#team (support) alice> hi [>>]"
-- alice disables support
threadDelay 1000000
alice ##> "/set support #team off"
alice <## "updated group preferences:"
alice <## "Chat with admins: off"
toggledSupport relay "alice" "team" "off"
concurrentlyN_
[ toggledSupport bob "alice" "team" "off",
toggledSupport cath "alice" "team" "off"
]
threadDelay 500000
-- cath can't send support (no existing chat)
cath ##> "#team (support) hey"
cath <## "bad chat command: feature not allowed Chat with admins"
alice ##> "#team (support: cath) hey too"
alice <## "bad chat command: feature not allowed Chat with admins"
-- bob can still send (existing chat)
bob #> "#team (support) still here"
concurrentlyN_
[ relay <# "#team (support: bob) bob> still here",
alice <# "#team (support: bob) bob> still here [>>]"
]
-- alice can still send to bob (existing chat)
alice #> "#team (support: bob) yes"
concurrentlyN_
[ relay <# "#team (support: bob) alice> yes",
bob <# "#team (support) alice> yes [>>]"
]
testChannels1RelayDeliver :: HasCallStack => TestParams -> IO ()
testChannels1RelayDeliver ps =
withNewTestChat ps "alice" aliceProfile $ \alice -> do
@@ -8858,7 +8976,7 @@ testChannelLinkAfterWelcomeUpdate ps =
shortLink' `shouldBe` shortLink
fullLink' `shouldBe` fullLink
memberJoinChannel "team" [bob] [alice] shortLink' fullLink' dan
dan #$> ("/_get chat #1 count=100", chat, groupFeaturesNoE2E <> [(0, "welcome to team"), (0, T.unpack publicGroupNoE2EText), (0, "connected")])
dan #$> ("/_get chat #1 count=100", chat, channelFeaturesNoE2E <> [(0, "welcome to team"), (0, T.unpack publicGroupNoE2EText), (0, "connected")])
alice #> "#team hi"
bob <# "#team> hi"
@@ -9568,6 +9686,45 @@ testChannelCreateDeletedRelay ps =
-- bob's agent reports AUTH error when the queue is gone — drain it.
void $ getTermLine bob
testChannelSupportScope :: HasCallStack => TestParams -> IO ()
testChannelSupportScope ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "relay" relayProfile $ \relay ->
withNewTestChat ps "cath" cathProfile $ \cath ->
withNewTestChat ps "dan" danProfile $ \dan -> do
(shortLink, fullLink) <- prepareChannel1Relay "team" alice relay
memberJoinChannel "team" [relay] [alice] shortLink fullLink cath
memberJoinChannel "team" [relay] [alice] shortLink fullLink dan
threadDelay 1000000
alice ##> "/set support #team on"
alice <## "updated group preferences:"
alice <## "Chat with admins: on"
toggledSupport relay "alice" "team" "on"
concurrentlyN_
[ toggledSupport cath "alice" "team" "on",
toggledSupport dan "alice" "team" "on"
]
-- owner sends to cath's support scope, dan doesn't receive
alice #> "#team (support: cath) hello"
relay <# "#team (support: cath) alice> hello"
cath <# "#team (support) alice> hello [>>]"
(dan </)
-- cath replies in support scope, dan doesn't receive
cath #> "#team (support) hi"
relay <# "#team (support: cath) cath> hi"
alice <# "#team (support: cath) cath> hi [>>]"
(dan </)
toggledSupport :: HasCallStack => TestCC -> String -> String -> String -> IO ()
toggledSupport c owner channel onOff = do
c <## (owner <> " updated group #" <> channel <> ": (signed)")
c <## "updated group preferences:"
c <## ("Chat with admins: " <> onOff)
testChannelMessageUpdate :: HasCallStack => TestParams -> IO ()
testChannelMessageUpdate ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
+11 -6
View File
@@ -290,7 +290,10 @@ groupFeatures :: [(Int, String)]
groupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 0
groupFeaturesNoE2E :: [(Int, String)]
groupFeaturesNoE2E = map (\(a, _, _) -> a) $ ((1, "chat banner"), Nothing, Nothing) : groupFeatures_ 0
groupFeaturesNoE2E = map (\(a, _, _) -> a) $ ((1, "chat banner"), Nothing, Nothing) : groupFeatures_ 0 False
channelFeaturesNoE2E :: [(Int, String)]
channelFeaturesNoE2E = map (\(a, _, _) -> a) $ ((1, "chat banner"), Nothing, Nothing) : groupFeatures_ 0 True
sndGroupFeatures :: [(Int, String)]
sndGroupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 1
@@ -299,10 +302,10 @@ groupFeatureStrs :: [String]
groupFeatureStrs = map (\(a, _, _) -> snd a) $ groupFeatures'' 0
groupFeatures'' :: Int -> [((Int, String), Maybe (Int, String), Maybe String)]
groupFeatures'' dir = ((1, "chat banner"), Nothing, Nothing) : ((dir, e2eeInfoNoPQStr), Nothing, Nothing) : groupFeatures_ dir
groupFeatures'' dir = ((1, "chat banner"), Nothing, Nothing) : ((dir, e2eeInfoNoPQStr), Nothing, Nothing) : groupFeatures_ dir False
groupFeatures_ :: Int -> [((Int, String), Maybe (Int, String), Maybe String)]
groupFeatures_ dir =
groupFeatures_ :: Int -> Bool -> [((Int, String), Maybe (Int, String), Maybe String)]
groupFeatures_ dir isChannel =
[ ((dir, "Disappearing messages: off"), Nothing, Nothing),
((dir, "Direct messages: on"), Nothing, Nothing),
((dir, "Full deletion: off"), Nothing, Nothing),
@@ -311,7 +314,8 @@ groupFeatures_ dir =
((dir, "Files and media: on"), Nothing, Nothing),
((dir, "SimpleX links: on"), Nothing, Nothing),
((dir, "Member reports: on"), Nothing, Nothing),
((dir, "Recent history: on"), Nothing, Nothing)
((dir, "Recent history: on"), Nothing, Nothing),
((dir, "Chat with admins: " <> (if isChannel then "off" else "on")), Nothing, Nothing)
]
businessGroupFeatures :: [(Int, String)]
@@ -329,7 +333,8 @@ businessGroupFeatures'' dir =
((dir, "Files and media: on"), Nothing, Nothing),
((dir, "SimpleX links: on"), Nothing, Nothing),
((dir, "Member reports: off"), Nothing, Nothing),
((dir, "Recent history: on"), Nothing, Nothing)
((dir, "Recent history: on"), Nothing, Nothing),
((dir, "Chat with admins: on"), Nothing, Nothing)
]
itemId :: Int -> String
+1 -1
View File
@@ -101,7 +101,7 @@ testChatPreferences :: Maybe Preferences
testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, files = Nothing, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}, sessions = Nothing, commands = Nothing}
testGroupPreferences :: Maybe GroupPreferences
testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, sessions = Nothing, comments = Nothing, commands = Nothing}
testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, commands = Nothing}
testProfile :: Profile
testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences}