forked from mirrors/linphone-iphone
310 lines
12 KiB
Swift
310 lines
12 KiB
Swift
/*
|
|
* Copyright (c) 2010-2024 Belledonne Communications SARL.
|
|
*
|
|
* This file is part of linphone-iphone
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import Foundation
|
|
import linphonesw
|
|
import Combine
|
|
|
|
class SelectedAddressModel: ObservableObject {
|
|
var address: Address
|
|
var avatarModel: ContactAvatarModel
|
|
|
|
init (addr: Address, avModel: ContactAvatarModel) {
|
|
address = addr
|
|
avatarModel = avModel
|
|
}
|
|
}
|
|
|
|
class ScheduleMeetingViewModel: ObservableObject {
|
|
static let TAG = "[ScheduleMeetingViewModel]"
|
|
|
|
@Published var isBroadcastSelected: Bool = false
|
|
@Published var showBroadcastHelp: Bool = false
|
|
@Published var subject: String = ""
|
|
@Published var description: String = ""
|
|
@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
|
|
@Published var participantsToAdd: [SelectedAddressModel] = []
|
|
@Published var participants: [SelectedAddressModel] = []
|
|
@Published var operationInProgress: Bool = false
|
|
@Published var conferenceCreatedEvent: Bool = false
|
|
|
|
@Published var searchField: String = ""
|
|
|
|
var conferenceScheduler: ConferenceScheduler?
|
|
private var mSchedulerSubscriptions = Set<AnyCancellable?>()
|
|
var conferenceInfoToEdit: ConferenceInfo?
|
|
|
|
@Published var fromDate: Date
|
|
@Published var toDate: Date
|
|
|
|
init() {
|
|
fromDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date.now)!
|
|
toDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date.now)!
|
|
computeDateLabels()
|
|
computeTimeLabels()
|
|
updateTimezone()
|
|
}
|
|
|
|
func resetViewModelData() {
|
|
isBroadcastSelected = false
|
|
showBroadcastHelp = false
|
|
subject = ""
|
|
description = ""
|
|
allDayMeeting = false
|
|
timezone = ""
|
|
sendInvitations = true
|
|
participantsToAdd = []
|
|
participants = []
|
|
operationInProgress = false
|
|
conferenceCreatedEvent = false
|
|
searchField = ""
|
|
|
|
fromDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date.now)!
|
|
toDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date.now)!
|
|
computeDateLabels()
|
|
computeTimeLabels()
|
|
updateTimezone()
|
|
}
|
|
|
|
func computeDateLabels() {
|
|
var day = fromDate.formatted(Date.FormatStyle().weekday(.wide))
|
|
var dayNumber = fromDate.formatted(Date.FormatStyle().day(.twoDigits))
|
|
var month = fromDate.formatted(Date.FormatStyle().month(.wide))
|
|
fromDateStr = "\(day) \(dayNumber), \(month)"
|
|
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("\(ScheduleMeetingViewModel.TAG)) computed end date is \(toDateStr)")
|
|
}
|
|
|
|
func computeTimeLabels() {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a"
|
|
fromTime = formatter.string(from: fromDate)
|
|
toTime = formatter.string(from: toDate)
|
|
}
|
|
|
|
private func updateTimezone() {
|
|
// TODO
|
|
}
|
|
|
|
func selectParticipant(addr: Address) {
|
|
if let idx = participantsToAdd.firstIndex(where: {$0.address.weakEqual(address2: addr)}) {
|
|
participantsToAdd.remove(at: idx)
|
|
} else {
|
|
participantsToAdd.append(SelectedAddressModel(addr: addr, avModel: ContactAvatarModel.getAvatarModelFromAddress(address: addr)))
|
|
}
|
|
}
|
|
func addParticipants() {
|
|
var list = participants
|
|
for selectedAddr in participantsToAdd {
|
|
if let found = list.first(where: { $0.address.weakEqual(address2: selectedAddr.address) }) {
|
|
Log.info("\(ScheduleMeetingViewModel.TAG) Participant \(found.address.asStringUriOnly()) already in list, skipping")
|
|
continue
|
|
}
|
|
|
|
list.append(selectedAddr)
|
|
Log.info("\(ScheduleMeetingViewModel.TAG) Added participant \(selectedAddr.address.asStringUriOnly())")
|
|
}
|
|
Log.info("\(ScheduleMeetingViewModel.TAG) [\(list.count - participants.count) participants added, now there are \(list.count) participants in list")
|
|
|
|
participants = list
|
|
participantsToAdd = []
|
|
}
|
|
|
|
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 = true
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
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")
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|