From c547302b36a71909630eb6bb97bc1876cf52c4ea Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:11:17 +0400 Subject: [PATCH] wip --- .../NetworkAndServers/OperatorView.swift | 157 ++++++++++++++++++ .../ProtocolServersView.swift | 30 ++++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + apps/ios/SimpleXChat/APITypes.swift | 66 ++++++++ 4 files changed, 257 insertions(+) create mode 100644 apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift new file mode 100644 index 0000000000..6be5a24406 --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -0,0 +1,157 @@ +// +// OperatorView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.10.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +import SwiftUI +import SimpleXChat + +struct OperatorView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + let serverProtocol: ServerProtocol + @Binding var serverOperator: ServerOperator // Binding + + var servers: [ServerCfg] // State / Binding + + var proto: String { serverProtocol.rawValue.uppercased() } + + var body: some View { + return VStack { + List { + Section(header: Text("Operator").foregroundColor(theme.colors.secondary)) { + Text(serverOperator.name) + infoViewLink() + } + + useOperatorSection() + } + } + } + + private func infoViewLink() -> some View { + NavigationLink() { + OperatorInfoView(serverOperator: serverOperator) + .navigationBarTitle("Operator information") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + Text("Information") + } + } + + private func useOperatorSection() -> some View { + Section(header: Text("Use operator").foregroundColor(theme.colors.secondary)) { + conditionsViewLink() + if let reviewDeadline = serverOperator.latestConditionsAcceptance.reviewDeadline { + infoRow("Review until", deadlineTimestamp(reviewDeadline)) + } + Toggle("Use operator", isOn: $serverOperator.enabled) + .disabled(!serverOperator.latestConditionsAcceptance.usageAllowed) + .foregroundColor(!serverOperator.latestConditionsAcceptance.usageAllowed ? theme.colors.secondary : theme.colors.onBackground) + Group { + Toggle("for storage", isOn: $serverOperator.roles.storage) + Toggle("as proxy", isOn: $serverOperator.roles.proxy) + } + .padding(.leading, 24) + .disabled(!serverOperator.enabled) + .foregroundColor(!serverOperator.enabled ? theme.colors.secondary : theme.colors.onBackground) + } + } + + private func deadlineTimestamp(_ date: Date) -> String { + let localDateFormatter = DateFormatter() + localDateFormatter.dateStyle = .medium + localDateFormatter.timeStyle = .none + return localDateFormatter.string(from: date) + } + + @ViewBuilder private func conditionsViewLink() -> some View { + NavigationLink() { + UsageConditionsView(serverOperator: $serverOperator) + .navigationBarTitle("Conditions of use") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + if case .accepted = serverOperator.latestConditionsAcceptance { + Text("Conditions accepted") + } else { + Text("Review conditions") + } + } + } +} + +struct OperatorInfoView: View { + @EnvironmentObject var theme: AppTheme + var serverOperator: ServerOperator + + var body: some View { + return VStack { + List { + Section(header: Text("Description").foregroundColor(theme.colors.secondary)) { + Text(serverOperator.info.description) + } + Section(header: Text("Website").foregroundColor(theme.colors.secondary)) { + Link("\(serverOperator.info.website)", destination: URL(string: serverOperator.info.website)!) + } + } + } + } +} + +struct UsageConditionsView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @Binding var serverOperator: ServerOperator + + let conditionsText = """ + Lorem ipsum odor amet, consectetuer adipiscing elit. Blandit mauris massa tempor ac; maximus accumsan magnis. Sollicitudin maximus tempor luctus sociosqu turpis dictum per imperdiet porttitor. Efficitur mattis fusce curae id efficitur. Non bibendum elementum faucibus vehicula morbi pulvinar. Accumsan habitant tincidunt sollicitudin taciti ad urna potenti velit. Primis laoreet pharetra magnis est dolor proin viverra. + + Laoreet auctor morbi a varius rutrum diam porta? In ad erat condimentum erat leo ornare. Eu venenatis inceptos rhoncus urna fringilla dis proin ante. Cras dignissim rutrum et faucibus feugiat neque curae tempus. Tellus ligula id dapibus, diam sollicitudin velit odio aliquam lectus. Maecenas ullamcorper arcu interdum cubilia donec iaculis. Maximus penatibus turpis a; vel fermentum ridiculus magna phasellus pellentesque. Eros tellus libero varius potenti; lobortis iaculis. + + Mollis condimentum potenti velit at rutrum tellus maximus suscipit nec. Vehicula aenean dui netus enim aliquam. Aliquam libero rhoncus per pharetra accumsan eros. Urna non eu sem varius vivamus mus tellus aptent quam. Tristique mi natoque lectus volutpat facilisi commodo ac consequat. Proin parturient facilisi senectus egestas ultrices. Fringilla nisi urna convallis molestie lorem varius phasellus a ornare. Ullamcorper varius praesent facilisi habitasse massa. + + Potenti dolor ridiculus est faucibus leo. Euismod consequat ultricies fringilla sociosqu duis sollicitudin. Eget convallis lacinia lacus justo per habitasse parturient. Donec nunc himenaeos pretium donec cursus pharetra ac phasellus? Fringilla sodales egestas orci ligula per ligula semper pellentesque. Potenti non dignissim tempor; orci rutrum elit. + + Habitasse eu sapien eleifend gravida tortor potenti senectus euismod. Lectus enim fames turpis lectus facilisi efficitur elit porttitor facilisi. Nisl quam senectus quam augue integer leo. In aliquam tempor nibh proin felis tortor elementum sodales lacinia. Ut per placerat bibendum magna dapibus fermentum bibendum amet congue. Curae bibendum enim platea per faucibus imperdiet morbi hac varius. Conubia feugiat justo hac faucibus dis. + """ + + var body: some View { + return VStack { + List { + Section { + Text(conditionsText) + } + + Section { + if case .accepted = serverOperator.latestConditionsAcceptance { + Text("Conditions accepted") + } else { + Button { + // Should call api to save state here, not when saving all servers + // (It's counterintuitive to lose to closed sheet or Reset) + serverOperator.latestConditionsAcceptance = .accepted + dismiss() + } label: { + Text("Accept conditions") + } + } + } + } + } + } +} + +#Preview { + OperatorView( + serverProtocol: .smp, + serverOperator: Binding.constant(ServerOperator.sampleData1), + servers: [ServerCfg.sampleData.preset] + ) +} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift index 0fb37d5c49..073752a941 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift @@ -54,6 +54,8 @@ struct ProtocolServersView: View { private func protocolServersView() -> some View { List { + operatorsSection() + if !configuredServers.isEmpty { Section { ForEach($configuredServers) { srv in @@ -175,6 +177,34 @@ struct ProtocolServersView: View { } } + @ViewBuilder private func operatorsSection() -> some View { + let operator1 = ServerOperator.sampleData1 + let operator2 = ServerOperator.sampleData2 + let servers = [ServerCfg.sampleData.preset, ServerCfg.sampleData.untested] + Section { + NavigationLink() { + OperatorView(serverProtocol: .smp, serverOperator: Binding.constant(operator1), servers: servers) + .navigationBarTitle("\(operator1.name) servers") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + Text(operator1.name) + } + + NavigationLink() { + OperatorView(serverProtocol: .smp, serverOperator: Binding.constant(operator2), servers: servers) + .navigationBarTitle("\(operator2.name) servers") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + Text(operator2.name) + } + } header: { + Text("Operators") + .foregroundColor(theme.colors.secondary) + } + } + private func partitionServers(_ servers: [ServerCfg]) { configuredServers = servers.filter { $0.preset || $0.enabled } otherServers = servers.filter { !($0.preset || $0.enabled) } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 7aaa439adb..4c56b00fcf 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -153,6 +153,7 @@ 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; }; 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */; }; 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; }; + 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; @@ -496,6 +497,7 @@ 643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = ""; }; 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; sourceTree = ""; }; 643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = ""; }; + 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; }; @@ -1058,6 +1060,7 @@ 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, + 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */, 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, ); @@ -1537,6 +1540,7 @@ 18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */, 18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */, 184158C131FDB829D8A117EA /* VideoPlayerView.swift in Sources */, + 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 3c9b91fa0b..a03123173b 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1194,8 +1194,74 @@ public struct UserProtoServers: Decodable { public var presetServers: [ServerCfg] } +public enum UsageConditionsAcceptance: Decodable, Hashable { + case accepted + case reviewAvailable(deadline: Date) + case reviewRequired + + public var usageAllowed: Bool { + switch self { + case .accepted: true + case .reviewAvailable: true + case .reviewRequired: false + } + } + + public var reviewDeadline: Date? { + switch self { + case .accepted: nil + case let .reviewAvailable(deadline): deadline + case .reviewRequired: nil + } + } +} + +public struct ServerOperator: Decodable { + public var operatorId: Int64 + public var name: String + public var info: ServerOperatorInfo + public var latestConditionsAcceptance: UsageConditionsAcceptance + public var enabled: Bool + public var roles: ServerRoles + + public static var sampleData1 = ServerOperator( + operatorId: 1, + name: "SimpleX Chat", + info: ServerOperatorInfo( + description: "SimpleX Chat preset servers", + website: "https://simplex.chat" + ), + latestConditionsAcceptance: .reviewAvailable(deadline: Date.distantFuture), + enabled: true, + roles: ServerRoles(storage: true, proxy: true) + ) + + public static var sampleData2 = ServerOperator( + operatorId: 2, + name: "XYZ", + info: ServerOperatorInfo( + description: "XYZ servers", + website: "https://xyz.com" + ), + latestConditionsAcceptance: .reviewRequired, + enabled: false, + roles: ServerRoles(storage: true, proxy: true) + ) +} + +public struct ServerOperatorInfo: Decodable { + public var description: String + public var website: String +} + +public struct ServerRoles: Decodable { + public var storage: Bool + public var proxy: Bool +} + public struct ServerCfg: Identifiable, Equatable, Codable, Hashable { public var server: String + public var operatorId: Int64? public var preset: Bool public var tested: Bool? public var enabled: Bool