mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-27 04:15:45 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+13
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -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 {
|
||||
|
||||
+4
-4
@@ -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)
|
||||
|
||||
+13
-4
@@ -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)
|
||||
}
|
||||
|
||||
+12
-6
@@ -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) {
|
||||
|
||||
+30
-2
@@ -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
|
||||
|
||||
+4
-1
@@ -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 {
|
||||
|
||||
+4
-1
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=?)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user