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"
+ )
+ }
}