From 62a027b3975fa072ee8435e67dc2cc0ee1292356 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Thu, 11 Apr 2024 17:49:27 +0200 Subject: [PATCH] Continue ScheduleMeetingViewModel impelmentation: ConferenceScheduler listeners, schedule(), update(), addparticipants(), and fillConferenceInfo() --- .../Meetings/ViewModel/MeetingViewModel.swift | 23 +- .../ViewModel/ScheduleMeetingViewModel.swift | 229 +++++++++++++++++- 2 files changed, 220 insertions(+), 32 deletions(-) diff --git a/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift b/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift index d65261290..bf612a90d 100644 --- a/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift +++ b/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift @@ -34,27 +34,8 @@ class ParticipantModel: ObservableObject { self.address = address self.sipUri = address.asStringUriOnly() - - let addressFriend = ContactsManager.shared.getFriendWithAddress(address: self.address) - - var nameTmp = "" - - if addressFriend != nil { - nameTmp = addressFriend!.name! - } else { - nameTmp = address.displayName != nil - ? address.displayName! - : address.username! - } - - self.name = nameTmp - - self.avatarModel = addressFriend != nil - ? ContactsManager.shared.avatarListModel.first(where: { - $0.friend!.name == addressFriend!.name - && $0.friend!.address!.asStringUriOnly() == address.asStringUriOnly() - }) ?? ContactAvatarModel(friend: nil, name: nameTmp, withPresence: false) - : ContactAvatarModel(friend: nil, name: nameTmp, withPresence: false) + + self.avatarModel = ContactAvatarModel.getAvatarModelFromAddress(address: self.address) } } diff --git a/Linphone/UI/Main/Meetings/ViewModel/ScheduleMeetingViewModel.swift b/Linphone/UI/Main/Meetings/ViewModel/ScheduleMeetingViewModel.swift index aa04c0ab9..9015389fb 100644 --- a/Linphone/UI/Main/Meetings/ViewModel/ScheduleMeetingViewModel.swift +++ b/Linphone/UI/Main/Meetings/ViewModel/ScheduleMeetingViewModel.swift @@ -21,26 +21,37 @@ import Foundation import linphonesw import Combine +class SelectedAddressModel { + var address: Address + var avatarModel: ContactAvatarModel? + + init (addr: Address, avModel: ContactAvatarModel?) { + address = addr + avatarModel = avModel + } +} + class ScheduleMeetingViewModel: ObservableObject { - private let TAG = "[ScheduleMeetingViewModel]" + static let TAG = "[ScheduleMeetingViewModel]" @Published var isBroadcastSelected: Bool = false @Published var showBroadcastHelp: Bool = false @Published var subject: String = "" - @Published var description: String = "" + @Published var description: String = "aaaaaa aaaaaa" @Published var allDayMeeting: Bool = false @Published var fromDateStr: String = "" @Published var fromTime: String = "" @Published var toDateStr: String = "" - @Published var toTime: String = "" - @Published var timezone: String = "" - @Published var sendInvitations: Bool = true - // var participants = MutableLiveData>() - @Published var operationInProgress: Bool = false - @Published var conferenceCreatedEvent: Bool = false + @Published var toTime: String = "" + @Published var timezone: String = "" + @Published var sendInvitations: Bool = true + @Published var participants: [SelectedAddressModel] = [] + @Published var operationInProgress: Bool = false + @Published var conferenceCreatedEvent: Bool = false var conferenceScheduler: ConferenceScheduler? - var conferenceInfoToEdit: ConferenceScheduler? + private var mSchedulerSubscriptions = Set() + var conferenceInfoToEdit: ConferenceInfo? private var fromDate: Date private var toDate: Date @@ -59,13 +70,13 @@ class ScheduleMeetingViewModel: ObservableObject { var dayNumber = fromDate.formatted(Date.FormatStyle().day(.twoDigits)) var month = fromDate.formatted(Date.FormatStyle().month(.wide)) fromDateStr = "\(day) \(dayNumber), \(month)" - Log.info("\(TAG) computed start date is \(fromDateStr)") + Log.info("\(ScheduleMeetingViewModel.TAG) computed start date is \(fromDateStr)") day = toDate.formatted(Date.FormatStyle().weekday(.wide)) dayNumber = toDate.formatted(Date.FormatStyle().day(.twoDigits)) month = toDate.formatted(Date.FormatStyle().month(.wide)) toDateStr = "\(day) \(dayNumber), \(month)" - Log.info("\(TAG) computed end date is \(toDateStr)") + Log.info("\(ScheduleMeetingViewModel.TAG)) computed end date is \(toDateStr)") } private func computeTimeLabels() { @@ -78,4 +89,200 @@ class ScheduleMeetingViewModel: ObservableObject { private func updateTimezone() { // TODO } + + func addParticipants(toAdd: [String]) { + CoreContext.shared.doOnCoreQueue { _ in + var list = self.participants + for participant in toAdd { + if let address = try? Factory.Instance.createAddress(addr: participant) { + if let found = list.first(where: { $0.address.weakEqual(address2: address) }) { + Log.info("\(ScheduleMeetingViewModel.TAG) Participant \(found.address.asStringUriOnly()) already in list, skipping") + continue + } + + let avatarModel = ContactAvatarModel.getAvatarModelFromAddress(address: address) + list.append(SelectedAddressModel(addr: address, avModel: avatarModel)) + Log.info("\(ScheduleMeetingViewModel.TAG) Added participant \(address.asStringUriOnly())") + } else { + Log.error("\(ScheduleMeetingViewModel.TAG) Failed to parse \(participant) as address!") + } + } + + Log.info("\(ScheduleMeetingViewModel.TAG) [\(toAdd.count) participants added, now there are \(list.count) participants in list") + DispatchQueue.main.async { + self.participants = list + } + } + } + + private func fillConferenceInfo(confInfo: ConferenceInfo) { + confInfo.subject = self.subject + confInfo.description = self.description + confInfo.dateTime = time_t(self.fromDate.timeIntervalSince1970) + confInfo.duration = UInt(self.fromDate.distance(to: self.toDate) / 60) + + let participantsList = self.participants + var participantsInfoList: [ParticipantInfo] = [] + for participant in participantsList { + if let info = try? Factory.Instance.createParticipantInfo(address: participant.address) { + // For meetings, all participants must have Speaker role + info.role = Participant.Role.Speaker + participantsInfoList.append(info) + } else { + Log.error("\(ScheduleMeetingViewModel.TAG) Failed to create Participant Info from address \(participant.address.asStringUriOnly())") + } + } + confInfo.participantInfos = participantsInfoList + } + + private func initConferenceSchedulerAndListeners(core: Core) { + self.conferenceScheduler = try? core.createConferenceScheduler() + + self.mSchedulerSubscriptions.insert(self.conferenceScheduler?.publisher?.onStateChanged?.postOnCoreQueue { (cbVal: (conferenceScheduler: ConferenceScheduler, state: ConferenceScheduler.State)) in + + Log.info("\(ScheduleMeetingViewModel.TAG) Conference state changed \(cbVal.state)") + if cbVal.state == ConferenceScheduler.State.Error { + DispatchQueue.main.async { + self.operationInProgress = false + // TODO: show error toast + } + } else if cbVal.state == ConferenceScheduler.State.Ready { + let conferenceAddress = self.conferenceScheduler?.info?.uri + if let confInfoToEdit = self.conferenceInfoToEdit { + Log.info("\(ScheduleMeetingViewModel.TAG) Conference info \(confInfoToEdit.uri?.asStringUriOnly() ?? "'nil'") has been updated") + } else { + Log.info("\(ScheduleMeetingViewModel.TAG) Conference info created, address will be \(conferenceAddress?.asStringUriOnly() ?? "'nil'")") + } + + if self.sendInvitations { + Log.info("\(ScheduleMeetingViewModel.TAG) User asked for invitations to be sent, let's do it") + if let chatRoomParams = try? core.createDefaultChatRoomParams() { + chatRoomParams.groupEnabled = false + chatRoomParams.backend = ChatRoom.Backend.FlexisipChat + chatRoomParams.encryptionEnabled = true + chatRoomParams.subject = "Meeting invitation" // Won't be used + self.conferenceScheduler?.sendInvitations(chatRoomParams: chatRoomParams) + } else { + Log.error("\(ScheduleMeetingViewModel.TAG) Failed to create default chatroom parameters. This should not happen") + } + } else { + Log.info("\(ScheduleMeetingViewModel.TAG) User didn't asked for invitations to be sent") + DispatchQueue.main.async { + self.operationInProgress = false + self.conferenceCreatedEvent = false + } + } + } + }) + + self.mSchedulerSubscriptions.insert(self.conferenceScheduler?.publisher?.onInvitationsSent?.postOnCoreQueue { (cbVal: (conferenceScheduler: ConferenceScheduler, failedInvitations: [Address])) in + + if cbVal.failedInvitations.isEmpty { + Log.info("\(ScheduleMeetingViewModel.TAG) All invitations have been sent") + } else if cbVal.failedInvitations.count == self.participants.count { + Log.error("\(ScheduleMeetingViewModel.TAG) No invitation sent!") + // TODO: show error toast + } else { + Log.warn("\(ScheduleMeetingViewModel.TAG) \(cbVal.failedInvitations.count) invitations couldn't have been sent for:") + for failInv in cbVal.failedInvitations { + Log.warn(failInv.asStringUriOnly()) + } + // TODO: show error toast + } + + DispatchQueue.main.async { + self.operationInProgress = false + self.conferenceCreatedEvent = true + } + }) + } + + func schedule() { + if subject.isEmpty || participants.isEmpty { + Log.error("\(ScheduleMeetingViewModel.TAG) Either no subject was set or no participant was selected, can't schedule meeting.") + // TODO: show red toast + return + } + operationInProgress = true + + CoreContext.shared.doOnCoreQueue { core in + Log.info("\(ScheduleMeetingViewModel.TAG) Scheduling \(self.isBroadcastSelected ? "broadcast" : "meeting")") + + let localAccount = core.defaultAccount + let localAddress = localAccount?.params?.identityAddress + + if let conferenceInfo = try? Factory.Instance.createConferenceInfo() { + conferenceInfo.organizer = localAddress + self.fillConferenceInfo(confInfo: conferenceInfo) + if self.conferenceScheduler == nil { + self.initConferenceSchedulerAndListeners(core: core) + } + self.conferenceScheduler?.account = localAccount + // Will trigger the conference creation automatically + self.conferenceScheduler?.info = conferenceInfo + } + } + } + + func update() { + self.operationInProgress = true + CoreContext.shared.doOnCoreQueue { core in + Log.info("\(ScheduleMeetingViewModel.TAG) Updating \(self.isBroadcastSelected ? "broadcast" : "meeting")") + + if let conferenceInfo = self.conferenceInfoToEdit { + self.fillConferenceInfo(confInfo: conferenceInfo) + if self.conferenceScheduler == nil { + self.initConferenceSchedulerAndListeners(core: core) + } + + // Will trigger the conference update automatically + self.conferenceScheduler?.info = conferenceInfo + } else { + Log.error("No conference info to edit found!") + return + } + } + } + + func loadExistingConferenceInfoFromUri(conferenceUri: String) { + CoreContext.shared.doOnCoreQueue { core in + if let conferenceAddress = core.interpretUrl(url: conferenceUri, applyInternationalPrefix: false) { + if let conferenceInfo = core.findConferenceInformationFromUri(uri: conferenceAddress) { + + self.conferenceInfoToEdit = conferenceInfo + Log.info("\(ScheduleMeetingViewModel.TAG) Found conference info matching URI \(conferenceInfo.uri?.asString()) with subject \(conferenceInfo.subject)") + + self.fromDate = Date(timeIntervalSince1970: TimeInterval(conferenceInfo.dateTime)) + self.toDate = Calendar.current.date(byAdding: .minute, value: Int(conferenceInfo.duration), to: self.fromDate)! + + var list: [SelectedAddressModel] = [] + for partInfo in conferenceInfo.participantInfos { + if let addr = partInfo.address { + let avatarModel = ContactAvatarModel.getAvatarModelFromAddress(address: addr) + list.append(SelectedAddressModel(addr: addr, avModel: avatarModel)) + Log.info("\(ScheduleMeetingViewModel.TAG) Loaded participant \(addr.asStringUriOnly())") + } + } + Log.info("\(ScheduleMeetingViewModel.TAG) \(list.count) participants loaded from found conference info") + + DispatchQueue.main.async { + self.subject = conferenceInfo.subject ?? "" + self.description = conferenceInfo.description ?? "" + self.isBroadcastSelected = false // TODO FIXME + self.computeDateLabels() + self.computeTimeLabels() + self.updateTimezone() + self.participants = list + } + + } else { + Log.error("\(ScheduleMeetingViewModel.TAG) Failed to find a conference info matching URI [${conferenceAddress.asString()}], abort") + } + } else { + Log.error("\(ScheduleMeetingViewModel.TAG) Failed to parse conference URI [$conferenceUri], abort") + } + + } + } + }