From 5b3f412bb7b8732f3b21284f22d9cde1a772e35d Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Thu, 27 Jun 2024 16:00:55 +0200 Subject: [PATCH] Implement meetings bottom sheet and meeting details delete action --- Linphone.xcodeproj/project.pbxproj | 6 + Linphone/UI/Main/ContentView.swift | 28 ++++- Linphone/UI/Main/Fragments/ToastView.swift | 7 ++ .../Meetings/Fragments/MeetingFragment.swift | 37 +++++-- .../Meetings/Fragments/MeetingsFragment.swift | 11 +- .../Fragments/MeetingsListBottomSheet.swift | 103 ++++++++++++++++++ Linphone/UI/Main/Meetings/MeetingsView.swift | 29 ++++- .../Meetings/ViewModel/MeetingViewModel.swift | 7 ++ .../ViewModel/MeetingsListViewModel.swift | 23 +++- 9 files changed, 236 insertions(+), 15 deletions(-) create mode 100644 Linphone/UI/Main/Meetings/Fragments/MeetingsListBottomSheet.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index ef7cae2fa..0c42611c7 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 4ED1F0A881A9ACB5977A8987 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; 660AAF7F2B839272004C0FA6 /* msgNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 660AAF7B2B839271004C0FA6 /* msgNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 660D8A712B517D260092694D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 660D8A702B517D260092694D /* GoogleService-Info.plist */; }; 6613A0AE2BAEB7DF008923A4 /* MeetingFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A0AD2BAEB7DF008923A4 /* MeetingFragment.swift */; }; @@ -29,6 +30,7 @@ 66E56BC92BA4A6D7006CE56F /* MeetingsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E56BC82BA4A6D7006CE56F /* MeetingsListViewModel.swift */; }; 66E56BCC2BA9A1E0006CE56F /* MeetingsListItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E56BCB2BA9A1E0006CE56F /* MeetingsListItemModel.swift */; }; 66E56BCE2BA9A1F8006CE56F /* MeetingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E56BCD2BA9A1F8006CE56F /* MeetingModel.swift */; }; + 66F08C892C2AFEF700D9AE2F /* MeetingsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F08C882C2AFEF700D9AE2F /* MeetingsListBottomSheet.swift */; }; 66F626B22BCEBB86003E2DEC /* AddParticipantsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F626B12BCEBB86003E2DEC /* AddParticipantsFragment.swift */; }; 66FBFC482B83B8CC00BC6AB1 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C492002B24DB6900CEA16D /* Log.swift */; }; 66FBFC492B83BD2400BC6AB1 /* ConfigExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491F82B24D25A00CEA16D /* ConfigExtension.swift */; }; @@ -207,6 +209,7 @@ 66E56BC82BA4A6D7006CE56F /* MeetingsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingsListViewModel.swift; sourceTree = ""; }; 66E56BCB2BA9A1E0006CE56F /* MeetingsListItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingsListItemModel.swift; sourceTree = ""; }; 66E56BCD2BA9A1F8006CE56F /* MeetingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingModel.swift; sourceTree = ""; }; + 66F08C882C2AFEF700D9AE2F /* MeetingsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingsListBottomSheet.swift; sourceTree = ""; }; 66F626B12BCEBB86003E2DEC /* AddParticipantsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsFragment.swift; sourceTree = ""; }; C60E8F182C0F649200A06DB8 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; C62817272C1B389700DBA646 /* SideMenuAccountRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuAccountRow.swift; sourceTree = ""; }; @@ -349,6 +352,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4ED1F0A881A9ACB5977A8987 /* BuildFile in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -410,6 +414,7 @@ 66E50A4A2BD12B7800AD61CA /* MeetingsFragment.swift */, 6613A0AD2BAEB7DF008923A4 /* MeetingFragment.swift */, 6613A0AF2BAEB7F4008923A4 /* MeetingsListFragment.swift */, + 66F08C882C2AFEF700D9AE2F /* MeetingsListBottomSheet.swift */, 6646A7A22BB2E224006B842A /* ScheduleMeetingFragment.swift */, 66F626B12BCEBB86003E2DEC /* AddParticipantsFragment.swift */, ); @@ -1126,6 +1131,7 @@ D7B99E9B2B29F7C300BE7BF2 /* ActivityIndicator.swift in Sources */, 66F626B22BCEBB86003E2DEC /* AddParticipantsFragment.swift in Sources */, D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */, + 66F08C892C2AFEF700D9AE2F /* MeetingsListBottomSheet.swift in Sources */, D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */, D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 8fd3d50b5..6f093534a 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -60,6 +60,7 @@ struct ContentView: View { @State var isShowEditContactFragment = false @State var isShowStartCallFragment = false @State var isShowDismissPopup = false + @State var isShowSendCancelMeetingNotificationPopup = false @State var fullscreenVideo = false @@ -530,7 +531,8 @@ struct ContentView: View { MeetingsView( meetingsListViewModel: meetingsListViewModel, meetingViewModel: meetingViewModel, - isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment + isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment, + isShowSendCancelMeetingNotificationPopup: $isShowSendCancelMeetingNotificationPopup ) } } @@ -762,7 +764,7 @@ struct ContentView: View { .background(Color.gray100) .ignoresSafeArea(.keyboard) } else if self.index == 3 { - MeetingFragment(meetingViewModel: meetingViewModel, meetingsListViewModel: meetingsListViewModel, isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment) + MeetingFragment(meetingViewModel: meetingViewModel, meetingsListViewModel: meetingsListViewModel, isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment, isShowSendCancelMeetingNotificationPopup: $isShowSendCancelMeetingNotificationPopup) .frame(maxWidth: .infinity) .background(Color.gray100) .ignoresSafeArea(.keyboard) @@ -980,6 +982,28 @@ struct ContentView: View { .onAppear { } } + + if isShowSendCancelMeetingNotificationPopup { + PopupView(isShowPopup: $isShowSendCancelMeetingNotificationPopup, + title: Text("The meeting has been cancelled"), + content: Text("Send notification to participants ?"), + titleFirstButton: Text("Cancel"), + actionFirstButton: { self.isShowSendCancelMeetingNotificationPopup.toggle() }, + titleSecondButton: Text("Ok"), + actionSecondButton: { + if let meetingToDelete = self.meetingsListViewModel.selectedMeetingToDelete { + // We're in the meeting list view + self.meetingViewModel.sendMeetingCancelledNotifications(meeting: meetingToDelete) + self.isShowSendCancelMeetingNotificationPopup.toggle() + } + }) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + self.isShowSendCancelMeetingNotificationPopup.toggle() + } + } + if telecomManager.meetingWaitingRoomDisplayed { MeetingWaitingRoomFragment(meetingWaitingRoomViewModel: meetingWaitingRoomViewModel) .zIndex(3) diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index 332e8b39b..406870977 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -130,6 +130,13 @@ struct ToastView: View { .default_text_style(styleSize: 15) .padding(8) + case "Success_toast_meeting_deleted": + Text("Successfully removed meeting") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) + case "Failed_toast_call_transfer_failed": Text("Call transfer failed!") .multilineTextAlignment(.center) diff --git a/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift b/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift index dcdc9e666..b16340c7d 100644 --- a/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift @@ -42,6 +42,7 @@ struct MeetingFragment: View { @State var addParticipantsViewModel = AddParticipantsViewModel() @Binding var isShowScheduleMeetingFragment: Bool + @Binding var isShowSendCancelMeetingNotificationPopup: Bool @ViewBuilder func getParticipantLine(participant: SelectedAddressModel) -> some View { @@ -105,13 +106,34 @@ struct MeetingFragment: View { } } } - Image("dots-three-vertical") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 25, height: 25, alignment: .leading) - .onTapGesture { + + Menu { + Button(role: .destructive) { + withAnimation { + meetingsListViewModel.selectedMeetingToDelete = meetingViewModel.displayedMeeting + meetingViewModel.displayedMeeting = nil + meetingsListViewModel.deleteSelectedMeeting() + isShowSendCancelMeetingNotificationPopup.toggle() + } + } label: { + HStack { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + Text("Delete this meeting") + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 16) + Spacer() + } } + } label: { + Image("dots-three-vertical") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + } } .frame(maxWidth: .infinity) .frame(height: 50) @@ -279,7 +301,8 @@ struct MeetingFragment: View { model.description = "description du meeting ça va être la bringue wesh wesh gros bien ou bien ça roule" return MeetingFragment(meetingViewModel: model , meetingsListViewModel: MeetingsListViewModel() - , isShowScheduleMeetingFragment: .constant(true)) + , isShowScheduleMeetingFragment: .constant(true) + , isShowSendCancelMeetingNotificationPopup: .constant(false)) } // swiftlint:enable line_length diff --git a/Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift b/Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift index 0c74e175c..f2f49aaae 100644 --- a/Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift @@ -27,8 +27,7 @@ struct MeetingsFragment: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - @State var showingSheet: Bool = false - @State var reader : ScrollViewProxy? + @Binding var showingSheet: Bool @ViewBuilder func createMonthLine(model: MeetingsListItemModel) -> some View { @@ -84,6 +83,10 @@ struct MeetingsFragment: View { } } } + .onLongPressGesture(minimumDuration: 0.2) { + meetingViewModel.displayedMeeting = model.model + showingSheet.toggle() + } } var body: some View { @@ -173,5 +176,7 @@ struct MeetingsFragment: View { } #Preview { - MeetingsFragment(meetingsListViewModel: MeetingsListViewModel(), meetingViewModel: MeetingViewModel()) + MeetingsFragment(meetingsListViewModel: MeetingsListViewModel(), + meetingViewModel: MeetingViewModel(), + showingSheet: .constant(false)) } diff --git a/Linphone/UI/Main/Meetings/Fragments/MeetingsListBottomSheet.swift b/Linphone/UI/Main/Meetings/Fragments/MeetingsListBottomSheet.swift new file mode 100644 index 000000000..cc37914d0 --- /dev/null +++ b/Linphone/UI/Main/Meetings/Fragments/MeetingsListBottomSheet.swift @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2010-2023 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 linphonesw +import Contacts + +struct MeetingsListBottomSheet: View { + + @Environment(\.dismiss) var dismiss + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @State private var orientation = UIDevice.current.orientation + + @ObservedObject var meetingsListViewModel: MeetingsListViewModel + @Binding var showingSheet: Bool + @Binding var isShowSendCancelMeetingNotificationPopup: Bool + + var body: some View { + VStack(alignment: .leading) { + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Spacer() + HStack { + Spacer() + Button("Close") { + if #available(iOS 16.0, *) { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } + } + .padding(.trailing) + } + + Button { + meetingsListViewModel.deleteSelectedMeeting() + CoreContext.shared.doOnCoreQueue { core in + if let organizerUri = self.meetingsListViewModel.selectedMeetingToDelete?.confInfo.organizer { + if core.defaultAccount?.contactAddress?.weakEqual(address2: organizerUri) ?? false { + DispatchQueue.main.async { + // If we are the organizer, display popup for sending + self.isShowSendCancelMeetingNotificationPopup = true + } + } + } + } + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.redDanger500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + Text("Delete this meeting") + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + } + .background(Color.gray100) + .frame(maxWidth: .infinity) + .onRotate { newOrientation in + orientation = newOrientation + } + } +} diff --git a/Linphone/UI/Main/Meetings/MeetingsView.swift b/Linphone/UI/Main/Meetings/MeetingsView.swift index ce663f0c8..98518402e 100644 --- a/Linphone/UI/Main/Meetings/MeetingsView.swift +++ b/Linphone/UI/Main/Meetings/MeetingsView.swift @@ -20,16 +20,40 @@ import SwiftUI struct MeetingsView: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @ObservedObject var meetingsListViewModel: MeetingsListViewModel @ObservedObject var meetingViewModel: MeetingViewModel @Binding var isShowScheduleMeetingFragment: Bool + @Binding var isShowSendCancelMeetingNotificationPopup: Bool + + @State private var showingSheet = false var body: some View { NavigationView { ZStack(alignment: .bottomTrailing) { - MeetingsFragment(meetingsListViewModel: meetingsListViewModel, meetingViewModel: meetingViewModel) + + if #available(iOS 16.0, *), idiom != .pad { + MeetingsFragment(meetingsListViewModel: meetingsListViewModel, meetingViewModel: meetingViewModel, showingSheet: $showingSheet) + .sheet(isPresented: $showingSheet) { + MeetingsListBottomSheet( + meetingsListViewModel: meetingsListViewModel, + showingSheet: $showingSheet, + isShowSendCancelMeetingNotificationPopup: $isShowSendCancelMeetingNotificationPopup + ) + .presentationDetents([.fraction(0.1)]) + } + } else { + MeetingsFragment(meetingsListViewModel: meetingsListViewModel, meetingViewModel: meetingViewModel, showingSheet: $showingSheet) + .halfSheet(showSheet: $showingSheet) { + MeetingsListBottomSheet( + meetingsListViewModel: meetingsListViewModel, + showingSheet: $showingSheet, + isShowSendCancelMeetingNotificationPopup: $isShowSendCancelMeetingNotificationPopup + ) + } onDismiss: {} + } Button { withAnimation { @@ -57,6 +81,7 @@ struct MeetingsView: View { MeetingsView( meetingsListViewModel: MeetingsListViewModel(), meetingViewModel: MeetingViewModel(), - isShowScheduleMeetingFragment: .constant(false) + isShowScheduleMeetingFragment: .constant(false), + isShowSendCancelMeetingNotificationPopup: .constant(false) ) } diff --git a/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift b/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift index 517b5d788..c937a2ed2 100644 --- a/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift +++ b/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift @@ -348,4 +348,11 @@ class MeetingViewModel: ObservableObject { } } + func sendMeetingCancelledNotifications(meeting: MeetingModel) { + Log.error("\(MeetingViewModel.TAG) - sendMeetingCancelledNotifications TODO") + //CoreContext.shared.doOnCoreQueue { core in + // self.resetConferenceSchedulerAndListeners(core: core) + // self.conferenceScheduler?.cancelConference(conferenceInfo: meeting.confInfo) + //} + } } diff --git a/Linphone/UI/Main/Meetings/ViewModel/MeetingsListViewModel.swift b/Linphone/UI/Main/Meetings/ViewModel/MeetingsListViewModel.swift index 3ba5f71bf..dad4aff90 100644 --- a/Linphone/UI/Main/Meetings/ViewModel/MeetingsListViewModel.swift +++ b/Linphone/UI/Main/Meetings/ViewModel/MeetingsListViewModel.swift @@ -27,7 +27,7 @@ class MeetingsListViewModel: ObservableObject { private var coreContext = CoreContext.shared private var mCoreSuscriptions = Set() - var selectedMeeting: ConversationModel? + var selectedMeetingToDelete: MeetingModel? @Published var meetingsList: [MeetingsListItemModel] = [] @Published var currentFilter = "" @@ -112,4 +112,25 @@ class MeetingsListViewModel: ObservableObject { } } + func deleteSelectedMeeting() { + guard let meetingToDelete = selectedMeetingToDelete else { + Log.error("\(MeetingsListViewModel.TAG) Could not delete meeting because none was selected") + return + } + + coreContext.doOnCoreQueue { core in + core.deleteConferenceInformation(conferenceInfo: meetingToDelete.confInfo) + DispatchQueue.main.async { + if let index = self.meetingsList.firstIndex(where: { $0.model?.address == meetingToDelete.address }) { + if self.todayIdx > index { + // bump todayIdx one place up + self.todayIdx -= 1 + } + self.meetingsList.remove(at: index) + ToastViewModel.shared.toastMessage = "Success_toast_meeting_deleted" + ToastViewModel.shared.displayToast = true + } + } + } + } }