import SwiftUI /// The Calls server setting, as one Form section: in Calls → Settings… /// on a Mac and in the home screen's settings sheet on iPhone. /// /// The field is a draft until Use this server; changing the server while /// signed in asks first, because it signs out. @MainActor public struct ServerSettingsSection: View { @ObservedObject private var model: CallsModel @State private var draft: String @State private var problem = "" @State private var confirming: String? public init(model: CallsModel) { self.model = model _draft = State(initialValue: model.server) } private var changed: Bool { guard case .success(let origin) = CallsServer.validate(draft) else { return true } return origin != model.server } public var body: some View { Section { TextField("Server address", text: $draft, prompt: Text(CallsServer.defaultOrigin)) .accessibilityIdentifier("serverAddress") #if os(iOS) .keyboardType(.URL).textInputAutocapitalization(.never).autocorrectionDisabled() #endif .onSubmit(apply) .onChange(of: draft) { problem = "" } if !problem.isEmpty { Label(problem, systemImage: "exclamationmark.circle.fill") .foregroundStyle(.red).fixedSize(horizontal: false, vertical: true) .accessibilityLabel("Error: \(problem)") } HStack { Button("Use this server", action: apply) .disabled(!changed || model.joining).accessibilityIdentifier("useServer") Spacer() Button("Reset to \(CallsServer.host(of: CallsServer.defaultOrigin))") { draft = CallsServer.defaultOrigin apply() } .disabled(model.server == CallsServer.defaultOrigin || model.joining) } #if os(iOS) .buttonStyle(.borderless) #endif } header: { Text("Calls server") } footer: { Text( model.joining ? "Leave the current meeting to change the server." : "Sign in goes to this server. If you run your own Calls, enter its address here. Links to meetings on other servers still open, just for that meeting." ) } .confirmationDialog( "Change server and sign out?", isPresented: Binding( get: { confirming != nil }, set: { if !$0 { confirming = nil } }), titleVisibility: .visible ) { Button("Change and sign out", role: .destructive) { if let origin = confirming { model.changeServer(to: origin) } confirming = nil } Button("Cancel", role: .cancel) { confirming = nil draft = model.server } } message: { Text( "You’re signed in to \(model.host). Changing the server signs you out there; you can sign in to \(CallsServer.host(of: confirming ?? "")) next." ) } .onChange(of: model.server) { draft = model.server } } private func apply() { switch CallsServer.validate(draft) { case .failure(let failure): problem = failure.message case .success(let origin): draft = origin guard origin != model.server else { return } if model.changingServerSignsOut { confirming = origin } else { model.changeServer(to: origin) } } } } /// iPhone's settings: there is no Settings scene, so the home screen's /// gear opens this. @MainActor public struct ServerSettingsSheet: View { @ObservedObject private var model: CallsModel private let done: () -> Void @AppStorage(CallLayoutStyle.key) private var callLayout = CallLayoutStyle.defaultStyle public init(model: CallsModel, done: @escaping () -> Void) { self.model = model self.done = done } public var body: some View { NavigationStack { Form { Section { Picker("Call layout", selection: $callLayout) { ForEach(CallLayoutStyle.allCases) { style in Text(style.title).tag(style) } } .accessibilityIdentifier("callLayout") } header: { Text("During a call") } footer: { Text( callLayout == .fullWindow ? "The call fills the screen. Tap it to show the controls." : "The call sits above a row of controls that are always showing." ) } ServerSettingsSection(model: model) QualityReportsSection() } .formStyle(.grouped) .navigationTitle("Settings") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done", action: done) } } } .tint(CallsAppearance.accent) } } /// The automatic quality reports (spec §15.26): said, and a switch. Off, /// no automatic reports go; "Report a problem" still works, because that /// one is sent by the person on purpose. @MainActor public struct QualityReportsSection: View { @AppStorage(QualityReporting.enabledKey) private var send = true public init() {} public var body: some View { Section { Toggle("Send anonymous quality reports", isOn: $send) .accessibilityIdentifier("qualityReports") } header: { Text("Call quality") } footer: { Text( QualityReporting.disclosure + " They hold connection numbers only — no names, no audio or video, nothing you type.") } } }