// // 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 @Environment(\.editMode) private var editMode let serverProtocol: ServerProtocol @State private var currServers: [ServerCfg] = [] @State private var presetServers: [String] = [] @State private var servers: [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 { Section { ForEach($servers) { srv in protocolServerView(srv) } .onMove { indexSet, offset in servers.move(fromOffsets: indexSet, toOffset: offset) } .onDelete { indexSet in servers.remove(atOffsets: indexSet) } Button("Add server…") { showAddServer = true } } header: { Text("\(proto) servers") } footer: { Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") .lineLimit(10) } Section { Button("Reset") { servers = currServers } .disabled(servers == 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") { servers.append(ServerCfg.empty) selectedServer = servers.last?.id } Button("Scan server QR code") { showScanProtoServer = true } Button("Add preset servers", action: addAllPresets) .disabled(hasAllPresets()) } .sheet(isPresented: $showScanProtoServer) { ScanProtocolServer(servers: $servers) } .modifier(BackButton(disabled: Binding.constant(false)) { if saveDisabled { dismiss() justOpened = false } else { showSaveDialog = true } }) .confirmationDialog("Save servers?", isPresented: $showSaveDialog) { 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 { return } do { let r = try getUserProtoServers(serverProtocol) currServers = r.protoServers presetServers = r.presetServers servers = currServers } catch let error { alert = .error( title: "Error loading \(proto) servers", error: "Error: \(responseError(error))" ) } justOpened = false } } private var saveDisabled: Bool { servers.isEmpty || servers == currServers || testing || !servers.allSatisfy { srv in if let address = parseServerAddress(srv.server) { return uniqueAddress(srv, address) } return false } || allServersDisabled } private var allServersDisabled: Bool { servers.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") .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(.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(.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 { servers.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) { servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true)) } } } private func hasPreset(_ srv: String) -> Bool { servers.contains(where: { $0.server == srv }) } 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..