Rename "ScheduleMeetingViewModel" to "MeetingViewModel"

This commit is contained in:
QuentinArguillere 2024-06-20 17:24:57 +02:00
parent 570007c2c6
commit e74b2dd4f3
13 changed files with 479 additions and 574 deletions

View file

@ -12,7 +12,6 @@
6613A0AE2BAEB7DF008923A4 /* MeetingFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A0AD2BAEB7DF008923A4 /* MeetingFragment.swift */; };
6613A0B02BAEB7F4008923A4 /* MeetingsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A0AF2BAEB7F4008923A4 /* MeetingsListFragment.swift */; };
6613A0B42BAEBE3F008923A4 /* MeetingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A0B32BAEBE3F008923A4 /* MeetingViewModel.swift */; };
6613A0B62BAEBE5C008923A4 /* ScheduleMeetingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A0B52BAEBE5C008923A4 /* ScheduleMeetingViewModel.swift */; };
66162A202BDFC2F900DCE913 /* AddParticipantsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66162A1F2BDFC2F900DCE913 /* AddParticipantsViewModel.swift */; };
662B69D92B25DE18007118BF /* TelecomManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662B69D82B25DE18007118BF /* TelecomManager.swift */; };
662B69DB2B25DE25007118BF /* ProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662B69DA2B25DE25007118BF /* ProviderDelegate.swift */; };
@ -192,7 +191,6 @@
6613A0AD2BAEB7DF008923A4 /* MeetingFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingFragment.swift; sourceTree = "<group>"; };
6613A0AF2BAEB7F4008923A4 /* MeetingsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingsListFragment.swift; sourceTree = "<group>"; };
6613A0B32BAEBE3F008923A4 /* MeetingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingViewModel.swift; sourceTree = "<group>"; };
6613A0B52BAEBE5C008923A4 /* ScheduleMeetingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleMeetingViewModel.swift; sourceTree = "<group>"; };
66162A1F2BDFC2F900DCE913 /* AddParticipantsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsViewModel.swift; sourceTree = "<group>"; };
662B69D82B25DE18007118BF /* TelecomManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelecomManager.swift; sourceTree = "<group>"; };
662B69DA2B25DE25007118BF /* ProviderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = "<group>"; };
@ -423,7 +421,6 @@
children = (
66E56BC82BA4A6D7006CE56F /* MeetingsListViewModel.swift */,
6613A0B32BAEBE3F008923A4 /* MeetingViewModel.swift */,
6613A0B52BAEBE5C008923A4 /* ScheduleMeetingViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -1109,7 +1106,6 @@
66C492012B24DB6900CEA16D /* Log.swift in Sources */,
C6A5A9432C10B5ED0070FEA4 /* DecodableExtension.swift in Sources */,
D714035B2BE11E00004BD8CA /* CallMediaEncryptionModel.swift in Sources */,
6613A0B62BAEBE5C008923A4 /* ScheduleMeetingViewModel.swift in Sources */,
D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */,
D7CEE0382B7A214F00FD79B7 /* ConversationsListViewModel.swift in Sources */,
D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */,

View file

