mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
ios: role-based preferences, SimpleX links preference (#4021)
* ios: role-based preferences, SimpleX links preference * disable some options
This commit is contained in:
committed by
GitHub
parent
e10c63dc64
commit
182225a5b1
@@ -779,6 +779,24 @@ final class Chat: ObservableObject, Identifiable {
|
||||
|
||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||
|
||||
func groupFeatureEnabled(_ feature: GroupFeature) -> Bool {
|
||||
if case let .group(groupInfo) = self.chatInfo {
|
||||
let p = groupInfo.fullGroupPreferences
|
||||
return switch feature {
|
||||
case .timedMessages: p.timedMessages.on
|
||||
case .directMessages: p.directMessages.on(for: groupInfo.membership)
|
||||
case .fullDelete: p.fullDelete.on
|
||||
case .reactions: p.reactions.on
|
||||
case .voice: p.voice.on(for: groupInfo.membership)
|
||||
case .files: p.files.on(for: groupInfo.membership)
|
||||
case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership)
|
||||
case .history: p.history.on
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIChatFeatureView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
var feature: Feature
|
||||
@@ -18,7 +19,7 @@ struct CIChatFeatureView: View {
|
||||
var iconColor: Color
|
||||
|
||||
var body: some View {
|
||||
if !revealed, let fs = mergedFeautures() {
|
||||
if !revealed, let fs = mergedFeatures() {
|
||||
HStack {
|
||||
ForEach(fs, content: featureIconView)
|
||||
}
|
||||
@@ -47,7 +48,7 @@ struct CIChatFeatureView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func mergedFeautures() -> [FeatureInfo]? {
|
||||
private func mergedFeatures() -> [FeatureInfo]? {
|
||||
var fs: [FeatureInfo] = []
|
||||
var icons: Set<String> = []
|
||||
if var i = m.getChatItemIndex(chatItem) {
|
||||
@@ -67,8 +68,8 @@ struct CIChatFeatureView: View {
|
||||
switch ci.content {
|
||||
case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param)
|
||||
case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param)
|
||||
case let .rcvGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param)
|
||||
case let .sndGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param)
|
||||
case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param)
|
||||
case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
@@ -103,6 +104,6 @@ struct CIChatFeatureView: View {
|
||||
struct CIChatFeatureView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let enabled = FeatureEnabled(forUser: false, forContact: false)
|
||||
CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
|
||||
CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,9 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case let .rcvChatPreference(feature, allowed, param):
|
||||
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
||||
case let .sndChatPreference(feature, _, _):
|
||||
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary)
|
||||
case let .rcvGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
|
||||
case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary)
|
||||
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor)
|
||||
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor)
|
||||
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case .sndModerated: deletedItemView()
|
||||
@@ -149,7 +149,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
||||
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
|
||||
}
|
||||
|
||||
private var mergedGroupEventText: Text? {
|
||||
|
||||
@@ -161,6 +161,16 @@ struct ComposeState {
|
||||
}
|
||||
}
|
||||
|
||||
var attachmentPreview: Bool {
|
||||
switch preview {
|
||||
case .noPreview: false
|
||||
case .linkPreview: false
|
||||
case let .mediaPreviews(mediaPreviews): !mediaPreviews.isEmpty
|
||||
case .voicePreview: false
|
||||
case .filePreview: true
|
||||
}
|
||||
}
|
||||
|
||||
var empty: Bool {
|
||||
message == "" && noPreview
|
||||
}
|
||||
@@ -234,6 +244,7 @@ struct ComposeView: View {
|
||||
@Binding var keyboardVisible: Bool
|
||||
|
||||
@State var linkUrl: URL? = nil
|
||||
@State var hasSimplexLink: Bool = false
|
||||
@State var prevLinkUrl: URL? = nil
|
||||
@State var pendingLinkUrl: URL? = nil
|
||||
@State var cancelledLinks: Set<String> = []
|
||||
@@ -260,6 +271,16 @@ struct ComposeView: View {
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
ContextInvitingContactMemberView()
|
||||
}
|
||||
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
||||
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
|
||||
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
|
||||
if simplexLinkProhibited {
|
||||
msgNotAllowedView("SimpleX links not allowed", icon: "link")
|
||||
} else if fileProhibited {
|
||||
msgNotAllowedView("Files and media not allowed", icon: "doc")
|
||||
} else if voiceProhibited {
|
||||
msgNotAllowedView("Voice messages not allowed", icon: "mic")
|
||||
}
|
||||
contextItemView()
|
||||
switch (composeState.editing, composeState.preview) {
|
||||
case (true, .filePreview): EmptyView()
|
||||
@@ -278,7 +299,7 @@ struct ComposeView: View {
|
||||
.padding(.bottom, 12)
|
||||
.padding(.leading, 12)
|
||||
if case let .group(g) = chat.chatInfo,
|
||||
!g.fullGroupPreferences.files.on {
|
||||
!g.fullGroupPreferences.files.on(for: g.membership) {
|
||||
b.disabled(true).onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Files and media prohibited!",
|
||||
@@ -303,6 +324,7 @@ struct ComposeView: View {
|
||||
},
|
||||
nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false,
|
||||
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
||||
disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited,
|
||||
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
||||
startVoiceMessageRecording: {
|
||||
Task {
|
||||
@@ -337,13 +359,18 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: composeState.message) { _ in
|
||||
.onChange(of: composeState.message) { msg in
|
||||
if composeState.linkPreviewAllowed {
|
||||
if composeState.message.count > 0 {
|
||||
showLinkPreview(composeState.message)
|
||||
if msg.count > 0 {
|
||||
showLinkPreview(msg)
|
||||
} else {
|
||||
resetLinkPreview()
|
||||
hasSimplexLink = false
|
||||
}
|
||||
} else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) {
|
||||
(_, hasSimplexLink) = parseMessage(msg)
|
||||
} else {
|
||||
hasSimplexLink = false
|
||||
}
|
||||
}
|
||||
.onChange(of: chat.userCanSend) { canSend in
|
||||
@@ -610,6 +637,18 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func msgNotAllowedView(_ reason: LocalizedStringKey, icon: String) -> some View {
|
||||
HStack {
|
||||
Image(systemName: icon).foregroundColor(.secondary)
|
||||
Text(reason).italic()
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@ViewBuilder private func contextItemView() -> some View {
|
||||
switch composeState.contextItem {
|
||||
case .noContextItem:
|
||||
@@ -818,7 +857,7 @@ struct ComposeView: View {
|
||||
func checkLinkPreview() -> MsgContent {
|
||||
switch (composeState.preview) {
|
||||
case let .linkPreview(linkPreview: linkPreview):
|
||||
if let url = parseMessage(msgText),
|
||||
if let url = parseMessage(msgText).url,
|
||||
let linkPreview = linkPreview,
|
||||
url == linkPreview.uri {
|
||||
return .link(text: msgText, preview: linkPreview)
|
||||
@@ -947,7 +986,7 @@ struct ComposeView: View {
|
||||
|
||||
private func showLinkPreview(_ s: String) {
|
||||
prevLinkUrl = linkUrl
|
||||
linkUrl = parseMessage(s)
|
||||
(linkUrl, hasSimplexLink) = parseMessage(s)
|
||||
if let url = linkUrl {
|
||||
if url != composeState.linkPreview?.uri && url != pendingLinkUrl {
|
||||
pendingLinkUrl = url
|
||||
@@ -964,13 +1003,17 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func parseMessage(_ msg: String) -> URL? {
|
||||
let parsedMsg = parseSimpleXMarkdown(msg)
|
||||
let uri = parsedMsg?.first(where: { ft in
|
||||
private func parseMessage(_ msg: String) -> (url: URL?, hasSimplexLink: Bool) {
|
||||
guard let parsedMsg = parseSimpleXMarkdown(msg) else { return (nil, false) }
|
||||
let url: URL? = if let uri = parsedMsg.first(where: { ft in
|
||||
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
|
||||
})
|
||||
if let uri = uri { return URL(string: uri.text) }
|
||||
else { return nil }
|
||||
}) {
|
||||
URL(string: uri.text)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
|
||||
return (url, simplexLink)
|
||||
}
|
||||
|
||||
private func isSimplexLink(_ link: String) -> Bool {
|
||||
|
||||
@@ -20,6 +20,7 @@ struct SendMessageView: View {
|
||||
var nextSendGrpInv: Bool = false
|
||||
var showVoiceMessageButton: Bool = true
|
||||
var voiceMessageAllowed: Bool = true
|
||||
var disableSendButton = false
|
||||
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
|
||||
var startVoiceMessageRecording: (() -> Void)? = nil
|
||||
var finishVoiceMessageRecording: (() -> Void)? = nil
|
||||
@@ -184,7 +185,8 @@ struct SendMessageView: View {
|
||||
!composeState.sendEnabled ||
|
||||
composeState.inProgress ||
|
||||
(!voiceMessageAllowed && composeState.voicePreview) ||
|
||||
composeState.endLiveDisabled
|
||||
composeState.endLiveDisabled ||
|
||||
disableSendButton
|
||||
)
|
||||
.frame(width: 29, height: 29)
|
||||
.contextMenu{
|
||||
|
||||
@@ -83,7 +83,7 @@ struct GroupMemberInfoView: View {
|
||||
Section {
|
||||
if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) {
|
||||
knownDirectChatButton(chat)
|
||||
} else if groupInfo.fullGroupPreferences.directMessages.on {
|
||||
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
||||
if let contactId = member.memberContactId {
|
||||
newDirectChatButton(contactId)
|
||||
} else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false {
|
||||
@@ -110,7 +110,7 @@ struct GroupMemberInfoView: View {
|
||||
Label("Share address", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
if let contactId = member.memberContactId {
|
||||
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on {
|
||||
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
||||
connectViaAddressButton(contactLink)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let featureRoles: [(role: GroupMemberRole?, text: LocalizedStringKey)] = [
|
||||
(nil, "all members"),
|
||||
(.admin, "admins"),
|
||||
(.owner, "owners")
|
||||
]
|
||||
|
||||
struct GroupPreferencesView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@@ -24,10 +30,12 @@ struct GroupPreferencesView: View {
|
||||
List {
|
||||
featureSection(.timedMessages, $preferences.timedMessages.enable)
|
||||
featureSection(.fullDelete, $preferences.fullDelete.enable)
|
||||
featureSection(.directMessages, $preferences.directMessages.enable)
|
||||
featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role)
|
||||
featureSection(.reactions, $preferences.reactions.enable)
|
||||
featureSection(.voice, $preferences.voice.enable)
|
||||
featureSection(.files, $preferences.files.enable)
|
||||
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
|
||||
featureSection(.files, $preferences.files.enable, $preferences.files.role)
|
||||
// TODO enable simplexLinks preference in 5.8
|
||||
// featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||
featureSection(.history, $preferences.history.enable)
|
||||
|
||||
if groupInfo.canEdit {
|
||||
@@ -64,7 +72,7 @@ struct GroupPreferencesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>) -> some View {
|
||||
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil) -> some View {
|
||||
Section {
|
||||
let color: Color = enableFeature.wrappedValue == .on ? .green : .secondary
|
||||
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
|
||||
@@ -87,6 +95,16 @@ struct GroupPreferencesView: View {
|
||||
)
|
||||
.frame(height: 36)
|
||||
}
|
||||
if enableFeature.wrappedValue == .on, let enableForRole {
|
||||
Picker("Enabled for", selection: enableForRole) {
|
||||
ForEach(featureRoles, id: \.role) { fr in
|
||||
Text(fr.text)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
// remove in v5.8
|
||||
.disabled(true)
|
||||
}
|
||||
} else {
|
||||
settingsRow(icon, color: color) {
|
||||
infoRow(Text(feature.text), enableFeature.wrappedValue.text)
|
||||
@@ -94,10 +112,26 @@ struct GroupPreferencesView: View {
|
||||
if timedOn {
|
||||
infoRow("Delete after", timeText(preferences.timedMessages.ttl))
|
||||
}
|
||||
if enableFeature.wrappedValue == .on, let enableForRole {
|
||||
HStack {
|
||||
Text("Enabled for").foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(
|
||||
featureRoles.first(where: { fr in fr.role == enableForRole.wrappedValue })?.text
|
||||
?? "all members"
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
|
||||
}
|
||||
.onChange(of: enableFeature.wrappedValue) { enabled in
|
||||
if case .off = enabled {
|
||||
enableForRole?.wrappedValue = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func savePreferences() {
|
||||
|
||||
@@ -543,6 +543,7 @@ public protocol Feature {
|
||||
var iconFilled: String { get }
|
||||
var iconScale: CGFloat { get }
|
||||
var hasParam: Bool { get }
|
||||
var hasRole: Bool { get }
|
||||
var text: String { get }
|
||||
}
|
||||
|
||||
@@ -569,6 +570,8 @@ public enum ChatFeature: String, Decodable, Feature {
|
||||
}
|
||||
}
|
||||
|
||||
public var hasRole: Bool { false }
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature")
|
||||
@@ -694,6 +697,7 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case reactions
|
||||
case voice
|
||||
case files
|
||||
case simplexLinks
|
||||
case history
|
||||
|
||||
public var id: Self { self }
|
||||
@@ -705,6 +709,19 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
}
|
||||
}
|
||||
|
||||
public var hasRole: Bool {
|
||||
switch self {
|
||||
case .timedMessages: false
|
||||
case .directMessages: true
|
||||
case .fullDelete: false
|
||||
case .reactions: false
|
||||
case .voice: true
|
||||
case .files: true
|
||||
case .simplexLinks: true
|
||||
case .history: false
|
||||
}
|
||||
}
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature")
|
||||
@@ -713,6 +730,7 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature")
|
||||
case .voice: return NSLocalizedString("Voice messages", comment: "chat feature")
|
||||
case .files: return NSLocalizedString("Files and media", comment: "chat feature")
|
||||
case .simplexLinks: return NSLocalizedString("SimpleX links", comment: "chat feature")
|
||||
case .history: return NSLocalizedString("Visible history", comment: "chat feature")
|
||||
}
|
||||
}
|
||||
@@ -725,6 +743,7 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .reactions: return "face.smiling"
|
||||
case .voice: return "mic"
|
||||
case .files: return "doc"
|
||||
case .simplexLinks: return "link.circle"
|
||||
case .history: return "clock"
|
||||
}
|
||||
}
|
||||
@@ -737,6 +756,7 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .reactions: return "face.smiling.fill"
|
||||
case .voice: return "mic.fill"
|
||||
case .files: return "doc.fill"
|
||||
case .simplexLinks: return "link.circle.fill"
|
||||
case .history: return "clock.fill"
|
||||
}
|
||||
}
|
||||
@@ -781,6 +801,11 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .on: return "Allow to send files and media."
|
||||
case .off: return "Prohibit sending files and media."
|
||||
}
|
||||
case .simplexLinks:
|
||||
switch enabled {
|
||||
case .on: return "Allow to send SimpleX links."
|
||||
case .off: return "Prohibit sending SimpleX links."
|
||||
}
|
||||
case .history:
|
||||
switch enabled {
|
||||
case .on: return "Send up to 100 last messages to new members."
|
||||
@@ -819,6 +844,11 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .on: return "Group members can send files and media."
|
||||
case .off: return "Files and media are prohibited in this group."
|
||||
}
|
||||
case .simplexLinks:
|
||||
switch enabled {
|
||||
case .on: return "Group members can send SimpleX links."
|
||||
case .off: return "SimpleX links are prohibited in this group."
|
||||
}
|
||||
case .history:
|
||||
switch enabled {
|
||||
case .on: return "Up to 100 last messages are sent to new members."
|
||||
@@ -958,20 +988,22 @@ public enum FeatureAllowed: String, Codable, Identifiable {
|
||||
|
||||
public struct FullGroupPreferences: Decodable, Equatable {
|
||||
public var timedMessages: TimedMessagesGroupPreference
|
||||
public var directMessages: GroupPreference
|
||||
public var directMessages: RoleGroupPreference
|
||||
public var fullDelete: GroupPreference
|
||||
public var reactions: GroupPreference
|
||||
public var voice: GroupPreference
|
||||
public var files: GroupPreference
|
||||
public var voice: RoleGroupPreference
|
||||
public var files: RoleGroupPreference
|
||||
public var simplexLinks: RoleGroupPreference
|
||||
public var history: GroupPreference
|
||||
|
||||
public init(
|
||||
timedMessages: TimedMessagesGroupPreference,
|
||||
directMessages: GroupPreference,
|
||||
directMessages: RoleGroupPreference,
|
||||
fullDelete: GroupPreference,
|
||||
reactions: GroupPreference,
|
||||
voice: GroupPreference,
|
||||
files: GroupPreference,
|
||||
voice: RoleGroupPreference,
|
||||
files: RoleGroupPreference,
|
||||
simplexLinks: RoleGroupPreference,
|
||||
history: GroupPreference
|
||||
) {
|
||||
self.timedMessages = timedMessages
|
||||
@@ -980,36 +1012,40 @@ public struct FullGroupPreferences: Decodable, Equatable {
|
||||
self.reactions = reactions
|
||||
self.voice = voice
|
||||
self.files = files
|
||||
self.simplexLinks = simplexLinks
|
||||
self.history = history
|
||||
}
|
||||
|
||||
public static let sampleData = FullGroupPreferences(
|
||||
timedMessages: TimedMessagesGroupPreference(enable: .off),
|
||||
directMessages: GroupPreference(enable: .off),
|
||||
directMessages: RoleGroupPreference(enable: .off, role: nil),
|
||||
fullDelete: GroupPreference(enable: .off),
|
||||
reactions: GroupPreference(enable: .on),
|
||||
voice: GroupPreference(enable: .on),
|
||||
files: GroupPreference(enable: .on),
|
||||
voice: RoleGroupPreference(enable: .on, role: nil),
|
||||
files: RoleGroupPreference(enable: .on, role: nil),
|
||||
simplexLinks: RoleGroupPreference(enable: .on, role: nil),
|
||||
history: GroupPreference(enable: .on)
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupPreferences: Codable {
|
||||
public var timedMessages: TimedMessagesGroupPreference?
|
||||
public var directMessages: GroupPreference?
|
||||
public var directMessages: RoleGroupPreference?
|
||||
public var fullDelete: GroupPreference?
|
||||
public var reactions: GroupPreference?
|
||||
public var voice: GroupPreference?
|
||||
public var files: GroupPreference?
|
||||
public var voice: RoleGroupPreference?
|
||||
public var files: RoleGroupPreference?
|
||||
public var simplexLinks: RoleGroupPreference?
|
||||
public var history: GroupPreference?
|
||||
|
||||
public init(
|
||||
timedMessages: TimedMessagesGroupPreference? = nil,
|
||||
directMessages: GroupPreference? = nil,
|
||||
directMessages: RoleGroupPreference? = nil,
|
||||
fullDelete: GroupPreference? = nil,
|
||||
reactions: GroupPreference? = nil,
|
||||
voice: GroupPreference? = nil,
|
||||
files: GroupPreference? = nil,
|
||||
voice: RoleGroupPreference? = nil,
|
||||
files: RoleGroupPreference? = nil,
|
||||
simplexLinks: RoleGroupPreference? = nil,
|
||||
history: GroupPreference? = nil
|
||||
) {
|
||||
self.timedMessages = timedMessages
|
||||
@@ -1018,16 +1054,18 @@ public struct GroupPreferences: Codable {
|
||||
self.reactions = reactions
|
||||
self.voice = voice
|
||||
self.files = files
|
||||
self.simplexLinks = simplexLinks
|
||||
self.history = history
|
||||
}
|
||||
|
||||
public static let sampleData = GroupPreferences(
|
||||
timedMessages: TimedMessagesGroupPreference(enable: .off),
|
||||
directMessages: GroupPreference(enable: .off),
|
||||
directMessages: RoleGroupPreference(enable: .off, role: nil),
|
||||
fullDelete: GroupPreference(enable: .off),
|
||||
reactions: GroupPreference(enable: .on),
|
||||
voice: GroupPreference(enable: .on),
|
||||
files: GroupPreference(enable: .on),
|
||||
voice: RoleGroupPreference(enable: .on, role: nil),
|
||||
files: RoleGroupPreference(enable: .on, role: nil),
|
||||
simplexLinks: RoleGroupPreference(enable: .on, role: nil),
|
||||
history: GroupPreference(enable: .on)
|
||||
)
|
||||
}
|
||||
@@ -1040,6 +1078,7 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group
|
||||
reactions: fullPreferences.reactions,
|
||||
voice: fullPreferences.voice,
|
||||
files: fullPreferences.files,
|
||||
simplexLinks: fullPreferences.simplexLinks,
|
||||
history: fullPreferences.history
|
||||
)
|
||||
}
|
||||
@@ -1051,11 +1090,37 @@ public struct GroupPreference: Codable, Equatable {
|
||||
enable == .on
|
||||
}
|
||||
|
||||
public func enabled(_ role: GroupMemberRole?, for m: GroupMember?) -> GroupFeatureEnabled {
|
||||
switch enable {
|
||||
case .off: .off
|
||||
case .on:
|
||||
if let role, let m {
|
||||
m.memberRole >= role ? .on : .off
|
||||
} else {
|
||||
.on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(enable: GroupFeatureEnabled) {
|
||||
self.enable = enable
|
||||
}
|
||||
}
|
||||
|
||||
public struct RoleGroupPreference: Codable, Equatable {
|
||||
public var enable: GroupFeatureEnabled
|
||||
public var role: GroupMemberRole?
|
||||
|
||||
public func on(for m: GroupMember) -> Bool {
|
||||
enable == .on && m.memberRole >= (role ?? .observer)
|
||||
}
|
||||
|
||||
public init(enable: GroupFeatureEnabled, role: GroupMemberRole?) {
|
||||
self.enable = enable
|
||||
self.role = role
|
||||
}
|
||||
}
|
||||
|
||||
public struct TimedMessagesGroupPreference: Codable, Equatable {
|
||||
public var enable: GroupFeatureEnabled
|
||||
public var ttl: Int?
|
||||
@@ -1280,7 +1345,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case .timedMessages: return prefs.timedMessages.on
|
||||
case .fullDelete: return prefs.fullDelete.on
|
||||
case .reactions: return prefs.reactions.on
|
||||
case .voice: return prefs.voice.on
|
||||
case .voice: return prefs.voice.on(for: groupInfo.membership)
|
||||
case .calls: return false
|
||||
}
|
||||
case .local:
|
||||
@@ -1323,7 +1388,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
return .other
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
if !groupInfo.fullGroupPreferences.voice.on {
|
||||
if !groupInfo.fullGroupPreferences.voice.on(for: groupInfo.membership) {
|
||||
return .groupOwnerCan
|
||||
} else {
|
||||
return .other
|
||||
@@ -1975,7 +2040,7 @@ public struct GroupMemberIds: Decodable {
|
||||
var groupId: Int64
|
||||
}
|
||||
|
||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
|
||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable {
|
||||
case observer = "observer"
|
||||
case author = "author"
|
||||
case member = "member"
|
||||
@@ -2759,8 +2824,8 @@ public enum CIContent: Decodable, ItemContent {
|
||||
case sndChatFeature(feature: ChatFeature, enabled: FeatureEnabled, param: Int?)
|
||||
case rcvChatPreference(feature: ChatFeature, allowed: FeatureAllowed, param: Int?)
|
||||
case sndChatPreference(feature: ChatFeature, allowed: FeatureAllowed, param: Int?)
|
||||
case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?)
|
||||
case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?)
|
||||
case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?, memberRole_: GroupMemberRole?)
|
||||
case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?, memberRole_: GroupMemberRole?)
|
||||
case rcvChatFeatureRejected(feature: ChatFeature)
|
||||
case rcvGroupFeatureRejected(groupFeature: GroupFeature)
|
||||
case sndModerated
|
||||
@@ -2794,8 +2859,8 @@ public enum CIContent: Decodable, ItemContent {
|
||||
case let .sndChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param)
|
||||
case let .rcvChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param)
|
||||
case let .sndChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param)
|
||||
case let .rcvGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, param)
|
||||
case let .sndGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, param)
|
||||
case let .rcvGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role)
|
||||
case let .sndGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role)
|
||||
case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text)
|
||||
case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text)
|
||||
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||
@@ -2820,10 +2885,25 @@ public enum CIContent: Decodable, ItemContent {
|
||||
NSLocalizedString("This chat is protected by end-to-end encryption.", comment: "E2EE info chat item")
|
||||
}
|
||||
|
||||
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
|
||||
feature.hasParam
|
||||
? "\(feature.text): \(timeText(param))"
|
||||
: "\(feature.text): \(enabled)"
|
||||
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?, _ role: GroupMemberRole? = nil) -> String {
|
||||
(
|
||||
feature.hasParam
|
||||
? "\(feature.text): \(timeText(param))"
|
||||
: "\(feature.text): \(enabled)"
|
||||
) +
|
||||
(
|
||||
feature.hasRole && role != nil
|
||||
? " (\(roleText(role)))"
|
||||
: ""
|
||||
)
|
||||
}
|
||||
|
||||
private static func roleText(_ role: GroupMemberRole?) -> String {
|
||||
switch role {
|
||||
case .owner: NSLocalizedString("owners", comment: "feature role")
|
||||
case .admin: NSLocalizedString("admins", comment: "feature role")
|
||||
default: NSLocalizedString("all members", comment: "feature role")
|
||||
}
|
||||
}
|
||||
|
||||
public static func preferenceText(_ feature: Feature, _ allowed: FeatureAllowed, _ param: Int?) -> String {
|
||||
|
||||
Reference in New Issue
Block a user