diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index e33ed4f0c..ef7cae2fa 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -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 = ""; }; 6613A0AF2BAEB7F4008923A4 /* MeetingsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingsListFragment.swift; sourceTree = ""; }; 6613A0B32BAEBE3F008923A4 /* MeetingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingViewModel.swift; sourceTree = ""; }; - 6613A0B52BAEBE5C008923A4 /* ScheduleMeetingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleMeetingViewModel.swift; sourceTree = ""; }; 66162A1F2BDFC2F900DCE913 /* AddParticipantsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsViewModel.swift; sourceTree = ""; }; 662B69D82B25DE18007118BF /* TelecomManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelecomManager.swift; sourceTree = ""; }; 662B69DA2B25DE25007118BF /* ProviderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = ""; }; @@ -423,7 +421,6 @@ children = ( 66E56BC82BA4A6D7006CE56F /* MeetingsListViewModel.swift */, 6613A0B32BAEBE3F008923A4 /* MeetingViewModel.swift */, - 6613A0B52BAEBE5C008923A4 /* ScheduleMeetingViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -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 */, diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 95cb4d7f8..a720905eb 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -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) } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 38802c523..c09d8a1e6 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -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 diff --git a/Linphone/UI/Main/Meetings/Fragments/AddParticipantsFragment.swift b/Linphone/UI/Main/Meetings/Fragments/AddParticipantsFragment.swift index 557f39be4..2466f59b2 100644 --- a/Linphone/UI/Main/Meetings/Fragments/AddParticipantsFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/AddParticipantsFragment.swift @@ -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 . + */ import SwiftUI import Foundation diff --git a/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift b/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift index e1d58118b..fd7d0c18b 100644 --- a/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift @@ -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... + */ 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()) } diff --git a/Linphone/UI/Main/Meetings/Fragments/ScheduleMeetingFragment.swift b/Linphone/UI/Main/Meetings/Fragments/ScheduleMeetingFragment.swift index 3e3a65890..f7d60b221 100644 --- a/Linphone/UI/Main/Meetings/Fragments/ScheduleMeetingFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/ScheduleMeetingFragment.swift @@ -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.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)) } diff --git a/Linphone/UI/Main/Meetings/MeetingsView.swift b/Linphone/UI/Main/Meetings/MeetingsView.swift index 9d82c8780..ce663f0c8 100644 --- a/Linphone/UI/Main/Meetings/MeetingsView.swift +++ b/Linphone/UI/Main/Meetings/MeetingsView.swift @@ -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 . + */ 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) ) } diff --git a/Linphone/UI/Main/Meetings/Models/MeetingModel.swift b/Linphone/UI/Main/Meetings/Models/MeetingModel.swift index ba89195dd..d363a2dcd 100644 --- a/Linphone/UI/Main/Meetings/Models/MeetingModel.swift +++ b/Linphone/UI/Main/Meetings/Models/MeetingModel.swift @@ -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 . + */ import linphonesw diff --git a/Linphone/UI/Main/Meetings/Models/MeetingsListItemModel.swift b/Linphone/UI/Main/Meetings/Models/MeetingsListItemModel.swift index 520a98bc9..644b118d5 100644 --- a/Linphone/UI/Main/Meetings/Models/MeetingsListItemModel.swift +++ b/Linphone/UI/Main/Meetings/Models/MeetingsListItemModel.swift @@ -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 . + */ import Foundation extension String { diff --git a/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift b/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift index 681bef225..2558ef730 100644 --- a/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift +++ b/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift @@ -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() + 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") + } + + } + } + } diff --git a/Linphone/UI/Main/Meetings/ViewModel/ScheduleMeetingViewModel.swift b/Linphone/UI/Main/Meetings/ViewModel/ScheduleMeetingViewModel.swift deleted file mode 100644 index 579b34a56..000000000 --- a/Linphone/UI/Main/Meetings/ViewModel/ScheduleMeetingViewModel.swift +++ /dev/null @@ -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 . - */ - -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() - 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") - } - - } - } - -} diff --git a/Linphone/UI/Main/Viewmodel/AddParticipantsViewModel.swift b/Linphone/UI/Main/Viewmodel/AddParticipantsViewModel.swift index 6e1f2d647..67ee27ff8 100644 --- a/Linphone/UI/Main/Viewmodel/AddParticipantsViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/AddParticipantsViewModel.swift @@ -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 . + */ import Foundation import linphonesw