// // ProtocolServersView.swift // SimpleX (iOS) // // Created by Evgeny on 15/11/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // import SwiftUI import SimpleXChat private let howToUrl = URL(string: "https://simplex.chat/docs/server.html")! struct ProtocolServersView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var m: ChatModel @EnvironmentObject var theme: AppTheme @Environment(\.editMode) private var editMode let serverProtocol: ServerProtocol @State private var currServers: [ServerCfg] = [] @State private var presetServers: [ServerCfg] = [] @State private var configuredServers: [ServerCfg] = [] @State private var otherServers: [ServerCfg] = [] @State private var selectedServer: String? = nil @State private var showAddServer = false @State private var showScanProtoServer = false @State private var justOpened = true @State private var testing = false @State private var alert: ServerAlert? = nil @State private var showSaveDialog = false var proto: String { serverProtocol.rawValue.uppercased() } var body: some View { ZStack { protocolServersView() if testing { ProgressView().scaleEffect(2) } } } enum ServerAlert: Identifiable { case testsFailed(failures: [String: ProtocolTestFailure]) case error(title: LocalizedStringKey, error: LocalizedStringKey = "") var id: String { switch self { case .testsFailed: return "testsFailed" case let .error(title, _): return "error \(title)" } } } private func protocolServersView() -> some View { List { if !configuredServers.isEmpty { Section { ForEach($configuredServers) { srv in protocolServerView(srv) } .onMove { indexSet, offset in configuredServers.move(fromOffsets: indexSet, toOffset: offset) } .onDelete { indexSet in configuredServers.remove(atOffsets: indexSet) } } header: { Text("Configured \(proto) servers") .foregroundColor(theme.colors.secondary) } footer: { Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") .foregroundColor(theme.colors.secondary) .lineLimit(10) } } if !otherServers.isEmpty { Section { ForEach($otherServers) { srv in protocolServerView(srv) } .onMove { indexSet, offset in otherServers.move(fromOffsets: indexSet, toOffset: offset) } .onDelete { indexSet in otherServers.remove(atOffsets: indexSet) } } header: { Text("Other \(proto) servers") .foregroundColor(theme.colors.secondary) } } Section { Button("Add server") { showAddServer = true } } Section { Button("Reset") { partitionServers(currServers) } .disabled(Set(allServers) == Set(currServers) || testing) Button("Test servers", action: testServers) .disabled(testing || allServersDisabled) Button("Save servers", action: saveServers) .disabled(saveDisabled) howToButton() } } .toolbar { EditButton() } .confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) { Button("Enter server manually") { otherServers.append(ServerCfg.empty) selectedServer = allServers.last?.id } Button("Scan server QR code") { showScanProtoServer = true } Button("Add preset servers", action: addAllPresets) .disabled(hasAllPresets()) } .sheet(isPresented: $showScanProtoServer) { ScanProtocolServer(servers: $otherServers) .modifier(ThemedBackground(grouped: true)) } .modifier(BackButton(disabled: Binding.constant(false)) { if saveDisabled { dismiss() justOpened = false } else { showSaveDialog = true } }) .confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) { Button("Save") { saveServers() dismiss() justOpened = false } Button("Exit without saving") { dismiss() } } .alert(item: $alert) { a in switch a { case let .testsFailed(fs): let msg = fs.map { (srv, f) in "\(srv): \(f.localizedDescription)" }.joined(separator: "\n") return Alert( title: Text("Tests failed!"), message: Text("Some servers failed the test:\n" + msg) ) case .error: return Alert( title: Text("Error") ) } } .onAppear { // this condition is needed to prevent re-setting the servers when exiting single server view if justOpened { do { let r = try getUserProtoServers(serverProtocol) currServers = r.protoServers presetServers = r.presetServers partitionServers(currServers) } catch let error { alert = .error( title: "Error loading \(proto) servers", error: "Error: \(responseError(error))" ) } justOpened = false } else { partitionServers(allServers) } } } private func partitionServers(_ servers: [ServerCfg]) { configuredServers = servers.filter { $0.preset || $0.enabled } otherServers = servers.filter { !($0.preset || $0.enabled) } } private var allServers: [ServerCfg] { configuredServers + otherServers } private var saveDisabled: Bool { allServers.isEmpty || Set(allServers) == Set(currServers) || testing || !allServers.allSatisfy { srv in if let address = parseServerAddress(srv.server) { return uniqueAddress(srv, address) } return false } || allServersDisabled } private var allServersDisabled: Bool { allServers.allSatisfy { !$0.enabled } } private func protocolServerView(_ server: Binding) -> some View { let srv = server.wrappedValue return NavigationLink(tag: srv.id, selection: $selectedServer) { ProtocolServerView( serverProtocol: serverProtocol, server: server, serverToEdit: srv ) .navigationBarTitle(srv.preset ? "Preset server" : "Your server") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { let address = parseServerAddress(srv.server) HStack { Group { if let address = address { if !address.valid || address.serverProtocol != serverProtocol { invalidServer() } else if !uniqueAddress(srv, address) { Image(systemName: "exclamationmark.circle").foregroundColor(.red) } else if !srv.enabled { Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary) } else { showTestStatus(server: srv) } } else { invalidServer() } } .frame(width: 16, alignment: .center) .padding(.trailing, 4) let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1) if srv.enabled { v } else { v.foregroundColor(theme.colors.secondary) } } } } func howToButton() -> some View { Button { DispatchQueue.main.async { UIApplication.shared.open(howToUrl) } } label: { HStack { Text("How to use your servers") Image(systemName: "arrow.up.right.circle") } } } private func invalidServer() -> some View { Image(systemName: "exclamationmark.circle").foregroundColor(.red) } private func uniqueAddress(_ s: ServerCfg, _ address: ServerAddress) -> Bool { allServers.allSatisfy { srv in address.hostnames.allSatisfy { host in srv.id == s.id || !srv.server.contains(host) } } } private func hasAllPresets() -> Bool { presetServers.allSatisfy { hasPreset($0) } } private func addAllPresets() { for srv in presetServers { if !hasPreset(srv) { configuredServers.append(srv) } } } private func hasPreset(_ srv: ServerCfg) -> Bool { allServers.contains(where: { $0.server == srv.server }) } private func testServers() { resetTestStatus() testing = true Task { let fs = await runServersTest() await MainActor.run { testing = false if !fs.isEmpty { alert = .testsFailed(failures: fs) } } } } private func resetTestStatus() { for i in 0.. [String: ProtocolTestFailure] { var fs: [String: ProtocolTestFailure] = [:] for i in 0..