mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-03-14 09:45:42 +00:00

* ios: new user picker (#4770) * current user picker progress * one hand picker * thin bullet icon * more user picker buttons * button clickable areas * divider padding * extra space after sun * send current user option to address view * add unread count badge * with anim for apperance close * edit current profile from picker * remove you section from settings * remove help and support * simplify * move settings and sun to same row * remove redundant vstack * long press on sun/moon switches to system setting * remove back button from migrate device * smooth profile transitions * close user picker on list profiles * fix dismiss on migrate from device * fix dismiss when deleting last visible user while having hidden users * picker visibility toggle tweaks * remove strange square from profile switcher click * dirty way to save auto accept settings on dismiss * Revert "dirty way to save auto accept settings on dismiss" This reverts commite7b19ee8aa
. * consistent animation on user picker toggle * change space between profiles * remove result * ignore result * unread badge * move to sheet * half sheet on one hand ui * fix dismiss on device migration * fix desktop connect * sun to meet other action icons * fill bullet list button * fix tap in settings to take full width * icon sizings and paddings * open settings in same sheet * apply same trick as other buttons for ligth toggle * layout * open profiles sheet large when +3 users * layout * layout * paddings * paddings * remove show progress * always small user picker * fixed height * open all actions as sheets * type, color * simpler and more effective way of avoid moving around on user select * dismiss user profiles sheet on user change * connect desktop back button remove * remove back buttons from user address view * remove porgress * header inside list * alert on auto accept unsaved changes * Cancel -> Discard * revert * fix connect to desktop * remove extra space * fix share inside multi sheet * user picker and options as separate sheet * revert showShareSheet * fix current profile and all profiles selection * change alert * update * cleanup user address * remove func * alert on unsaved changes in chat prefs * fix layout * cleanup --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: fix switching profiles (#4822) * ios: different user picker layout (#4826) * ios: different user picker layout * remove section * layout, color * color * remove activeUser * fix gradient * recursive sheets * gradient padding * share sheet * layout * dismiss sheets --------- Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com> * ios: use the same way to share from all sheets (#4829) * ios: close user picker before opening other sheets * Revert "share sheet" This reverts commit0064155825
. * dismiss/show via callback * Revert "ios: close user picker before opening other sheets" This reverts commit19110398f8
. * ios: show alerts from sheets (#4839) * padding * remove gradient * cleanup * simplify settings * padding --------- Co-authored-by: Diogo <diogofncunha@gmail.com> Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
556 lines
20 KiB
Swift
556 lines
20 KiB
Swift
//
|
|
// ConnectDesktopView.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by Evgeny on 13/10/2023.
|
|
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
import CodeScanner
|
|
|
|
struct ConnectDesktopView: View {
|
|
@EnvironmentObject var m: ChatModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@Environment(\.dismiss) var dismiss: DismissAction
|
|
var viaSettings = false
|
|
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
|
|
@AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false
|
|
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = true
|
|
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO) private var connectRemoteViaMulticastAuto = true
|
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
|
@State private var sessionAddress: String = ""
|
|
@State private var remoteCtrls: [RemoteCtrlInfo] = []
|
|
@State private var alert: ConnectDesktopAlert?
|
|
@State private var showConnectScreen = true
|
|
@State private var showQRCodeScanner = true
|
|
@State private var firstAppearance = true
|
|
|
|
private var useMulticast: Bool {
|
|
connectRemoteViaMulticast && !remoteCtrls.isEmpty
|
|
}
|
|
|
|
private enum ConnectDesktopAlert: Identifiable {
|
|
case unlinkDesktop(rc: RemoteCtrlInfo)
|
|
case disconnectDesktop(action: UserDisconnectAction)
|
|
case badInvitationError
|
|
case badVersionError(version: String?)
|
|
case desktopDisconnectedError
|
|
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
|
|
|
var id: String {
|
|
switch self {
|
|
case let .unlinkDesktop(rc): "unlinkDesktop \(rc.remoteCtrlId)"
|
|
case let .disconnectDesktop(action): "disconnectDecktop \(action)"
|
|
case .badInvitationError: "badInvitationError"
|
|
case let .badVersionError(v): "badVersionError \(v ?? "")"
|
|
case .desktopDisconnectedError: "desktopDisconnectedError"
|
|
case let .error(title, _): "error \(title)"
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum UserDisconnectAction: String {
|
|
case back
|
|
case dismiss // TODO dismiss settings after confirmation
|
|
}
|
|
|
|
var body: some View {
|
|
if viaSettings {
|
|
viewBody
|
|
} else {
|
|
NavigationView {
|
|
viewBody
|
|
}
|
|
}
|
|
}
|
|
|
|
var viewBody: some View {
|
|
Group {
|
|
let discovery = m.remoteCtrlSession?.discovery
|
|
if discovery == true || (discovery == nil && !showConnectScreen) {
|
|
searchingDesktopView()
|
|
} else if let session = m.remoteCtrlSession {
|
|
switch session.sessionState {
|
|
case .starting: connectingDesktopView(session, nil)
|
|
case .searching: searchingDesktopView()
|
|
case let .found(rc, compatible): foundDesktopView(session, rc, compatible)
|
|
case let .connecting(rc_): connectingDesktopView(session, rc_)
|
|
case let .pendingConfirmation(rc_, sessCode):
|
|
if confirmRemoteSessions || rc_ == nil {
|
|
verifySessionView(session, rc_, sessCode)
|
|
} else {
|
|
connectingDesktopView(session, rc_).onAppear {
|
|
verifyDesktopSessionCode(sessCode)
|
|
}
|
|
}
|
|
case let .connected(rc, _): activeSessionView(session, rc)
|
|
}
|
|
// The hack below prevents camera freezing when exiting linked devices view.
|
|
// Using showQRCodeScanner inside connectDesktopView or passing it as parameter still results in freezing.
|
|
} else if showQRCodeScanner || firstAppearance {
|
|
connectDesktopView()
|
|
} else {
|
|
connectDesktopView(showScanner: false)
|
|
}
|
|
}
|
|
.onAppear {
|
|
setDeviceName(deviceName)
|
|
updateRemoteCtrls()
|
|
showConnectScreen = !useMulticast
|
|
if m.remoteCtrlSession != nil {
|
|
disconnectDesktop()
|
|
} else if useMulticast {
|
|
findKnownDesktop()
|
|
}
|
|
// The hack below prevents camera freezing when exiting linked devices view.
|
|
// `firstAppearance` prevents camera flicker when the view first opens.
|
|
// moving `showQRCodeScanner = false` to `onDisappear` (to avoid `firstAppearance`) does not prevent freeze.
|
|
showQRCodeScanner = false
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
|
firstAppearance = false
|
|
showQRCodeScanner = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
if m.remoteCtrlSession != nil {
|
|
showConnectScreen = false
|
|
disconnectDesktop()
|
|
}
|
|
}
|
|
.onChange(of: deviceName) {
|
|
setDeviceName($0)
|
|
}
|
|
.onChange(of: m.activeRemoteCtrl) {
|
|
UIApplication.shared.isIdleTimerDisabled = $0
|
|
}
|
|
.alert(item: $alert) { a in
|
|
switch a {
|
|
case let .unlinkDesktop(rc):
|
|
Alert(
|
|
title: Text("Unlink desktop?"),
|
|
primaryButton: .destructive(Text("Unlink")) {
|
|
unlinkDesktop(rc)
|
|
},
|
|
secondaryButton: .cancel()
|
|
)
|
|
case let .disconnectDesktop(action):
|
|
Alert(
|
|
title: Text("Disconnect desktop?"),
|
|
primaryButton: .destructive(Text("Disconnect")) {
|
|
disconnectDesktop(action)
|
|
},
|
|
secondaryButton: .cancel()
|
|
)
|
|
case .badInvitationError:
|
|
Alert(title: Text("Bad desktop address"))
|
|
case let .badVersionError(v):
|
|
Alert(
|
|
title: Text("Incompatible version"),
|
|
message: Text("Desktop app version \(v ?? "") is not compatible with this app.")
|
|
)
|
|
case .desktopDisconnectedError:
|
|
Alert(title: Text("Connection terminated"))
|
|
case let .error(title, error):
|
|
mkAlert(title: title, message: error)
|
|
}
|
|
}
|
|
.interactiveDismissDisabled(m.activeRemoteCtrl)
|
|
}
|
|
|
|
private func connectDesktopView(showScanner: Bool = true) -> some View {
|
|
List {
|
|
Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) {
|
|
devicesView()
|
|
}
|
|
if showScanner {
|
|
scanDesctopAddressView()
|
|
}
|
|
if developerTools {
|
|
desktopAddressView()
|
|
}
|
|
}
|
|
.navigationTitle("Connect to desktop")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
}
|
|
|
|
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
|
|
ZStack {
|
|
List {
|
|
Section(header: Text("Connecting to desktop").foregroundColor(theme.colors.secondary)) {
|
|
ctrlDeviceNameText(session, rc)
|
|
ctrlDeviceVersionText(session)
|
|
}
|
|
|
|
if let sessCode = session.sessionCode {
|
|
Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) {
|
|
sessionCodeText(sessCode)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
disconnectButton()
|
|
}
|
|
}
|
|
.navigationTitle("Connecting to desktop")
|
|
|
|
ProgressView().scaleEffect(2)
|
|
}
|
|
.modifier(ThemedBackground(grouped: true))
|
|
}
|
|
|
|
private func searchingDesktopView() -> some View {
|
|
List {
|
|
Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) {
|
|
devicesView()
|
|
}
|
|
Section(header: Text("Found desktop").foregroundColor(theme.colors.secondary)) {
|
|
Text("Waiting for desktop...").italic()
|
|
Button {
|
|
disconnectDesktop()
|
|
} label: {
|
|
Label("Scan QR code", systemImage: "qrcode")
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Connecting to desktop")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
}
|
|
|
|
@ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View {
|
|
let v = List {
|
|
Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) {
|
|
devicesView()
|
|
}
|
|
Section(header: Text("Found desktop").foregroundColor(theme.colors.secondary)) {
|
|
ctrlDeviceNameText(session, rc)
|
|
ctrlDeviceVersionText(session)
|
|
if !compatible {
|
|
Text("Not compatible!").foregroundColor(.red)
|
|
} else if !connectRemoteViaMulticastAuto {
|
|
Button {
|
|
confirmKnownDesktop(rc)
|
|
} label: {
|
|
Label("Connect", systemImage: "checkmark")
|
|
}
|
|
}
|
|
}
|
|
if !compatible && !connectRemoteViaMulticastAuto {
|
|
Section {
|
|
disconnectButton("Cancel")
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Found desktop")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
|
|
if compatible && connectRemoteViaMulticastAuto {
|
|
v.onAppear { confirmKnownDesktop(rc) }
|
|
} else {
|
|
v
|
|
}
|
|
}
|
|
|
|
private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View {
|
|
List {
|
|
Section(header: Text("Connected to desktop").foregroundColor(theme.colors.secondary)) {
|
|
ctrlDeviceNameText(session, rc)
|
|
ctrlDeviceVersionText(session)
|
|
}
|
|
|
|
Section(header: Text("Verify code with desktop").foregroundColor(theme.colors.secondary)) {
|
|
sessionCodeText(sessCode)
|
|
Button {
|
|
verifyDesktopSessionCode(sessCode)
|
|
} label: {
|
|
Label("Confirm", systemImage: "checkmark")
|
|
}
|
|
}
|
|
|
|
Section {
|
|
disconnectButton()
|
|
}
|
|
}
|
|
.navigationTitle("Verify connection")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
}
|
|
|
|
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
|
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "")
|
|
if (rc == nil) {
|
|
t = t + Text(" ") + Text("(new)").italic()
|
|
}
|
|
return t
|
|
}
|
|
|
|
private func ctrlDeviceVersionText(_ session: RemoteCtrlSession) -> Text {
|
|
let v = session.ctrlAppInfo?.appVersionRange.maxVersion
|
|
var t = Text("v\(v ?? "")")
|
|
if v != session.appVersion {
|
|
t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic()
|
|
}
|
|
return t
|
|
}
|
|
|
|
private func activeSessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo) -> some View {
|
|
List {
|
|
Section(header: Text("Connected desktop").foregroundColor(theme.colors.secondary)) {
|
|
Text(rc.deviceViewName)
|
|
ctrlDeviceVersionText(session)
|
|
}
|
|
|
|
if let sessCode = session.sessionCode {
|
|
Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) {
|
|
sessionCodeText(sessCode)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
disconnectButton()
|
|
} footer: {
|
|
// This is specific to iOS
|
|
Text("Keep the app open to use it from desktop")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
.navigationTitle("Connected to desktop")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
}
|
|
|
|
private func sessionCodeText(_ code: String) -> some View {
|
|
Text(code.prefix(23))
|
|
}
|
|
|
|
private func devicesView() -> some View {
|
|
Group {
|
|
TextField("Enter this device name…", text: $deviceName)
|
|
if !remoteCtrls.isEmpty {
|
|
NavigationLink {
|
|
linkedDesktopsView()
|
|
} label: {
|
|
Text("Linked desktops")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func scanDesctopAddressView() -> some View {
|
|
Section(header: Text("Scan QR code from desktop").foregroundColor(theme.colors.secondary)) {
|
|
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode)
|
|
}
|
|
}
|
|
|
|
private func desktopAddressView() -> some View {
|
|
Section(header: Text("Desktop address").foregroundColor(theme.colors.secondary)) {
|
|
if sessionAddress.isEmpty {
|
|
Button {
|
|
sessionAddress = UIPasteboard.general.string ?? ""
|
|
} label: {
|
|
Label("Paste desktop address", systemImage: "doc.plaintext")
|
|
}
|
|
.disabled(!UIPasteboard.general.hasStrings)
|
|
} else {
|
|
HStack {
|
|
Text(sessionAddress).lineLimit(1)
|
|
Spacer()
|
|
Image(systemName: "multiply.circle.fill")
|
|
.foregroundColor(theme.colors.secondary)
|
|
.onTapGesture { sessionAddress = "" }
|
|
}
|
|
}
|
|
Button {
|
|
connectDesktopAddress(sessionAddress)
|
|
} label: {
|
|
Label("Connect to desktop", systemImage: "rectangle.connected.to.line.below")
|
|
}
|
|
.disabled(sessionAddress.isEmpty)
|
|
}
|
|
}
|
|
|
|
private func linkedDesktopsView() -> some View {
|
|
List {
|
|
Section(header: Text("Desktop devices").foregroundColor(theme.colors.secondary)) {
|
|
ForEach(remoteCtrls, id: \.remoteCtrlId) { rc in
|
|
remoteCtrlView(rc)
|
|
}
|
|
.onDelete { indexSet in
|
|
if let i = indexSet.first, i < remoteCtrls.count {
|
|
alert = .unlinkDesktop(rc: remoteCtrls[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Linked desktop options").foregroundColor(theme.colors.secondary)) {
|
|
Toggle("Verify connections", isOn: $confirmRemoteSessions)
|
|
Toggle("Discover via local network", isOn: $connectRemoteViaMulticast)
|
|
if connectRemoteViaMulticast {
|
|
Toggle("Connect automatically", isOn: $connectRemoteViaMulticastAuto)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Linked desktops")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
}
|
|
|
|
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
|
|
Text(rc.deviceViewName)
|
|
}
|
|
|
|
|
|
private func setDeviceName(_ name: String) {
|
|
do {
|
|
try setLocalDeviceName(deviceName)
|
|
} catch let e {
|
|
errorAlert(e)
|
|
}
|
|
}
|
|
|
|
private func updateRemoteCtrls() {
|
|
do {
|
|
remoteCtrls = try listRemoteCtrls()
|
|
} catch let e {
|
|
errorAlert(e)
|
|
}
|
|
}
|
|
|
|
private func processDesktopQRCode(_ resp: Result<ScanResult, ScanError>) {
|
|
switch resp {
|
|
case let .success(r): connectDesktopAddress(r.string)
|
|
case let .failure(e): errorAlert(e)
|
|
}
|
|
}
|
|
|
|
private func findKnownDesktop() {
|
|
Task {
|
|
do {
|
|
try await findKnownRemoteCtrl()
|
|
await MainActor.run {
|
|
m.remoteCtrlSession = RemoteCtrlSession(
|
|
ctrlAppInfo: nil,
|
|
appVersion: "",
|
|
sessionState: .searching
|
|
)
|
|
showConnectScreen = true
|
|
}
|
|
} catch let e {
|
|
await MainActor.run {
|
|
errorAlert(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func confirmKnownDesktop(_ rc: RemoteCtrlInfo) {
|
|
connectDesktop_ {
|
|
try await confirmRemoteCtrl(rc.remoteCtrlId)
|
|
}
|
|
}
|
|
|
|
private func connectDesktopAddress(_ addr: String) {
|
|
connectDesktop_ {
|
|
try await connectRemoteCtrl(desktopAddress: addr)
|
|
}
|
|
}
|
|
|
|
private func connectDesktop_(_ connect: @escaping () async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String)) {
|
|
Task {
|
|
do {
|
|
let (rc_, ctrlAppInfo, v) = try await connect()
|
|
await MainActor.run {
|
|
sessionAddress = ""
|
|
m.remoteCtrlSession = RemoteCtrlSession(
|
|
ctrlAppInfo: ctrlAppInfo,
|
|
appVersion: v,
|
|
sessionState: .connecting(remoteCtrl_: rc_)
|
|
)
|
|
}
|
|
} catch let e {
|
|
await MainActor.run {
|
|
switch e as? ChatResponse {
|
|
case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError
|
|
case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError
|
|
case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v)
|
|
case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil)
|
|
case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError
|
|
default: errorAlert(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func verifyDesktopSessionCode(_ sessCode: String) {
|
|
Task {
|
|
do {
|
|
let rc = try await verifyRemoteCtrlSession(sessCode)
|
|
await MainActor.run {
|
|
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(.connected(remoteCtrl: rc, sessionCode: sessCode))
|
|
}
|
|
await MainActor.run {
|
|
updateRemoteCtrls()
|
|
}
|
|
} catch let error {
|
|
await MainActor.run {
|
|
errorAlert(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func disconnectButton(_ label: LocalizedStringKey = "Disconnect") -> some View {
|
|
Button {
|
|
disconnectDesktop(.dismiss)
|
|
} label: {
|
|
Label(label, systemImage: "multiply")
|
|
}
|
|
}
|
|
|
|
private func disconnectDesktop(_ action: UserDisconnectAction? = nil) {
|
|
Task {
|
|
do {
|
|
try await stopRemoteCtrl()
|
|
await MainActor.run {
|
|
if case .connected = m.remoteCtrlSession?.sessionState {
|
|
switchToLocalSession()
|
|
} else {
|
|
m.remoteCtrlSession = nil
|
|
}
|
|
switch action {
|
|
case .back: dismiss()
|
|
case .dismiss: dismiss()
|
|
case .none: ()
|
|
}
|
|
}
|
|
} catch let e {
|
|
await MainActor.run {
|
|
errorAlert(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func unlinkDesktop(_ rc: RemoteCtrlInfo) {
|
|
Task {
|
|
do {
|
|
try await deleteRemoteCtrl(rc.remoteCtrlId)
|
|
await MainActor.run {
|
|
remoteCtrls.removeAll(where: { $0.remoteCtrlId == rc.remoteCtrlId })
|
|
}
|
|
} catch let e {
|
|
await MainActor.run {
|
|
errorAlert(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func errorAlert(_ error: Error) {
|
|
let a = getErrorAlert(error, "Error")
|
|
alert = .error(title: a.title, error: a.message)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ConnectDesktopView()
|
|
}
|