ios: chat preferences, UI and types (#1360)

This commit is contained in:
Evgeny Poberezkin
2022-11-14 10:12:17 +00:00
committed by GitHub
parent 07e8c1d76e
commit faceeb6fce
7 changed files with 342 additions and 1 deletions

View File

@@ -99,6 +99,16 @@ struct ChatInfoView: View {
}
}
Section("Preferences") {
NavigationLink {
ContactPreferencesView()
.navigationBarTitle("Contact preferences")
.navigationBarTitleDisplayMode(.large)
} label: {
Text("Contact preferences")
}
}
Section("Servers") {
networkStatusRow()
.onTapGesture {

View File

@@ -0,0 +1,82 @@
//
// ContactPreferencesView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 13/11/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ContactPreferencesView: View {
@State var allowFullDeletion = ContactFeatureAllowed.yes
@State var allowVoice = ContactFeatureAllowed.yes
@State var prefs = ContactUserPreferences(
fullDelete: ContactUserPreference(
enabled: FeatureEnabled(forUser: true, forContact: true),
userPreference: .user(preference: Preference(allow: .yes)),
contactPreference: Preference(allow: .no)
),
voice: ContactUserPreference(
enabled: FeatureEnabled(forUser: true, forContact: true),
userPreference: .user(preference: Preference(allow: .yes)),
contactPreference: Preference(allow: .no)
)
)
var body: some View {
VStack {
List {
featureSection(.fullDelete, .yes, prefs.fullDelete, $allowFullDeletion)
featureSection(.voice, .yes, prefs.voice, $allowVoice)
Section {
HStack {
Text("Reset")
Spacer()
Text("Save")
}
.foregroundColor(.accentColor)
.disabled(true)
}
.listRowBackground(Color.clear)
}
}
}
private func featureSection(_ feature: Feature, _ userDefault: FeatureAllowed, _ pref: ContactUserPreference, _ allowFeature: Binding<ContactFeatureAllowed>) -> some View {
let enabled = FeatureEnabled.enabled(
user: Preference(allow: allowFeature.wrappedValue.allowed),
contact: pref.contactPreference
)
return Section {
Picker("You allow", selection: allowFeature) {
ForEach(ContactFeatureAllowed.values(userDefault)) { allow in
Text(allow.text)
}
}
.frame(height: 36)
HStack {
Text("Contact allows")
Spacer()
Text(pref.contactPreference.allow.text)
}
} header: {
HStack {
Image(systemName: "\(feature.icon).fill")
.foregroundColor(enabled.forUser ? .green : enabled.forContact ? .yellow : .red)
Text(feature.text)
}
} footer: {
Text(feature.enabledDescription(enabled))
.frame(height: 36, alignment: .topLeading)
}
}
}
struct ContactPreferencesView_Previews: PreviewProvider {
static var previews: some View {
ContactPreferencesView()
}
}

View File

@@ -158,7 +158,7 @@ struct DatabaseView: View {
Section {
Picker("Delete messages after", selection: $chatItemTTL) {
ForEach([ChatItemTTL.none, ChatItemTTL.month, ChatItemTTL.week, ChatItemTTL.day]) { ttl in
ForEach(ChatItemTTL.values) { ttl in
Text(ttl.deleteAfterText).tag(ttl)
}
if case .seconds = chatItemTTL {

View File

@@ -0,0 +1,57 @@
//
// PreferencesView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 13/11/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct PreferencesView: View {
@State var allowFullDeletion = FeatureAllowed.yes
@State var allowVoice = FeatureAllowed.yes
var body: some View {
VStack {
List {
featureSection(.fullDelete, $allowFullDeletion)
featureSection(.voice, $allowVoice)
Section {
HStack {
Text("Reset")
Spacer()
Text("Save")
}
.foregroundColor(.accentColor)
.disabled(true)
}
.listRowBackground(Color.clear)
}
}
}
private func featureSection(_ feature: Feature, _ allowFeature: Binding<FeatureAllowed>) -> some View {
Section {
settingsRow(feature.icon) {
Picker(feature.text, selection: allowFeature) {
ForEach(FeatureAllowed.values) { allow in
Text(allow.text)
}
}
.frame(height: 36)
}
} footer: {
Text(feature.allowDescription(allowFeature.wrappedValue))
.frame(height: 36, alignment: .topLeading)
}
}
}
struct PreferencesView_Previews: PreviewProvider {
static var previews: some View {
PreferencesView()
}
}

View File

@@ -120,6 +120,13 @@ struct SettingsView: View {
}
Section("Settings") {
NavigationLink {
PreferencesView()
.navigationTitle("Your preferences")
} label: {
settingsRow("list.bullet") { Text("Chat preferences") }
}
NavigationLink {
NotificationsView()
.navigationTitle("Notifications")

View File

@@ -68,6 +68,8 @@
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */; };
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; };
5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; };
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; };
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; };
5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */; };
5CB0BA8B2826CB3A00B3292C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA892826CB3A00B3292C /* Localizable.strings */; };
5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8D2827126500B3292C /* OnboardingView.swift */; };
@@ -267,6 +269,8 @@
5CA059D7279559F40002BEB4 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
5CA059DB279559F40002BEB4 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = "<group>"; };
5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOSLaunchTests.swift; sourceTree = "<group>"; };
5CADE79929211BB900072E13 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
5CADE79B292131E900072E13 /* ContactPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPreferencesView.swift; sourceTree = "<group>"; };
5CB0BA872826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5CB0BA8A2826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
5CB0BA8D2827126500B3292C /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
@@ -428,6 +432,7 @@
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */,
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */,
5CE4407127ADB1D0007B033A /* Emoji.swift */,
5CADE79B292131E900072E13 /* ContactPreferencesView.swift */,
);
path = Chat;
sourceTree = "<group>";
@@ -571,6 +576,7 @@
5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
5CADE79929211BB900072E13 /* PreferencesView.swift */,
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */,
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
@@ -956,6 +962,7 @@
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */,
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */,
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */,
@@ -978,6 +985,7 @@
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -117,6 +117,181 @@ extension NamedChat {
public typealias ChatId = String
public struct FullPreferences: Decodable {
public var fullDelete: Preference
public var voice: Preference
public init(fullDelete: Preference, voice: Preference) {
self.fullDelete = fullDelete
self.voice = voice
}
}
public struct Preference: Codable {
public var allow: FeatureAllowed
public init(allow: FeatureAllowed) {
self.allow = allow
}
}
public struct ContactUserPreferences: Decodable {
public var fullDelete: ContactUserPreference
public var voice: ContactUserPreference
public init(fullDelete: ContactUserPreference, voice: ContactUserPreference) {
self.fullDelete = fullDelete
self.voice = voice
}
}
public struct ContactUserPreference: Decodable {
public var enabled: FeatureEnabled
public var userPreference: ContactUserPref
public var contactPreference: Preference
public init(enabled: FeatureEnabled, userPreference: ContactUserPref, contactPreference: Preference) {
self.enabled = enabled
self.userPreference = userPreference
self.contactPreference = contactPreference
}
}
public struct FeatureEnabled: Decodable {
public var forUser: Bool
public var forContact: Bool
public init(forUser: Bool, forContact: Bool) {
self.forUser = forUser
self.forContact = forContact
}
public static func enabled(user: Preference, contact: Preference) -> FeatureEnabled {
switch (user.allow, contact.allow) {
case (.always, .no): return FeatureEnabled(forUser: false, forContact: true)
case (.no, .always): return FeatureEnabled(forUser: true, forContact: false)
case (_, .no): return FeatureEnabled(forUser: false, forContact: false)
case (.no, _): return FeatureEnabled(forUser: false, forContact: false)
default: return FeatureEnabled(forUser: true, forContact: true)
}
}
}
public enum ContactUserPref: Decodable {
case contact(preference: Preference) // contact override is set
case user(preference: Preference) // global user default is used
}
public enum Feature {
case fullDelete
case voice
public var values: [Feature] { [.fullDelete, .voice] }
public var id: Self { self }
public var text: LocalizedStringKey {
switch self {
case .fullDelete: return "Full deletion"
case .voice: return "Voice messages"
}
}
public var icon: String {
switch self {
case .fullDelete: return "trash.slash"
case .voice: return "speaker.wave.2"
}
}
public func allowDescription(_ allowed: FeatureAllowed) -> LocalizedStringKey {
switch self {
case .fullDelete:
switch allowed {
case .always: return "Allow your contacts to irreversibly delete sent messages."
case .yes: return "Allow irreversible message deletion only if your contact allows it to you."
case .no: return "Contacts can mark messages for deletion; you will be able to view them."
}
case .voice:
switch allowed {
case .always: return "Allow your contacts to send voice messages."
case .yes: return "Allow voice messages only if your contact allows them."
case .no: return "Prohibit sending voice messages."
}
}
}
public func enabledDescription(_ enabled: FeatureEnabled) -> LocalizedStringKey {
switch self {
case .fullDelete:
return enabled.forUser && enabled.forContact
? "Both you and your contact can irreversibly delete sent messages."
: enabled.forUser
? "Only you can irreversibly delete messages (your contact can mark them for deletion)."
: enabled.forContact
? "Only your contact can irreversibly delete messages (you can mark them for deletion)."
: "Irreversible message deletion is prohibited in this chat."
case .voice:
return enabled.forUser && enabled.forContact
? "Both you and your contact can send voice messages."
: enabled.forUser
? "Only you can send voice messages."
: enabled.forContact
? "Only your contact can send voice messages."
: "Voice messages are prohibited in this chat."
}
}
}
public enum ContactFeatureAllowed: Identifiable, Hashable {
case userDefault(FeatureAllowed)
case always
case yes
case no
public static func values(_ def: FeatureAllowed) -> [ContactFeatureAllowed] {
[.userDefault(def) , .always, .yes, .no]
}
public var id: Self { self }
public var allowed: FeatureAllowed {
switch self {
case let .userDefault(def): return def
case .always: return .always
case .yes: return .yes
case .no: return .no
}
}
public var text: String {
switch self {
case let .userDefault(def): return String.localizedStringWithFormat(NSLocalizedString("default (%@)", comment: "pref value"), def.text)
case .always: return NSLocalizedString("always", comment: "pref value")
case .yes: return NSLocalizedString("yes", comment: "pref value")
case .no: return NSLocalizedString("no", comment: "pref value")
}
}
}
public enum FeatureAllowed: String, Codable, Identifiable {
case always
case yes
case no
public static var values: [FeatureAllowed] { [.always, .yes, .no] }
public var id: Self { self }
public var text: String {
switch self {
case .always: return NSLocalizedString("always", comment: "pref value")
case .yes: return NSLocalizedString("yes", comment: "pref value")
case .no: return NSLocalizedString("no", comment: "pref value")
}
}
}
public enum ChatInfo: Identifiable, Decodable, NamedChat {
case direct(contact: Contact)
case group(groupInfo: GroupInfo)
@@ -1561,6 +1736,8 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable {
case seconds(_ seconds: Int64)
case none
public static var values: [ChatItemTTL] { [.none, .month, .week, .day] }
public var id: Self { self }
public init(_ seconds: Int64?) {