From dd89f70258392432ce9d43980b99235373c272da Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Wed, 30 Mar 2022 23:27:58 +0200 Subject: [PATCH 1/4] add ProfileSettingsView --- NioUIKit/Menu/MenuOwnAccountView.swift | 2 +- .../ProfileSettingsContainerView.swift | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift diff --git a/NioUIKit/Menu/MenuOwnAccountView.swift b/NioUIKit/Menu/MenuOwnAccountView.swift index 7ff8da80..fc553a1c 100644 --- a/NioUIKit/Menu/MenuOwnAccountView.swift +++ b/NioUIKit/Menu/MenuOwnAccountView.swift @@ -31,7 +31,7 @@ struct MenuOwnAccountView: View { VStack(alignment: .leading) { ForEach(accounts, id: \.userID) { account in NavigationLink(destination: { - Text("Account") + ProfileSettingsContainerView(account: account) }) { MenuAccountPickerAccountView(account: account) } diff --git a/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift b/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift new file mode 100644 index 00000000..c33b18e8 --- /dev/null +++ b/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift @@ -0,0 +1,115 @@ +// +// ProfileSettingsContainerView.swift +// NioUIKit_iOS +// +// Created by Finn Behrens on 30.03.22. +// + +import MatrixCore +import SwiftUI + +struct ProfileSettingsContainerView: View { + @ObservedObject var account: MatrixAccount + + @Environment(\.dismiss) private var dismiss + + var body: some View { + List { + Section(header: Text("USER SETTINGS")) { + // TODO: Profile Picture + + // Display Name + HStack { + Text("Display Name") + Spacer(minLength: 20) + + TextField("Display Name", text: $account.wrappedDisplayName) + .multilineTextAlignment(.trailing) + } + + // Password + Button("Change password", role: .destructive) { + print("TODO: implement change password") + } + } + + Section(header: Text("SECURITY")) { + NavigationLink("Security") { + VStack { + Text("Device id's and fun") + } + .navigationTitle("Security") + } + } + + ProfileSettingsDangerZone(account: account) + } + .navigationTitle(account.displayName ?? account.userID ?? "Settings") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem { + Button(action: { + print("saving...") + // TODO: save to CoreData + dismiss() + }) { + Text("Save") + } + .disabled(!account.hasChanges) + } + } + .onDisappear { + print("discarding") + } + } +} + +struct ProfileSettingsDangerZone: View { + @ObservedObject var account: MatrixAccount + + @Environment(\.dismiss) private var dismiss + + @State private var showSignOutDialog: Bool = false + @State private var showDeactivateDialog: Bool = false + + var body: some View { + Section(header: Text("DANGER ZONE")) { + Button("Sign Out") { + showSignOutDialog = true + } + .disabled(showSignOutDialog) + .confirmationDialog("Are you sure you want to sign out?", isPresented: $showSignOutDialog, titleVisibility: .visible) { + Button("Sign out", role: .destructive) { + print("TODO: implement sign out") + // TODO: + } + } + + Button("Deactivate my account", role: .destructive) { + showDeactivateDialog = true + } + .disabled(showDeactivateDialog) + .confirmationDialog("Are you sure you want to disable your account? This cannot be undone", isPresented: $showDeactivateDialog, titleVisibility: .visible) { + Text("This cannot be undone") + .font(.headline) + .foregroundColor(.red) + Button("Deactivate", role: .destructive) { + print("TODO: deactivate account") + // TODO: + } + } + } + } +} + +struct ProfileSettingsContainerView_Previews: PreviewProvider { + static let account: MatrixAccount = MatrixStore.createAmir(context: MatrixStore.preview.viewContext) + + static var previews: some View { + Group { + NavigationView { + ProfileSettingsContainerView(account: ProfileSettingsContainerView_Previews.account) + } + } + } +} From 651aa41194f1bcf0d68605cbee0e939cd7aeb817 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Thu, 31 Mar 2022 14:40:57 +0200 Subject: [PATCH 2/4] add Session view --- .../ProfileSettingsContainerView.swift | 20 +- ...SettingsSecurityDevicesContainerView.swift | 256 ++++++++++++++++++ ...ProfileSettingsSecurityContainerView.swift | 29 ++ 3 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 NioUIKit/ProfileSettings/Security/Devices/ProfileSettingsSecurityDevicesContainerView.swift create mode 100644 NioUIKit/ProfileSettings/Security/ProfileSettingsSecurityContainerView.swift diff --git a/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift b/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift index c33b18e8..1b405788 100644 --- a/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift +++ b/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift @@ -6,6 +6,7 @@ // import MatrixCore +import NioKit import SwiftUI struct ProfileSettingsContainerView: View { @@ -35,15 +36,14 @@ struct ProfileSettingsContainerView: View { Section(header: Text("SECURITY")) { NavigationLink("Security") { - VStack { - Text("Device id's and fun") - } - .navigationTitle("Security") + ProfileSettingsSecurityContainerView() + .environmentObject(account) } } - ProfileSettingsDangerZone(account: account) + ProfileSettingsDangerZone() } + .environmentObject(account) .navigationTitle(account.displayName ?? account.userID ?? "Settings") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -65,7 +65,8 @@ struct ProfileSettingsContainerView: View { } struct ProfileSettingsDangerZone: View { - @ObservedObject var account: MatrixAccount + @EnvironmentObject var account: MatrixAccount + @EnvironmentObject var store: NioAccountStore @Environment(\.dismiss) private var dismiss @@ -81,6 +82,13 @@ struct ProfileSettingsDangerZone: View { .confirmationDialog("Are you sure you want to sign out?", isPresented: $showSignOutDialog, titleVisibility: .visible) { Button("Sign out", role: .destructive) { print("TODO: implement sign out") + Task(priority: .userInitiated) { + do { + try await self.store.logout(accountName: account.userID!) + } catch { + NioAccountStore.logger.fault("Failed to log out: \(error.localizedDescription)") + } + } // TODO: } } diff --git a/NioUIKit/ProfileSettings/Security/Devices/ProfileSettingsSecurityDevicesContainerView.swift b/NioUIKit/ProfileSettings/Security/Devices/ProfileSettingsSecurityDevicesContainerView.swift new file mode 100644 index 00000000..15647725 --- /dev/null +++ b/NioUIKit/ProfileSettings/Security/Devices/ProfileSettingsSecurityDevicesContainerView.swift @@ -0,0 +1,256 @@ +// +// ProfileSettingsSecurityDevicesContainerView.swift +// Nio +// +// Created by Finn Behrens on 31.03.22. +// + +import MatrixClient +import MatrixCore +import NioKit +import SwiftUI + +struct ProfileSettingsSecurityDevicesContainerView: View { + @EnvironmentObject var account: MatrixAccount + @EnvironmentObject var store: NioAccountStore + + @State private var devices: [MatrixDevice] = [] + @State private var ownDevice: MatrixDevice? + + @State private var selection = Set() + @State private var editMode = EditMode.inactive + + var body: some View { + List(selection: $selection) { + Section(header: Text("This device")) { + if let ownDevice = ownDevice { + NavigationLink { + ProfileSettingsSecurityDeviceDetailView(device: ownDevice, isSelf: true) + .environmentObject(account) + } label: { + ProfileSettingsSecurityDeviceView(device: ownDevice) + } + } else { + Text(account.deviceID ?? "Unknown Device") + } + } + + // TODO: verified/unverified devices sections? + Section(header: Text("Devices")) { + ForEach(devices, id: \.deviceID) { device in + NavigationLink { + ProfileSettingsSecurityDeviceDetailView(device: device) + .environmentObject(account) + } label: { + ProfileSettingsSecurityDeviceView(device: device) + } + .tag(device.id) + } + .onDelete(perform: delete) + } + } + .environment(\.editMode, $editMode) + .toolbar { + ToolbarItem(id: "delete", placement: .bottomBar, showsByDefault: false) { + if editMode == .active { + Button("Delete", role: .destructive) { + print("TODO: Mass delete") + withAnimation { + self.editMode = .inactive + } + } + .disabled(self.selection.isEmpty) + } + } + } + .toolbar { + ToolbarItem { + if editMode == .active { + Button("Cancel") { + self.selection.removeAll() + withAnimation { + self.editMode = .inactive + } + } + } else { + Button("Edit") { + withAnimation { + self.editMode = .active + } + } + } + } + } + .onAppear { + self.updateDevices() + } + } + + private func updateDevices() { + NioAccountStore.logger.debug("Updating device list") + Task(priority: .high) { + do { + var devices = try await self.store.accounts[account.userID!]?.matrixCore.client.getDevices().devices ?? [] + + if let ownIndex = devices.firstIndex(where: { $0.deviceID == account.deviceID ?? "" }) { + self.ownDevice = devices.remove(at: ownIndex) + } + + self.devices = devices + } catch { + NioAccountStore.logger.fault("Failed to get device list: \(error.localizedDescription)") + } + } + } + + private func delete(at offsets: IndexSet) { + let idsToDelete = offsets.map { self.devices[$0].deviceID } + + _ = idsToDelete.compactMap { id in + self.logoutOther(deviceID: id) + } + } + + private func logoutOther(deviceID: String) { + Task(priority: .medium) { + print("deleting \(deviceID)") + do { + let delete = try await self.store.accounts[account.userID!]?.matrixCore.client.deleteDevice(deviceID: deviceID) + print(delete as Any) + // TODO: do interactive auth + } catch { + print(error) + } + } + } +} + +struct ProfileSettingsSecurityDeviceView: View { + let device: MatrixDevice + + let subText: String + + init(device: MatrixDevice) { + self.device = device + + var subText = "" + if let lastSeen = device.lastSeen { + subText.append("Last seen \(lastSeen.formatted()) ") + } + + if let lastSeenIP = device.lastSeenIP { + subText.append("at \(lastSeenIP)") + } + + self.subText = subText + } + + var body: some View { + VStack(alignment: .leading) { + Text(device.displayName ?? device.deviceID) + .font(.subheadline) + + Text(subText) + .font(.footnote) + } + } +} + +struct ProfileSettingsSecurityDeviceDetailView: View { + @EnvironmentObject var account: MatrixAccount + @EnvironmentObject var store: NioAccountStore + + @Environment(\.dismiss) private var dismiss + + let device: MatrixDevice + + let isSelf: Bool + + @State var displayName: String + @State var working: Bool = false + + init(device: MatrixDevice, isSelf: Bool = false) { + self.device = device + self.isSelf = isSelf + + _displayName = State(initialValue: device.displayName ?? "") + } + + var body: some View { + List { + Section(header: Text("Session info")) { + HStack { + Text("Display Name") + Spacer(minLength: 20) + + TextField("Display Name", text: $displayName) + .multilineTextAlignment(.trailing) + .disabled(working) + } + + HStack { + Text("Session") + Spacer(minLength: 20) + + Text(device.deviceID) + .foregroundColor(.gray) + } + + if let lastSeenIP = device.lastSeenIP { + HStack { + Text("Last Seen IP") + Spacer(minLength: 20) + + Text(lastSeenIP) + .foregroundColor(.gray) + } + } + + if let lastSeen = device.lastSeen { + HStack { + Text("Last Seen") + Spacer(minLength: 20) + + Text(lastSeen.formatted()) + .foregroundColor(.gray) + } + } + } + } + .navigationTitle("Manage session") + .toolbar { + ToolbarItem { + Button(action: { + self.setDisplayName() + }) { + Text("Save") + } + .disabled(displayName == device.displayName || working) + } + } + } + + private func setDisplayName() { + working = true + Task(priority: .userInitiated) { + do { + let core = store.accounts[account.userID!]!.matrixCore + try await core.client.setDeviceDisplayName(displayName, deviceID: device.deviceID) + self.dismiss() + } catch { + NioAccountStore.logger.fault("Failed to set device display name") + } + self.working = false + } + } +} + +struct ProfileSettingsSecurityDevicesContainerView_Previews: PreviewProvider { + static var previews: some View { + Group { + // ProfileSettingsSecurityDevicesContainerView() + + ProfileSettingsSecurityDeviceDetailView(device: .init(deviceID: "EXMPLA", displayName: "Exmaple Device", lastSeenIP: "192.0.2.53", lastSeen: .now)) + } + } +} diff --git a/NioUIKit/ProfileSettings/Security/ProfileSettingsSecurityContainerView.swift b/NioUIKit/ProfileSettings/Security/ProfileSettingsSecurityContainerView.swift new file mode 100644 index 00000000..357d96e5 --- /dev/null +++ b/NioUIKit/ProfileSettings/Security/ProfileSettingsSecurityContainerView.swift @@ -0,0 +1,29 @@ +// +// ProfileSettingsSecurityContainerView.swift +// Nio +// +// Created by Finn Behrens on 31.03.22. +// + +import MatrixCore +import SwiftUI + +struct ProfileSettingsSecurityContainerView: View { + @EnvironmentObject var account: MatrixAccount + + var body: some View { + List { + NavigationLink("Sessions") { + ProfileSettingsSecurityDevicesContainerView() + .environmentObject(account) + } + } + .navigationTitle("Security") + } +} + +struct ProfileSettingsSecurityContainerView_Previews: PreviewProvider { + static var previews: some View { + ProfileSettingsSecurityContainerView() + } +} From 5f75173eecf5b18cdc648949848a79092dc759b2 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Thu, 31 Mar 2022 19:30:02 +0200 Subject: [PATCH 3/4] add refreshable to Session list --- NioKit/Account.swift | 2 +- .../ProfileSettingsContainerView.swift | 56 ++++++++++++++++++- ...SettingsSecurityDevicesContainerView.swift | 25 ++++++--- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/NioKit/Account.swift b/NioKit/Account.swift index 59040024..3961eb3f 100644 --- a/NioKit/Account.swift +++ b/NioKit/Account.swift @@ -71,7 +71,7 @@ public class NioAccountStore: ObservableObject { // MARK: - logout public func logout(account: NioAccount) async throws { - let userID = await account.userID.FQMXID! + let userID = account.userID.FQMXID! accounts.removeValue(forKey: userID) try await account.logout() diff --git a/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift b/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift index 1b405788..06000db6 100644 --- a/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift +++ b/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift @@ -5,15 +5,22 @@ // Created by Finn Behrens on 30.03.22. // +import MatrixClient import MatrixCore import NioKit import SwiftUI struct ProfileSettingsContainerView: View { @ObservedObject var account: MatrixAccount + @EnvironmentObject var store: NioAccountStore + @Environment(\.managedObjectContext) var moc @Environment(\.dismiss) private var dismiss + @State var capabilities = MatrixCapabilities() + + @State var task: Task? + var body: some View { List { Section(header: Text("USER SETTINGS")) { @@ -26,12 +33,14 @@ struct ProfileSettingsContainerView: View { TextField("Display Name", text: $account.wrappedDisplayName) .multilineTextAlignment(.trailing) + .disabled(!self.capabilities.capabilities.canSetDisplayName) } // Password Button("Change password", role: .destructive) { print("TODO: implement change password") } + .disabled(!self.capabilities.capabilities.canChangePassword) } Section(header: Text("SECURITY")) { @@ -50,16 +59,59 @@ struct ProfileSettingsContainerView: View { ToolbarItem { Button(action: { print("saving...") - // TODO: save to CoreData - dismiss() + Task(priority: .userInitiated) { + let changes = account.changedValues() + + let nioAccount = store.accounts[account.userID!]! + + if let displayName = changes["displayName"] as? String { + NioAccountStore.logger.debug("Changing displayName to \(displayName)") + + do { + try await nioAccount.matrixCore.client.setDisplayName(displayName, userID: account.userID!) + } catch { + NioAccountStore.logger.fault("Failed to save displayName to matrix account") + } + } + do { + try moc.save() + } catch { + NioAccountStore.logger.fault("Failed to save changed account to CoreData") + } + + // TODO: save to CoreData + self.dismiss() + } }) { Text("Save") } .disabled(!account.hasChanges) } } + .onAppear { + moc.undoManager = UndoManager() + self.probeServer() + } .onDisappear { + task?.cancel() print("discarding") + moc.undoManager?.undo() + moc.undoManager = nil + } + } + + private func probeServer() { + task = Task(priority: .high) { + let nioAccount = store.accounts[account.userID!] + + do { + let capabilities = try await nioAccount?.matrixCore.client.getCapabilities() + if let capabilities = capabilities { + self.capabilities = capabilities + } + } catch { + NioAccountStore.logger.fault("Failed to get server capabilities") + } } } } diff --git a/NioUIKit/ProfileSettings/Security/Devices/ProfileSettingsSecurityDevicesContainerView.swift b/NioUIKit/ProfileSettings/Security/Devices/ProfileSettingsSecurityDevicesContainerView.swift index 15647725..24788c1c 100644 --- a/NioUIKit/ProfileSettings/Security/Devices/ProfileSettingsSecurityDevicesContainerView.swift +++ b/NioUIKit/ProfileSettings/Security/Devices/ProfileSettingsSecurityDevicesContainerView.swift @@ -63,6 +63,9 @@ struct ProfileSettingsSecurityDevicesContainerView: View { } } } + .refreshable { + await self.updateDevices() + } .toolbar { ToolbarItem { if editMode == .active { @@ -87,19 +90,23 @@ struct ProfileSettingsSecurityDevicesContainerView: View { } private func updateDevices() { - NioAccountStore.logger.debug("Updating device list") Task(priority: .high) { - do { - var devices = try await self.store.accounts[account.userID!]?.matrixCore.client.getDevices().devices ?? [] + await self.updateDevices() + } + } - if let ownIndex = devices.firstIndex(where: { $0.deviceID == account.deviceID ?? "" }) { - self.ownDevice = devices.remove(at: ownIndex) - } + private func updateDevices() async { + NioAccountStore.logger.debug("Updating device list") + do { + var devices = try await store.accounts[account.userID!]?.matrixCore.client.getDevices().devices ?? [] - self.devices = devices - } catch { - NioAccountStore.logger.fault("Failed to get device list: \(error.localizedDescription)") + if let ownIndex = devices.firstIndex(where: { $0.deviceID == account.deviceID ?? "" }) { + ownDevice = devices.remove(at: ownIndex) } + + self.devices = devices + } catch { + NioAccountStore.logger.fault("Failed to get device list: \(error.localizedDescription)") } } From e392fabbbbc35f4a2964ad57b621ffb97e030e2c Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Fri, 1 Apr 2022 16:44:14 +0200 Subject: [PATCH 4/4] Remove undoManager on save to prevent undo --- .../ProfileSettings/ProfileSettingsContainerView.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift b/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift index 06000db6..cc59266f 100644 --- a/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift +++ b/NioUIKit/ProfileSettings/ProfileSettingsContainerView.swift @@ -80,6 +80,7 @@ struct ProfileSettingsContainerView: View { } // TODO: save to CoreData + self.moc.undoManager = nil self.dismiss() } }) { @@ -89,14 +90,14 @@ struct ProfileSettingsContainerView: View { } } .onAppear { - moc.undoManager = UndoManager() + self.moc.undoManager = UndoManager() self.probeServer() } .onDisappear { - task?.cancel() + self.task?.cancel() print("discarding") - moc.undoManager?.undo() - moc.undoManager = nil + self.moc.undoManager?.undo() + self.moc.undoManager = nil } }