Add devices list in Account profile

This commit is contained in:
Benoit Martins 2024-12-30 16:01:08 +01:00
parent e383826e91
commit 11053b2ca3
9 changed files with 329 additions and 18 deletions

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#4e6074" viewBox="0 0 256 256"><path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Z"></path></svg>

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#4e6074" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z"></path></svg>

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

View file

@ -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
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24h72v16H96a8,8,0,0,0,0,16h64a8,8,0,0,0,0-16H136V200h72a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40ZM48,56H208a8,8,0,0,1,8,8v80H40V64A8,8,0,0,1,48,56ZM208,184H48a8,8,0,0,1-8-8V160H216v16A8,8,0,0,1,208,184Z"></path></svg>

After

Width:  |  Height:  |  Size: 373 B

View file

@ -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
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M176,16H80A24,24,0,0,0,56,40V216a24,24,0,0,0,24,24h96a24,24,0,0,0,24-24V40A24,24,0,0,0,176,16Zm8,200a8,8,0,0,1-8,8H80a8,8,0,0,1-8-8V40a8,8,0,0,1,8-8h96a8,8,0,0,1,8,8ZM140,60a12,12,0,1,1-12-12A12,12,0,0,1,140,60Z"></path></svg>

After

Width:  |  Height:  |  Size: 335 B

View file

@ -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" : {

View file

@ -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)

View file

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