@ -80,7 +80,7 @@ struct LinphoneApp: App {
@State private var conversationsListViewModel: ConversationsListViewModel?
@State private var conversationViewModel: ConversationViewModel?
@State private var meetingsListViewModel: MeetingsListViewModel?
@State private var scheduleMeetingViewModel: ScheduleMeetingViewModel?
@State private var meetingViewModel: MeetingViewModel?
var body: some Scene {
WindowGroup {
@ -111,7 +111,7 @@ struct LinphoneApp: App {
&& conversationsListViewModel != nil
&& conversationViewModel != nil
&& meetingsListViewModel != nil
&& scheduleMeetingViewModel != nil {
&& meetingViewModel != nil {
ContentView(
contactViewModel: contactViewModel!,
editContactViewModel: editContactViewModel!,
@ -123,7 +123,7 @@ struct LinphoneApp: App {
conversationsListViewModel: conversationsListViewModel!,
conversationViewModel: conversationViewModel!,
meetingsListViewModel: meetingsListViewModel!,
scheduleMeetingViewModel: scheduleMeetingViewModel!
meetingViewModel: meetingViewModel!
).onOpenURL { url in
URIHandler.handleURL(url: url)
}
@ -145,7 +145,7 @@ struct LinphoneApp: App {
conversationsListViewModel = ConversationsListViewModel()
conversationViewModel = ConversationViewModel()
meetingsListViewModel = MeetingsListViewModel()
scheduleMeetingViewModel = ScheduleMeetingViewModel()
meetingViewModel = MeetingViewModel()
}.onOpenURL { url in
URIHandler.handleURL(url: url)
}

View file

@ -44,7 +44,7 @@ struct ContentView: View {
@ObservedObject var conversationsListViewModel: ConversationsListViewModel
@ObservedObject var conversationViewModel: ConversationViewModel
@ObservedObject var meetingsListViewModel: MeetingsListViewModel
@ObservedObject var scheduleMeetingViewModel: ScheduleMeetingViewModel
@ObservedObject var meetingViewModel: MeetingViewModel
@State var index = 0
@State private var orientation = UIDevice.current.orientation
@ -509,7 +509,7 @@ struct ContentView: View {
} else if self.index == 3 {
MeetingsView(
meetingsListViewModel: meetingsListViewModel,
scheduleMeetingViewModel: scheduleMeetingViewModel,
meetingViewModel: meetingViewModel,
isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment
)
}
@ -699,7 +699,7 @@ struct ContentView: View {
}
if contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil || conversationViewModel.displayedConversation != nil ||
scheduleMeetingViewModel.displayedMeeting != nil
meetingViewModel.displayedMeeting != nil
{
HStack(spacing: 0) {
Spacer()
@ -742,7 +742,7 @@ struct ContentView: View {
.background(Color.gray100)
.ignoresSafeArea(.keyboard)
} else if self.index == 3 {
MeetingFragment(scheduleMeetingViewModel: scheduleMeetingViewModel, meetingsListViewModel: meetingsListViewModel, isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment)
MeetingFragment(meetingViewModel: meetingViewModel, meetingsListViewModel: meetingsListViewModel, isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment)
.frame(maxWidth: .infinity)
.background(Color.gray100)
.ignoresSafeArea(.keyboard)
@ -951,7 +951,7 @@ struct ContentView: View {
if isShowScheduleMeetingFragment {
ScheduleMeetingFragment(
scheduleMeetingViewModel: scheduleMeetingViewModel,
meetingViewModel: meetingViewModel,
meetingsListViewModel: meetingsListViewModel,
isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment
)
@ -1038,7 +1038,7 @@ struct ContentView: View {
conversationsListViewModel: ConversationsListViewModel(),
conversationViewModel: ConversationViewModel(),
meetingsListViewModel: MeetingsListViewModel(),
scheduleMeetingViewModel: ScheduleMeetingViewModel()
meetingViewModel: MeetingViewModel()
)
}
// swiftlint:enable type_body_length

View file

@ -1,9 +1,21 @@
//
// ParticipantsListFragment.swift
// Linphone
//
// Created by QuentinArguillere on 16/04/2024.
//
/*
* 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 SwiftUI
import Foundation

View file

@ -28,7 +28,7 @@ struct MeetingFragment: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State private var orientation = UIDevice.current.orientation
@ObservedObject var scheduleMeetingViewModel: ScheduleMeetingViewModel
@ObservedObject var meetingViewModel: MeetingViewModel
@ObservedObject var meetingsListViewModel: MeetingsListViewModel
@State private var showDatePicker = false
@ -87,11 +87,11 @@ struct MeetingFragment: View {
.padding(.leading, -10)
.onTapGesture {
withAnimation {
scheduleMeetingViewModel.displayedMeeting = nil
meetingViewModel.displayedMeeting = nil
}
}
Spacer()
if scheduleMeetingViewModel.myself != nil && scheduleMeetingViewModel.myself!.isOrganizer {
if meetingViewModel.myself != nil && meetingViewModel.myself!.isOrganizer {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
@ -126,7 +126,7 @@ struct MeetingFragment: View {
.foregroundStyle(Color.grayMain2c800)
.frame(width: 24, height: 24)
.padding(.leading, 15)
Text(scheduleMeetingViewModel.subject)
Text(meetingViewModel.subject)
.fontWeight(.bold)
.default_text_style(styleSize: 20)
.frame(height: 29, alignment: .leading)
@ -145,7 +145,7 @@ struct MeetingFragment: View {
.foregroundStyle(Color.grayMain2c800)
.frame(width: 24, height: 24)
.padding(.leading, 15)
Text(scheduleMeetingViewModel.conferenceUri)
Text(meetingViewModel.conferenceUri)
.underline()
.default_text_style(styleSize: 14)
Spacer()
@ -165,7 +165,7 @@ struct MeetingFragment: View {
.foregroundStyle(Color.grayMain2c800)
.frame(width: 24, height: 24)
.padding(.leading, 15)
Text(scheduleMeetingViewModel.getFullDateString())
Text(meetingViewModel.getFullDateString())
.default_text_style(styleSize: 14)
Spacer()
}
@ -195,7 +195,7 @@ struct MeetingFragment: View {
.frame(width: 24, height: 24)
.padding(.leading, 15)
Text(scheduleMeetingViewModel.description)
Text(meetingViewModel.description)
.default_text_style(styleSize: 14)
Spacer()
}.padding(.top, 10)
@ -216,11 +216,11 @@ struct MeetingFragment: View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
if scheduleMeetingViewModel.myself != nil {
getParticipantLine(participant: scheduleMeetingViewModel.myself!)
if meetingViewModel.myself != nil {
getParticipantLine(participant: meetingViewModel.myself!)
}
ForEach(0..<scheduleMeetingViewModel.participants.count, id: \.self) { index in
getParticipantLine(participant: scheduleMeetingViewModel.participants[index])
ForEach(0..<meetingViewModel.participants.count, id: \.self) { index in
getParticipantLine(participant: meetingViewModel.participants[index])
}
}
}.frame(maxHeight: 170)
@ -238,7 +238,7 @@ struct MeetingFragment: View {
Spacer()
Button(action: {
TelecomManager.shared.meetingWaitingRoomSelected = try? Factory.Instance.createAddress(addr: scheduleMeetingViewModel.displayedMeeting?.address ?? "")
TelecomManager.shared.meetingWaitingRoomSelected = try? Factory.Instance.createAddress(addr: meetingViewModel.displayedMeeting?.address ?? "")
TelecomManager.shared.meetingWaitingRoomDisplayed = true
}, label: {
Text("Join the meeting now")
@ -258,11 +258,11 @@ struct MeetingFragment: View {
}
#Preview {
let model = ScheduleMeetingViewModel()
let model = MeetingViewModel()
model.subject = "Meeting subject"
model.conferenceUri = "linphone.com/lalalal.fr"
model.description = "description du meeting ça va être la bringue wesh wesh gros bien ou bien ça roule"
return MeetingFragment(scheduleMeetingViewModel: model
return MeetingFragment(meetingViewModel: model
, meetingsListViewModel: MeetingsListViewModel()
, isShowScheduleMeetingFragment: .constant(true))
}

View file

@ -1,9 +1,21 @@
//
// MeetingsFragment.swift
// Linphone
//
// Created by QuentinArguillere on 18/04/2024.
//
/*
* 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 SwiftUI
import linphonesw
@ -11,7 +23,7 @@ import linphonesw
struct MeetingsFragment: View {
@ObservedObject var meetingsListViewModel: MeetingsListViewModel
@ObservedObject var scheduleMeetingViewModel: ScheduleMeetingViewModel
@ObservedObject var meetingViewModel: MeetingViewModel
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ -68,7 +80,7 @@ struct MeetingsFragment: View {
.onTapGesture {
withAnimation {
if let meetingModel = model.model {
scheduleMeetingViewModel.loadExistingMeeting(meeting: meetingModel)
meetingViewModel.loadExistingMeeting(meeting: meetingModel)
}
}
}
@ -172,5 +184,5 @@ struct MeetingsFragment: View {
}
#Preview {
MeetingsFragment(meetingsListViewModel: MeetingsListViewModel(), scheduleMeetingViewModel: ScheduleMeetingViewModel())
MeetingsFragment(meetingsListViewModel: MeetingsListViewModel(), meetingViewModel: MeetingViewModel())
}

View file

@ -28,7 +28,7 @@ struct ScheduleMeetingFragment: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State private var orientation = UIDevice.current.orientation
@ObservedObject var scheduleMeetingViewModel: ScheduleMeetingViewModel
@ObservedObject var meetingViewModel: MeetingViewModel
@ObservedObject var meetingsListViewModel: MeetingsListViewModel
@State private var delayedColor = Color.white
@ -74,15 +74,15 @@ struct ScheduleMeetingFragment: View {
.padding(.leading, -10)
.onTapGesture {
withAnimation {
if let meeting = scheduleMeetingViewModel.displayedMeeting {
if let meeting = meetingViewModel.displayedMeeting {
// reload meeting to cancel change from edit
scheduleMeetingViewModel.loadExistingMeeting(meeting: meeting)
meetingViewModel.loadExistingMeeting(meeting: meeting)
}
isShowScheduleMeetingFragment.toggle()
}
}
Text("\(scheduleMeetingViewModel.displayedMeeting != nil ? "Edit" : "New") meeting" )
Text("\(meetingViewModel.displayedMeeting != nil ? "Edit" : "New") meeting" )
.multilineTextAlignment(.leading)
.default_text_style_orange_800(styleSize: 16)
@ -98,17 +98,17 @@ struct ScheduleMeetingFragment: View {
Spacer()
HStack(alignment: .center) {
Button(action: {
scheduleMeetingViewModel.isBroadcastSelected.toggle()
meetingViewModel.isBroadcastSelected.toggle()
}, label: {
Image("users-three")
.renderingMode(.template)
.resizable()
.foregroundStyle(scheduleMeetingViewModel.isBroadcastSelected ? .white : Color.orangeMain500)
.foregroundStyle(meetingViewModel.isBroadcastSelected ? .white : Color.orangeMain500)
.frame(width: 25, height: 25)
})
Text("Meeting")
.default_text_style_orange_500( styleSize: 15)
.foregroundStyle(scheduleMeetingViewModel.isBroadcastSelected ? .white : Color.orangeMain500)
.foregroundStyle(meetingViewModel.isBroadcastSelected ? .white : Color.orangeMain500)
}
.padding(.horizontal, 40)
.padding(.vertical, 10)
@ -117,13 +117,13 @@ struct ScheduleMeetingFragment: View {
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(Color.orangeMain500, lineWidth: 1)
.background(scheduleMeetingViewModel.isBroadcastSelected ? Color.orangeMain500 : Color.white)
.background(meetingViewModel.isBroadcastSelected ? Color.orangeMain500 : Color.white)
)
Spacer()
HStack(alignment: .center) {
Button(action: {
scheduleMeetingViewModel.isBroadcastSelected.toggle()
meetingViewModel.isBroadcastSelected.toggle()
}, label: {
Image("slideshow")
.renderingMode(.template)
@ -153,7 +153,7 @@ struct ScheduleMeetingFragment: View {
.foregroundStyle(Color.grayMain2c800)
.frame(width: 24, height: 24)
.padding(.leading, 16)
TextField("Subject", text: $scheduleMeetingViewModel.subject)
TextField("Subject", text: $meetingViewModel.subject)
.default_text_style_700(styleSize: 20)
.frame(height: 29, alignment: .leading)
Spacer()
@ -171,43 +171,43 @@ struct ScheduleMeetingFragment: View {
.foregroundStyle(Color.grayMain2c800)
.frame(width: 24, height: 24)
.padding(.leading, 16)
Text(scheduleMeetingViewModel.fromDateStr)
Text(meetingViewModel.fromDateStr)
.fontWeight(.bold)
.default_text_style_500(styleSize: 16)
.onTapGesture {
setFromDate = true
selectedDate = scheduleMeetingViewModel.fromDate
selectedDate = meetingViewModel.fromDate
showDatePicker.toggle()
}
Spacer()
}
if !scheduleMeetingViewModel.allDayMeeting {
if !meetingViewModel.allDayMeeting {
HStack(spacing: 8) {
Text(scheduleMeetingViewModel.fromTime)
Text(meetingViewModel.fromTime)
.fontWeight(.bold)
.padding(.leading, 48)
.frame(height: 29, alignment: .leading)
.default_text_style_500(styleSize: 16)
.opacity(scheduleMeetingViewModel.allDayMeeting ? 0 : 1)
.opacity(meetingViewModel.allDayMeeting ? 0 : 1)
.onTapGesture {
setFromDate = true
selectedDate = scheduleMeetingViewModel.fromDate
selectedDate = meetingViewModel.fromDate
showTimePicker.toggle()
}
Text(scheduleMeetingViewModel.toTime)
Text(meetingViewModel.toTime)
.fontWeight(.bold)
.padding(.leading, 8)
.frame(height: 29, alignment: .leading)
.default_text_style_500(styleSize: 16)
.opacity(scheduleMeetingViewModel.allDayMeeting ? 0 : 1)
.opacity(meetingViewModel.allDayMeeting ? 0 : 1)
.onTapGesture {
setFromDate = false
selectedDate = scheduleMeetingViewModel.toDate
selectedDate = meetingViewModel.toDate
showTimePicker.toggle()
}
Spacer()
Toggle("", isOn: $scheduleMeetingViewModel.allDayMeeting)
Toggle("", isOn: $meetingViewModel.allDayMeeting)
.labelsHidden()
.tint(Color.orangeMain300)
Text("All day")
@ -222,16 +222,16 @@ struct ScheduleMeetingFragment: View {
.foregroundStyle(Color.grayMain2c800)
.frame(width: 24, height: 24)
.padding(.leading, 16)
Text(scheduleMeetingViewModel.toDateStr)
Text(meetingViewModel.toDateStr)
.fontWeight(.bold)
.default_text_style_500(styleSize: 16)
.onTapGesture {
setFromDate = false
selectedDate = scheduleMeetingViewModel.toDate
selectedDate = meetingViewModel.toDate
showDatePicker.toggle()
}
Spacer()
Toggle("", isOn: $scheduleMeetingViewModel.allDayMeeting)
Toggle("", isOn: $meetingViewModel.allDayMeeting)
.labelsHidden()
.tint(Color.orangeMain300)
Text("All day")
@ -279,7 +279,7 @@ struct ScheduleMeetingFragment: View {
.frame(width: 24, height: 24)
.padding(.leading, 16)
TextField("Add a description", text: $scheduleMeetingViewModel.description)
TextField("Add a description", text: $meetingViewModel.description)
.default_text_style_700(styleSize: 16)
}
@ -290,9 +290,9 @@ struct ScheduleMeetingFragment: View {
VStack {
NavigationLink(destination: {
AddParticipantsFragment(addParticipantsViewModel: addParticipantsViewModel, confirmAddParticipantsFunc: scheduleMeetingViewModel.addParticipants)
AddParticipantsFragment(addParticipantsViewModel: addParticipantsViewModel, confirmAddParticipantsFunc: meetingViewModel.addParticipants)
.onAppear {
addParticipantsViewModel.participantsToAdd = scheduleMeetingViewModel.participants
addParticipantsViewModel.participantsToAdd = meetingViewModel.participants
}
}, label: {
HStack(alignment: .center, spacing: 8) {
@ -310,20 +310,20 @@ struct ScheduleMeetingFragment: View {
}
})
if !scheduleMeetingViewModel.participants.isEmpty {
if !meetingViewModel.participants.isEmpty {
ScrollView {
ForEach(0..<scheduleMeetingViewModel.participants.count, id: \.self) { index in
ForEach(0..<meetingViewModel.participants.count, id: \.self) { index in
VStack {
HStack {
Avatar(contactAvatarModel: scheduleMeetingViewModel.participants[index].avatarModel, avatarSize: 50)
Avatar(contactAvatarModel: meetingViewModel.participants[index].avatarModel, avatarSize: 50)
.padding(.leading, 20)
Text(scheduleMeetingViewModel.participants[index].avatarModel.name)
Text(meetingViewModel.participants[index].avatarModel.name)
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
Button(action: {
scheduleMeetingViewModel.participants.remove(at: index)
meetingViewModel.participants.remove(at: index)
}, label: {
Image("x")
.renderingMode(.template)
@ -344,7 +344,7 @@ struct ScheduleMeetingFragment: View {
.background(Color.gray200)
HStack(spacing: 8) {
Toggle("", isOn: $scheduleMeetingViewModel.sendInvitations)
Toggle("", isOn: $meetingViewModel.sendInvitations)
.padding(.leading, 16)
.labelsHidden()
.tint(Color.orangeMain300)
@ -358,7 +358,7 @@ struct ScheduleMeetingFragment: View {
Button {
withAnimation {
scheduleMeetingViewModel.schedule()
meetingViewModel.schedule()
}
} label: {
Image("check")
@ -371,7 +371,7 @@ struct ScheduleMeetingFragment: View {
}
.padding()
if scheduleMeetingViewModel.operationInProgress {
if meetingViewModel.operationInProgress {
HStack {
Spacer()
VStack {
@ -384,7 +384,7 @@ struct ScheduleMeetingFragment: View {
Spacer()
}.onDisappear {
withAnimation {
if scheduleMeetingViewModel.conferenceCreatedEvent {
if meetingViewModel.conferenceCreatedEvent {
meetingsListViewModel.computeMeetingsList()
isShowScheduleMeetingFragment.toggle()
}
@ -463,26 +463,26 @@ struct ScheduleMeetingFragment: View {
}
func pickDate() {
let duration = min(scheduleMeetingViewModel.fromDate.distance(to: scheduleMeetingViewModel.toDate), 86400) // Limit auto correction of dates to 24h
let duration = min(meetingViewModel.fromDate.distance(to: meetingViewModel.toDate), 86400) // Limit auto correction of dates to 24h
if setFromDate {
scheduleMeetingViewModel.fromDate = selectedDate
meetingViewModel.fromDate = selectedDate
// If new startdate is after previous end date, bump up the end date
if selectedDate > scheduleMeetingViewModel.toDate {
scheduleMeetingViewModel.toDate = Calendar.current.date(byAdding: .second, value: Int(duration), to: selectedDate)!
if selectedDate > meetingViewModel.toDate {
meetingViewModel.toDate = Calendar.current.date(byAdding: .second, value: Int(duration), to: selectedDate)!
}
} else {
scheduleMeetingViewModel.toDate = selectedDate
if selectedDate < scheduleMeetingViewModel.fromDate {
meetingViewModel.toDate = selectedDate
if selectedDate < meetingViewModel.fromDate {
// If new end date is before the previous start date, bump down the start date to the earlier possible from current time
if (Date.now.distance(to: selectedDate) < duration) {
scheduleMeetingViewModel.fromDate = Date.now
meetingViewModel.fromDate = Date.now
} else {
scheduleMeetingViewModel.fromDate = Calendar.current.date(byAdding: .second, value: (-1)*Int(duration), to: selectedDate)!
meetingViewModel.fromDate = Calendar.current.date(byAdding: .second, value: (-1)*Int(duration), to: selectedDate)!
}
}
}
scheduleMeetingViewModel.computeDateLabels()
scheduleMeetingViewModel.computeTimeLabels()
meetingViewModel.computeDateLabels()
meetingViewModel.computeTimeLabels()
}
@Sendable private func delayColor() async {
@ -499,7 +499,7 @@ struct ScheduleMeetingFragment: View {
}
#Preview {
ScheduleMeetingFragment(scheduleMeetingViewModel: ScheduleMeetingViewModel()
ScheduleMeetingFragment(meetingViewModel: MeetingViewModel()
, meetingsListViewModel: MeetingsListViewModel()
, isShowScheduleMeetingFragment: .constant(true))
}

View file

@ -1,27 +1,39 @@
//
// MeetingsView.swift
// Linphone
//
// Created by QuentinArguillere on 18/04/2024.
//
/*
* 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 SwiftUI
struct MeetingsView: View {
@ObservedObject var meetingsListViewModel: MeetingsListViewModel
@ObservedObject var scheduleMeetingViewModel: ScheduleMeetingViewModel
@ObservedObject var meetingViewModel: MeetingViewModel
@Binding var isShowScheduleMeetingFragment: Bool
var body: some View {
NavigationView {
ZStack(alignment: .bottomTrailing) {
MeetingsFragment(meetingsListViewModel: meetingsListViewModel, scheduleMeetingViewModel: scheduleMeetingViewModel)
MeetingsFragment(meetingsListViewModel: meetingsListViewModel, meetingViewModel: meetingViewModel)
Button {
withAnimation {
scheduleMeetingViewModel.resetViewModelData()
meetingViewModel.resetViewModelData()
isShowScheduleMeetingFragment.toggle()
}
} label: {
@ -44,7 +56,7 @@ struct MeetingsView: View {
#Preview {
MeetingsView(
meetingsListViewModel: MeetingsListViewModel(),
scheduleMeetingViewModel: ScheduleMeetingViewModel(),
meetingViewModel: MeetingViewModel(),
isShowScheduleMeetingFragment: .constant(false)
)
}

View file

@ -1,9 +1,21 @@
//
// MeetingModel.swift
// Linphone
//
// Created by QuentinArguillere on 19/03/2024.
//
/*
* 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 linphonesw

View file

@ -1,9 +1,21 @@
//
// MeetingsListItemModel.swift
// Linphone
//
// Created by QuentinArguillere on 19/03/2024.
//
/*
* 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
extension String {

View file

@ -19,145 +19,321 @@
import Foundation
import linphonesw
import Combine
class MeetingViewModel: ObservableObject {
static let TAG = "[Meeting ViewModel]"
/*
private var coreContext = CoreContext.shared
static let TAG = "[MeetingViewModel]"
@Published var showBackbutton: Bool = false
@Published var isBroadcast: Bool = false
@Published var isEditable: Bool = false
@Published var isBroadcastSelected: Bool = false
@Published var showBroadcastHelp: Bool = false
@Published var subject: String = ""
@Published var sipUri: String = ""
@Published var description: 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 startDate: Date?
@Published var endDate: Date?
@Published var dateTime: String = ""
@Published var sendInvitations: Bool = true
@Published var participants: [SelectedAddressModel] = []
@Published var operationInProgress: Bool = false
@Published var conferenceCreatedEvent: Bool = false
@Published var conferenceUri: String = ""
@Published var speakers: [ParticipantModel] = []
@Published var participants: [ParticipantModel] = []
@Published var conferenceInfoFoundEvent: Bool = false
var conferenceScheduler: ConferenceScheduler?
private var mSchedulerSubscriptions = Set<AnyCancellable?>()
var conferenceInfoToEdit: ConferenceInfo?
@Published var displayedMeeting: MeetingModel? // if nil, then we are currently creating a new meeting
@Published var myself: SelectedAddressModel?
@Published var fromDate: Date
@Published var toDate: Date
@Published var errorMsg: String = ""
var meetingModel: MeetingModel
init(model: MeetingModel) {
meetingModel = model
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 findConferenceInfo(uri: String) {
coreContext.doOnCoreQueue { core in
var confInfoFound = false
if let address = try? Factory.Instance.createAddress(addr: uri) {
if let confInfo = core.findConferenceInformationFromUri(uri: address) {
Log.info("\(MeetingViewModel.TAG) Conference info with SIP URI \(uri) was found")
self.meetingModel.confInfo = confInfo
self.configureConferenceInfo(core: core)
confInfoFound = true
} else {
Log.error("\(MeetingViewModel.TAG) Conference info with SIP URI \(uri) couldn't be found!")
confInfoFound = false
}
} else {
Log.error("\(MeetingViewModel.TAG) Failed to parse SIP URI \(uri) as Address!")
confInfoFound = false
}
DispatchQueue.main.sync {
self.conferenceInfoFoundEvent = confInfoFound
}
}
func resetViewModelData() {
isBroadcastSelected = false
showBroadcastHelp = false
subject = ""
description = ""
allDayMeeting = false
timezone = ""
sendInvitations = true
participants = []
operationInProgress = false
conferenceCreatedEvent = false
conferenceUri = ""
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()
}
private func configureConferenceInfo(core: Core) {
/*
timezone.postValue(
AppUtils.getFormattedString(
R.string.meeting_schedule_timezone_title,
TimeZone.getDefault().displayName
)
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
)
*/
var isEditable = false
if let organizerAddress = meetingModel.confInfo.organizer {
let localAccount = core.accountList.first(where: {
if let address = $0.params?.identityAddress {
return organizerAddress.weakEqual(address2: address)
} else {
return false
}
})
isEditable = localAccount != nil
} else {
Log.error("\(MeetingViewModel.TAG) No organizer SIP URI found for: \(meetingModel.confInfo.uri?.asStringUriOnly() ?? "(empty)")")
}
let startDate = Date(timeIntervalSince1970: TimeInterval(meetingModel.confInfo.dateTime))
let endDate = Calendar.current.date(byAdding: .minute, value: Int(meetingModel.confInfo.duration), to: startDate)!
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("\(MeetingViewModel.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("\(MeetingViewModel.TAG)) computed end date is \(toDateStr)")
}
func computeTimeLabels() {
let formatter = DateFormatter()
formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a"
let startTime = formatter.string(from: startDate)
let endTime = formatter.string(from: endDate)
let dateTime = "\(startTime) - \(endTime)"
DispatchQueue.main.async {
self.subject = self.meetingModel.confInfo.subject ?? ""
self.sipUri = self.meetingModel.confInfo.uri?.asStringUriOnly() ?? ""
self.description = self.meetingModel.confInfo.description
self.startDate = startDate
self.endDate = endDate
self.dateTime = dateTime
self.isEditable = isEditable
}
self.computeParticipantsList()
fromTime = formatter.string(from: fromDate)
toTime = formatter.string(from: toDate)
}
private func computeParticipantsList() {
var speakersList: [ParticipantModel] = []
var participantsList: [ParticipantModel] = []
var allSpeaker = true
let organizer = meetingModel.confInfo.organizer
var organizerFound = false
for pInfo in meetingModel.confInfo.participantInfos {
if let participantAddress = pInfo.address {
let isOrganizer = organizer != nil && organizer!.weakEqual(address2: participantAddress)
Log.info("\(MeetingViewModel.TAG) Conference \(meetingModel.confInfo.subject)[${conferenceInfo.subject}] \(isOrganizer ? "organizer: " : "participant: ") \(participantAddress.asStringUriOnly()) is a \(pInfo.role)")
if isOrganizer {
organizerFound = true
}
if pInfo.role == Participant.Role.Listener {
allSpeaker = false
participantsList.append(ParticipantModel(address: participantAddress))
} else {
speakersList.append(ParticipantModel(address: participantAddress))
}
func getFullDateString() -> String {
var day = fromDate.formatted(Date.FormatStyle().weekday(.abbreviated))
var dayNumber = fromDate.formatted(Date.FormatStyle().day(.twoDigits))
var month = fromDate.formatted(Date.FormatStyle().month(.wide))
var year = fromDate.formatted(Date.FormatStyle().year(.defaultDigits))
return "\(day). \(dayNumber) \(month) \(year) | \(allDayMeeting ? "All day" : "\(fromTime) - \(toTime)")"
}
private func updateTimezone() {
// TODO
}
func addParticipants(participantsToAdd: [SelectedAddressModel]) {
var list = participants
for selectedAddr in participantsToAdd {
if let found = list.first(where: { $0.address.weakEqual(address2: selectedAddr.address) }) {
Log.info("\(MeetingViewModel.TAG) Participant \(found.address.asStringUriOnly()) already in list, skipping")
continue
}
list.append(selectedAddr)
Log.info("\(MeetingViewModel.TAG) Added participant \(selectedAddr.address.asStringUriOnly())")
}
Log.info("\(MeetingViewModel.TAG) [\(list.count - participants.count) participants added, now there are \(list.count) participants in list")
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("\(MeetingViewModel.TAG) Failed to create Participant Info from address \(participant.address.asStringUriOnly())")
}
}
confInfo.participantInfos = participantsInfoList
}
private func initConferenceSchedulerAndListeners(core: Core) {
self.conferenceScheduler = try? core.createConferenceScheduler()
if allSpeaker {
Log.info("$TAG All participants have Speaker role, considering it is a meeting")
participantsList = speakersList
self.mSchedulerSubscriptions.insert(self.conferenceScheduler?.publisher?.onStateChanged?.postOnCoreQueue { (cbVal: (conferenceScheduler: ConferenceScheduler, state: ConferenceScheduler.State)) in
Log.info("\(MeetingViewModel.TAG) Conference state changed \(cbVal.state)")
if cbVal.state == ConferenceScheduler.State.Error {
DispatchQueue.main.async {
self.operationInProgress = false
self.errorMsg = (self.displayedMeeting != nil) ? "Could not edit conference" : "Could not create conference"
// TODO: show error toast
}
} else if cbVal.state == ConferenceScheduler.State.Ready {
let conferenceAddress = self.conferenceScheduler?.info?.uri
if let confInfoToEdit = self.conferenceInfoToEdit {
Log.info("\(MeetingViewModel.TAG) Conference info \(confInfoToEdit.uri?.asStringUriOnly() ?? "'nil'") has been updated")
} else {
Log.info("\(MeetingViewModel.TAG) Conference info created, address will be \(conferenceAddress?.asStringUriOnly() ?? "'nil'")")
}
if self.sendInvitations {
Log.info("\(MeetingViewModel.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("\(MeetingViewModel.TAG) Failed to create default chatroom parameters. This should not happen")
}
} else {
Log.info("\(MeetingViewModel.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("\(MeetingViewModel.TAG) All invitations have been sent")
} else if cbVal.failedInvitations.count == self.participants.count {
Log.error("\(MeetingViewModel.TAG) No invitation sent!")
// TODO: show error toast
} else {
Log.warn("\(MeetingViewModel.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("\(MeetingViewModel.TAG) Either no subject was set or no participant was selected, can't schedule meeting.")
// TODO: show red toast
return
}
operationInProgress = true
if !organizerFound, let organizerAddress = organizer {
Log.info("$TAG Organizer not found in participants list, adding it to participants list")
participantsList.append(ParticipantModel(address: organizerAddress))
}
DispatchQueue.main.async {
self.isBroadcast = !allSpeaker
self.speakers = speakersList
self.participants = participantsList
CoreContext.shared.doOnCoreQueue { core in
Log.info("\(MeetingViewModel.TAG) Scheduling \(self.isBroadcastSelected ? "broadcast" : "meeting")")
if let conferenceInfo = self.displayedMeeting != nil ? self.displayedMeeting!.confInfo : try? Factory.Instance.createConferenceInfo() {
let localAccount = core.defaultAccount
conferenceInfo.organizer = localAccount?.params?.identityAddress
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("\(MeetingViewModel.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 loadExistingMeeting(meeting: MeetingModel) {
DispatchQueue.main.async {
self.resetViewModelData()
self.subject = meeting.confInfo.subject ?? ""
self.description = meeting.confInfo.description ?? ""
self.fromDate = meeting.meetingDate
self.toDate = meeting.endDate
self.participants = []
let organizer = meeting.confInfo.organizer
CoreContext.shared.doOnCoreQueue { core in
if let myAddr = core.defaultAccount?.contactAddress {
ContactAvatarModel.getAvatarModelFromAddress(address: myAddr) { avatarResult in
DispatchQueue.main.async {
let isOrganizer = (organizer != nil) ? myAddr.weakEqual(address2: organizer!) : false
self.myself = SelectedAddressModel(addr: myAddr, avModel: avatarResult, isOrg: isOrganizer)
}
}
}
}
for pInfo in meeting.confInfo.participantInfos {
if let addr = pInfo.address {
ContactAvatarModel.getAvatarModelFromAddress(address: addr) { avatarResult in
DispatchQueue.main.async {
let isOrganizer = (organizer != nil) ? addr.weakEqual(address2: organizer!) : false
self.participants.append(SelectedAddressModel(addr: addr, avModel: avatarResult, isOrg:isOrganizer))
}
}
}
}
self.conferenceUri = meeting.confInfo.uri?.asStringUriOnly() ?? ""
self.computeDateLabels()
self.computeTimeLabels()
self.updateTimezone()
self.displayedMeeting = meeting
}
}
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("\(MeetingViewModel.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 {
ContactAvatarModel.getAvatarModelFromAddress(address: addr) { avatarResult in
let avatarModel = avatarResult
self.participants.append(SelectedAddressModel(addr: addr, avModel: avatarModel))
Log.info("\(MeetingViewModel.TAG) Loaded participant \(addr.asStringUriOnly())")
}
}
}
Log.info("\(MeetingViewModel.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("\(MeetingViewModel.TAG) Failed to find a conference info matching URI [${conferenceAddress.asString()}], abort")
}
} else {
Log.error("\(MeetingViewModel.TAG) Failed to parse conference URI [$conferenceUri], abort")
}
}
}
}

View file

@ -1,339 +0,0 @@
/*
* 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 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 participants: [SelectedAddressModel] = []
@Published var operationInProgress: Bool = false
@Published var conferenceCreatedEvent: Bool = false
@Published var conferenceUri: String = ""
var conferenceScheduler: ConferenceScheduler?
private var mSchedulerSubscriptions = Set<AnyCancellable?>()
var conferenceInfoToEdit: ConferenceInfo?
@Published var displayedMeeting: MeetingModel? // if nil, then we are currently creating a new meeting
@Published var myself: SelectedAddressModel?
@Published var fromDate: Date
@Published var toDate: Date
@Published var errorMsg: String = ""
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
participants = []
operationInProgress = false
conferenceCreatedEvent = false
conferenceUri = ""
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)
}
func getFullDateString() -> String {
var day = fromDate.formatted(Date.FormatStyle().weekday(.abbreviated))
var dayNumber = fromDate.formatted(Date.FormatStyle().day(.twoDigits))
var month = fromDate.formatted(Date.FormatStyle().month(.wide))
var year = fromDate.formatted(Date.FormatStyle().year(.defaultDigits))
return "\(day). \(dayNumber) \(month) \(year) | \(allDayMeeting ? "All day" : "\(fromTime) - \(toTime)")"
}
private func updateTimezone() {
// TODO
}
func addParticipants(participantsToAdd: [SelectedAddressModel]) {
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
}
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
self.errorMsg = (self.displayedMeeting != nil) ? "Could not edit conference" : "Could not create conference"
// 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")")
if let conferenceInfo = self.displayedMeeting != nil ? self.displayedMeeting!.confInfo : try? Factory.Instance.createConferenceInfo() {
let localAccount = core.defaultAccount
conferenceInfo.organizer = localAccount?.params?.identityAddress
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 loadExistingMeeting(meeting: MeetingModel) {
DispatchQueue.main.async {
self.resetViewModelData()
self.subject = meeting.confInfo.subject ?? ""
self.description = meeting.confInfo.description ?? ""
self.fromDate = meeting.meetingDate
self.toDate = meeting.endDate
self.participants = []
let organizer = meeting.confInfo.organizer
CoreContext.shared.doOnCoreQueue { core in
if let myAddr = core.defaultAccount?.contactAddress {
ContactAvatarModel.getAvatarModelFromAddress(address: myAddr) { avatarResult in
DispatchQueue.main.async {
let isOrganizer = (organizer != nil) ? myAddr.weakEqual(address2: organizer!) : false
self.myself = SelectedAddressModel(addr: myAddr, avModel: avatarResult, isOrg: isOrganizer)
}
}
}
}
for pInfo in meeting.confInfo.participantInfos {
if let addr = pInfo.address {
ContactAvatarModel.getAvatarModelFromAddress(address: addr) { avatarResult in
DispatchQueue.main.async {
let isOrganizer = (organizer != nil) ? addr.weakEqual(address2: organizer!) : false
self.participants.append(SelectedAddressModel(addr: addr, avModel: avatarResult, isOrg:isOrganizer))
}
}
}
}
self.conferenceUri = meeting.confInfo.uri?.asStringUriOnly() ?? ""
self.computeDateLabels()
self.computeTimeLabels()
self.updateTimezone()
self.displayedMeeting = meeting
}
}
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 {
ContactAvatarModel.getAvatarModelFromAddress(address: addr) { avatarResult in
let avatarModel = avatarResult
self.participants.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")
}
}
}
}

View file

@ -1,9 +1,21 @@
//
// AddParticipantsViewModel.swift
// Linphone
//
// Created by QuentinArguillere on 29/04/2024.
//
/*
* 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