From 11053b2ca3f55a0187ea034568a04cf17e229dfc Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 30 Dec 2024 16:01:08 +0100 Subject: [PATCH] Add devices list in Account profile --- .../calendar-blank.svg | 2 +- .../Assets.xcassets/clock.imageset/clock.svg | 2 +- .../desktop.imageset/Contents.json | 21 ++++ .../desktop.imageset/desktop.svg | 1 + .../Contents.json | 21 ++++ .../device-mobile-camera.svg | 1 + Linphone/Localizable.xcstrings | 68 ++++++++++ .../Fragments/AccountProfileFragment.swift | 112 ++++++++++++++--- Linphone/UI/Main/Viewmodel/AccountModel.swift | 119 ++++++++++++++++++ 9 files changed, 329 insertions(+), 18 deletions(-) create mode 100644 Linphone/Assets.xcassets/desktop.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/desktop.imageset/desktop.svg create mode 100644 Linphone/Assets.xcassets/device-mobile-camera.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/device-mobile-camera.imageset/device-mobile-camera.svg diff --git a/Linphone/Assets.xcassets/calendar-blank.imageset/calendar-blank.svg b/Linphone/Assets.xcassets/calendar-blank.imageset/calendar-blank.svg index 81024d312..93d0645c8 100644 --- a/Linphone/Assets.xcassets/calendar-blank.imageset/calendar-blank.svg +++ b/Linphone/Assets.xcassets/calendar-blank.imageset/calendar-blank.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/clock.imageset/clock.svg b/Linphone/Assets.xcassets/clock.imageset/clock.svg index 18f1a5b97..00079cec2 100644 --- a/Linphone/Assets.xcassets/clock.imageset/clock.svg +++ b/Linphone/Assets.xcassets/clock.imageset/clock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/desktop.imageset/Contents.json b/Linphone/Assets.xcassets/desktop.imageset/Contents.json new file mode 100644 index 000000000..5c5a0c295 --- /dev/null +++ b/Linphone/Assets.xcassets/desktop.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "desktop.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/desktop.imageset/desktop.svg b/Linphone/Assets.xcassets/desktop.imageset/desktop.svg new file mode 100644 index 000000000..520150f68 --- /dev/null +++ b/Linphone/Assets.xcassets/desktop.imageset/desktop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/device-mobile-camera.imageset/Contents.json b/Linphone/Assets.xcassets/device-mobile-camera.imageset/Contents.json new file mode 100644 index 000000000..70a415402 --- /dev/null +++ b/Linphone/Assets.xcassets/device-mobile-camera.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "device-mobile-camera.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/device-mobile-camera.imageset/device-mobile-camera.svg b/Linphone/Assets.xcassets/device-mobile-camera.imageset/device-mobile-camera.svg new file mode 100644 index 000000000..ae8b87922 --- /dev/null +++ b/Linphone/Assets.xcassets/device-mobile-camera.imageset/device-mobile-camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index dc2db14e0..7aaad98c4 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -3814,6 +3814,57 @@ } } }, + "manage_account_device_last_connection" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Last connection:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dernière connexion :" + } + } + } + }, + "manage_account_device_remove" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer" + } + } + } + }, + "manage_account_devices_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Devices" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appareils" + } + } + } + }, "manage_account_dialog_international_prefix_help_message" : { "extractionState" : "manual", "localizations" : { @@ -3864,6 +3915,23 @@ } } }, + "manage_account_no_device" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No device found…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucun appareil n'a été trouvé…" + } + } + } + }, "manage_account_remove_picture" : { "localizations" : { "en" : { diff --git a/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift b/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift index 1eb0f8d80..98a9a58e8 100644 --- a/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift +++ b/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift @@ -31,6 +31,7 @@ struct AccountProfileFragment: View { @Binding var isShowAccountProfileFragment: Bool @State var detailIsOpen: Bool = true + @State var deviceIsOpen: Bool = false @State private var showPhotoPicker = false @State private var selectedImage: UIImage? @@ -384,7 +385,7 @@ struct AccountProfileFragment: View { HStack(spacing: 20) { Toggle("", isOn: Binding( get: { accountModel.isRegistrered }, - set: { newValue in + set: { _ in accountProfileViewModel.toggleRegister() } )) @@ -408,46 +409,123 @@ struct AccountProfileFragment: View { .background(.white) .cornerRadius(15) .padding(.all) + .background(Color.gray100) - /* HStack(alignment: .center) { - Text("manage_account_details_title") + Text("manage_account_devices_title") .default_text_style_800(styleSize: 18) .frame(maxWidth: .infinity, alignment: .leading) Spacer() - Image(detailIsOpen ? "caret-up" : "caret-down") + Image(deviceIsOpen ? "caret-up" : "caret-down") .renderingMode(.template) .resizable() .foregroundStyle(Color.grayMain2c600) .frame(width: 25, height: 25, alignment: .leading) .padding(.all, 10) } - .padding(.top, 10) - .padding(.bottom, 10) + .padding(.vertical, 10) .padding(.horizontal, 20) .background(Color.gray100) .onTapGesture { withAnimation { - detailIsOpen.toggle() + deviceIsOpen.toggle() } } - if detailIsOpen { + if deviceIsOpen { VStack(spacing: 0) { - VStack(spacing: 30) { - Text("manage_account_dialog_international_prefix_help_message") - .default_text_style_700(styleSize: 15) - .frame(maxWidth: .infinity, alignment: .leading) + VStack(spacing: 15) { + ForEach(accountModel.devices.indices, id: \.self) { index in + VStack { + HStack { + Image(accountModel.devices[index].isMobileDevice ? "device-mobile-camera" : "desktop") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + + Text(accountModel.devices[index].deviceName) + .default_text_style_700(styleSize: 15) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + Button(action: { + deviceIsOpen = false + accountModel.removeDevice(deviceIndex: index) + deviceIsOpen = true + }, label: { + HStack { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 20, height: 20) + + Text("manage_account_device_remove") + .default_text_style_orange_500(styleSize: 14) + .frame(height: 35) + } + + }) + .padding(.horizontal, 10) + .background(Color.orangeMain100) + .cornerRadius(60) + } + .padding(.bottom, 10) + + Text("manage_account_device_last_connection") + .default_text_style_700(styleSize: 15) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack { + Image("calendar-blank") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + + Text(accountModel.devices[index].lastDate) + .default_text_style(styleSize: 15) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + Image("clock") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + + Text(accountModel.devices[index].lastTime) + .default_text_style(styleSize: 15) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(.all, 20) + .background(Color.gray100) + .cornerRadius(15) + + } } - .padding(.vertical, 30) - .padding(.horizontal, 20) + .padding(.all, 20) + .frame(maxWidth: .infinity) + .overlay( + VStack { + if accountModel.devices.indices.isEmpty { + Text("manage_account_no_device") + .default_text_style_500(styleSize: 16) + } + } + .padding(.all) + ) } .background(.white) .cornerRadius(15) .padding(.horizontal) - .zIndex(-1) + .zIndex(-2) .transition(.move(edge: .top)) } @@ -463,9 +541,11 @@ struct AccountProfileFragment: View { .background(.white) .cornerRadius(15) .padding(.all) - */ } .frame(maxWidth: sharedMainViewModel.maxWidth) + .onAppear { + accountModel.requestDevicesList() + } } } .frame(maxWidth: .infinity) diff --git a/Linphone/UI/Main/Viewmodel/AccountModel.swift b/Linphone/UI/Main/Viewmodel/AccountModel.swift index f72e204d6..ebf83be6b 100644 --- a/Linphone/UI/Main/Viewmodel/AccountModel.swift +++ b/Linphone/UI/Main/Viewmodel/AccountModel.swift @@ -23,6 +23,8 @@ import SwiftUI import Combine class AccountModel: ObservableObject { + static let TAG = "[AccountModel]" + let account: Account @Published var humanReadableRegistrationState: String = "" @Published var summary: String = "" @@ -38,9 +40,14 @@ class AccountModel: ObservableObject { @Published var usernaneAvatar: String = "" @Published var imagePathAvatar: URL? + @Published var devices: [AccountDeviceModel] = [] + private var accountDelegate: AccountDelegate? private var coreDelegate: CoreDelegate? + private var accountManagerServices: AccountManagerServices? + private var requestDelegate: AccountManagerServicesRequestDelegate? + init(account: Account, core: Core) { self.account = account @@ -83,6 +90,8 @@ class AccountModel: ObservableObject { let displayName = account.displayName() let address = account.params?.identityAddress?.asString() + self.requestDevicesList() + let displayNameTmp = account.params?.identityAddress?.displayName ?? "" let usernaneAvatarTmp = account.contactAddress?.username ?? "" var photoAvatarModelTmp = "" @@ -153,4 +162,114 @@ class AccountModel: ObservableObject { return imagePath } + + func requestDevicesList() { + if account.params != nil && account.params!.identityAddress != nil, let identityAddress = account.params!.identityAddress { + Log.info( + "\(AccountModel.TAG) Request devices list for identity address \(identityAddress.asStringUriOnly())" + ) + CoreContext.shared.doOnCoreQueue { core in + do { + self.accountManagerServices = try core.createAccountManagerServices() + if self.accountManagerServices != nil { + self.accountManagerServices!.language = Locale.current.identifier + + do { + let request = try self.accountManagerServices!.createGetDevicesListRequest(sipIdentity: identityAddress) + self.addDelegate(request: request) + } catch { + print("\(AccountModel.TAG) Failed to create request: \(error.localizedDescription)") + } + } + } catch { + + } + } + } + } + + func addDelegate(request: AccountManagerServicesRequest) { + self.requestDelegate = AccountManagerServicesRequestDelegateStub( + onRequestSuccessful: { (request: AccountManagerServicesRequest, data: String) in + Log.info("\(AccountModel.TAG) Request \(request) was successful, data is \(data)") + }, onRequestError: { (request: AccountManagerServicesRequest, statusCode: Int, errorMessage: String, parameterErrors: Dictionary?) in + Log.error( + "\(AccountModel.TAG) Request \(request) returned an error with status code \(statusCode) and message \(errorMessage)" + ) + // TODO Display Error Toast + }, onDevicesListFetched: { (request: AccountManagerServicesRequest, accountDevices: [AccountDevice]) in + Log.info("\(AccountModel.TAG) Fetched \(accountDevices.count) devices for our account") + var devicesList: [AccountDeviceModel] = [] + accountDevices.forEach { accountDevice in + devicesList.append(AccountDeviceModel(accountDevice: accountDevice)) + } + + request.removeDelegate(delegate: self.requestDelegate!) + DispatchQueue.main.async { + self.devices = devicesList + } + } + ) + + request.addDelegate(delegate: self.requestDelegate!) + request.submit() + } + + func removeDevice(deviceIndex: Int) { + let removedDevice = self.devices[deviceIndex].accountDevice + self.devices.remove(at: deviceIndex) + if account.params != nil && account.params!.identityAddress != nil, let identityAddress = account.params!.identityAddress { + Log.info( + "\(AccountModel.TAG) Delete device for identity address \(identityAddress.asStringUriOnly())" + ) + CoreContext.shared.doOnCoreQueue { core in + do { + self.accountManagerServices = try core.createAccountManagerServices() + if self.accountManagerServices != nil { + self.accountManagerServices!.language = Locale.current.identifier + + do { + let request = try self.accountManagerServices!.createDeleteDeviceRequest(sipIdentity: identityAddress, device: removedDevice) + self.addDelegate(request: request) + } catch { + print("\(AccountModel.TAG) Failed to create request: \(error.localizedDescription)") + } + } + } catch { + + } + } + } + } +} + +class AccountDeviceModel: ObservableObject { + let accountDevice: AccountDevice + @Published var deviceName: String = "" + @Published var lastDate: String = "" + @Published var lastTime: String = "" + @Published var isMobileDevice: Bool = true + + init(accountDevice: AccountDevice) { + self.accountDevice = accountDevice + self.deviceName = accountDevice.name ?? "" + + let timeInterval = TimeInterval(accountDevice.lastUpdateTimestamp ?? 0) + let dateTmp = Date(timeIntervalSince1970: timeInterval) + + let dateFormat = DateFormatter() + dateFormat.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM/YYYY" : "MM/dd/YYYY" + let date = dateFormat.string(from: dateTmp) + + let dateFormatBis = DateFormatter() + dateFormatBis.dateFormat = "HH:mm" + let time = dateFormatBis.string(from: dateTmp) + + self.lastDate = date + self.lastTime = time + + self.isMobileDevice = accountDevice.userAgent.contains("LinphoneAndroid") || accountDevice.userAgent.contains( + "LinphoneiOS" + ) + } }