From 601be3ebed54c4a12036a62a466145d338649754 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 10 Apr 2024 17:24:19 +0200 Subject: [PATCH] Add Meeting Waiting Room --- Linphone.xcodeproj/project.pbxproj | 8 + Linphone/Core/CoreContext.swift | 1 + Linphone/LinphoneApp.swift | 4 + Linphone/Localizable.xcstrings | 12 + .../TelecomManager/ProviderDelegate.swift | 2 +- Linphone/TelecomManager/TelecomManager.swift | 11 +- Linphone/UI/Call/CallView.swift | 15 +- .../UI/Call/MeetingWaitingRoomFragment.swift | 542 ++++++++++++++++++ .../UI/Call/ViewModel/CallViewModel.swift | 63 +- .../MeetingWaitingRoomViewModel.swift | 261 +++++++++ Linphone/UI/Main/ContentView.swift | 19 +- .../Fragments/HistoryListFragment.swift | 14 + Linphone/Utils/ActivityIndicator.swift | 7 +- 13 files changed, 908 insertions(+), 51 deletions(-) create mode 100644 Linphone/UI/Call/MeetingWaitingRoomFragment.swift create mode 100644 Linphone/UI/Call/ViewModel/MeetingWaitingRoomViewModel.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index a2b10c1f1..16879d82e 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -63,6 +63,8 @@ D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */; }; D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */; }; D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */; }; + D73449992BC6932A00778C56 /* MeetingWaitingRoomFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */; }; + D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */; }; D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; }; D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; }; D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */; }; @@ -200,6 +202,8 @@ D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListFragment.swift; sourceTree = ""; }; D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListViewModel.swift; sourceTree = ""; }; D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListBottomSheet.swift; sourceTree = ""; }; + D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingWaitingRoomFragment.swift; sourceTree = ""; }; + D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingWaitingRoomViewModel.swift; sourceTree = ""; }; D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = ""; }; D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = ""; }; D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = ""; }; @@ -610,6 +614,7 @@ D720E6AB2BAD81C800DDFD87 /* Model */, D7B99E972B29B37F00BE7BF2 /* ViewModel */, D7B5678D2B28888F00DE63EB /* CallView.swift */, + D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */, ); path = Call; sourceTree = ""; @@ -618,6 +623,7 @@ isa = PBXGroup; children = ( D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */, + D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -890,6 +896,7 @@ D70A26F02B7D02E6006CC8FC /* ConversationViewModel.swift in Sources */, D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */, D7E6D04D2AEBD77600A57AAF /* CustomBottomSheet.swift in Sources */, + D73449992BC6932A00778C56 /* MeetingWaitingRoomFragment.swift in Sources */, D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */, 66C491FF2B24D4AC00CEA16D /* FileUtils.swift in Sources */, D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */, @@ -898,6 +905,7 @@ D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */, D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */, D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */, + D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */, D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */, D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */, D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */, diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 98cb3105e..30adf40a9 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -109,6 +109,7 @@ final class CoreContext: ObservableObject { self.mCore.videoCaptureEnabled = true self.mCore.videoDisplayEnabled = true + self.mCore.videoPreviewEnabled = false self.mCoreSuscriptions.insert(self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in if cbVal.state == GlobalState.On { diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 6f62860e6..2bc7b715f 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -76,6 +76,7 @@ struct LinphoneApp: App { @State private var historyListViewModel: HistoryListViewModel? @State private var startCallViewModel: StartCallViewModel? @State private var callViewModel: CallViewModel? + @State private var meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel? @State private var conversationsListViewModel: ConversationsListViewModel? @State private var conversationViewModel: ConversationViewModel? @@ -99,6 +100,7 @@ struct LinphoneApp: App { && historyListViewModel != nil && startCallViewModel != nil && callViewModel != nil + && meetingWaitingRoomViewModel != nil && conversationsListViewModel != nil && conversationViewModel != nil { ContentView( @@ -108,6 +110,7 @@ struct LinphoneApp: App { historyListViewModel: historyListViewModel!, startCallViewModel: startCallViewModel!, callViewModel: callViewModel!, + meetingWaitingRoomViewModel: meetingWaitingRoomViewModel!, conversationsListViewModel: conversationsListViewModel!, conversationViewModel: conversationViewModel! ) @@ -123,6 +126,7 @@ struct LinphoneApp: App { historyListViewModel = HistoryListViewModel() startCallViewModel = StartCallViewModel() callViewModel = CallViewModel() + meetingWaitingRoomViewModel = MeetingWaitingRoomViewModel() conversationsListViewModel = ConversationsListViewModel() conversationViewModel = ConversationViewModel() } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 526a5e932..2f86da3a3 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -241,6 +241,9 @@ }, "Conditions de service" : { + }, + "Connexion à la réunion" : { + }, "Contacts" : { @@ -443,6 +446,9 @@ }, "Marquer comme non lu" : { + }, + "Mercredi 10 Avril 2024 | 2:41 PM - 2:42 PM" : { + }, "Message" : { @@ -553,6 +559,9 @@ }, "Register" : { + }, + "Rejoindre" : { + }, "Remove from favourites" : { @@ -696,6 +705,9 @@ }, "Vos communications sont en sécurité grâce aux **Chiffrement de bout en bout**." : { + }, + "Vous allez rejoindre la réunion dans quelques instants..." : { + }, "Welcome" : { diff --git a/Linphone/TelecomManager/ProviderDelegate.swift b/Linphone/TelecomManager/ProviderDelegate.swift index 7a56be324..1c7fb444e 100644 --- a/Linphone/TelecomManager/ProviderDelegate.swift +++ b/Linphone/TelecomManager/ProviderDelegate.swift @@ -329,7 +329,7 @@ extension ProviderDelegate: CXProviderDelegate { CoreContext.shared.doOnCoreQueue { core in do { core.configureAudioSession() - try TelecomManager.shared.doCall(core: core, addr: addr!, isSas: callInfo?.sasEnabled ?? false, isVideo: callInfo?.videoEnabled ?? false, isConference: callInfo?.isConference ?? false) + try TelecomManager.shared.doCall(core: core, addr: addr!, isSas: callInfo?.sasEnabled ?? false, isVideo: ((callInfo?.videoEnabled ?? false) && core.videoPreviewEnabled), isConference: callInfo?.isConference ?? false) action.fulfill() } catch { Log.info("CallKit: Call started failed because \(error)") diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index fc7eaa832..92d32421a 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -50,6 +50,9 @@ class TelecomManager: ObservableObject { @Published var refreshCallViewModel: Bool = false @Published var remainingCall: Bool = false @Published var callConnected: Bool = false + @Published var meetingWaitingRoomDisplayed: Bool = false + @Published var meetingWaitingRoomSelected: Address? + @Published var meetingWaitingRoomName: String = "" var actionToFulFill: CXCallAction? var callkitAudioSessionActivated: Bool? @@ -215,6 +218,7 @@ class TelecomManager: ObservableObject { if isConference { lcallParams.videoEnabled = true + lcallParams.videoDirection = isVideo ? MediaDirection.SendRecv : MediaDirection.RecvOnly /* if (ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! != .AudioOnly) { lcallParams.videoEnabled = true lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly @@ -393,6 +397,7 @@ class TelecomManager: ObservableObject { } } + /* if self.remoteConfVideo && self.remoteConfVideo != oldRemoteConfVideo { do { try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) @@ -400,6 +405,7 @@ class TelecomManager: ObservableObject { } } + */ if self.remoteConfVideo { Log.info("[Call] Remote video is activated") @@ -444,8 +450,11 @@ class TelecomManager: ObservableObject { self.isPausedByRemote = false } - if (cstate == Call.State.Connected) { + if cstate == Call.State.Connected { self.callConnected = true + + self.meetingWaitingRoomSelected = nil + self.meetingWaitingRoomDisplayed = false } } diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index ed87d1389..aca0212eb 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -538,7 +538,7 @@ struct CallView: View { if contactAvatarModel != nil { Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 200, hidePresence: true) .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { displayVideo = true } } @@ -554,7 +554,7 @@ struct CallView: View { .frame(width: 200, height: 200) .clipShape(Circle()) .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { displayVideo = true } } @@ -569,7 +569,7 @@ struct CallView: View { .frame(width: 200, height: 200) .clipShape(Circle()) .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { displayVideo = true } } @@ -750,7 +750,7 @@ struct CallView: View { if telecomManager.outgoingCallStarted { VStack { - ActivityIndicator() + ActivityIndicator(color: .white) .frame(width: 20, height: 20) .padding(.top, 60) @@ -772,7 +772,8 @@ struct CallView: View { } .onDisappear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - callViewModel.getConference() + // callViewModel.getConference() + callViewModel.waitingForCreatedStateConference() } } .background(.clear) @@ -799,6 +800,8 @@ struct CallView: View { angleDegree = -90 } else if orientation == .landscapeRight { angleDegree = 90 + } else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { + angleDegree = 90 } } @@ -820,6 +823,8 @@ struct CallView: View { angleDegree = -90 } else if orientation == .landscapeRight { angleDegree = 90 + } else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { + angleDegree = 90 } } diff --git a/Linphone/UI/Call/MeetingWaitingRoomFragment.swift b/Linphone/UI/Call/MeetingWaitingRoomFragment.swift new file mode 100644 index 000000000..dcb9195c1 --- /dev/null +++ b/Linphone/UI/Call/MeetingWaitingRoomFragment.swift @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2010-2020 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 AVFAudio + +struct MeetingWaitingRoomFragment: View { + + @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var telecomManager = TelecomManager.shared + + @ObservedObject var meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @State private var orientation = UIDevice.current.orientation + + let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification) + + @State var audioRouteSheet: Bool = false + @State var options: Int = 1 + @State var angleDegree = 0.0 + + var body: some View { + GeometryReader { geometry in + + if #available(iOS 16.0, *), idiom != .pad { + innerView(geometry: geometry) + .sheet(isPresented: $audioRouteSheet, onDismiss: { + audioRouteSheet = false + }) { + innerBottomSheet() + .presentationDetents([.fraction(0.3)]) + } + .onAppear { + meetingWaitingRoomViewModel.enableAVAudioSession() + + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + } catch _ { + + } + } + .onDisappear { + meetingWaitingRoomViewModel.disableAVAudioSession() + } + } else { + innerView(geometry: geometry) + .halfSheet(showSheet: $audioRouteSheet) { + innerBottomSheet() + } onDismiss: { + audioRouteSheet = false + } + .onAppear { + meetingWaitingRoomViewModel.enableAVAudioSession() + + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + } catch _ { + + } + } + .onDisappear { + meetingWaitingRoomViewModel.disableAVAudioSession() + } + } + } + } + + @ViewBuilder + func innerView(geometry: GeometryProxy) -> some View { + VStack { + if #available(iOS 16.0, *) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + } else if idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 1) + } + + HStack { + Button { + withAnimation { + meetingWaitingRoomViewModel.disableVideoPreview() + telecomManager.meetingWaitingRoomSelected = nil + telecomManager.meetingWaitingRoomDisplayed = false + } + } label: { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + + Text(telecomManager.meetingWaitingRoomName) + .default_text_style_white_800(styleSize: 16) + + Spacer() + } + .frame(height: 40) + .zIndex(1) + + HStack { + Button { + } label: { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + .hidden() + + Text("Mercredi 10 Avril 2024 | 2:41 PM - 2:42 PM") + .foregroundStyle(.white) + .default_text_style_white(styleSize: 12) + + Spacer() + } + .frame(height: 40) + .padding(.top, -25) + .zIndex(1) + + if !telecomManager.callStarted { + ZStack { + VStack { + Spacer() + + if meetingWaitingRoomViewModel.avatarDisplayed { + ZStack { + + if meetingWaitingRoomViewModel.isRemoteDeviceTrusted { + Circle() + .fill(Color.blueInfo500) + .frame(width: 206, height: 206) + } + + if meetingWaitingRoomViewModel.avatarModel != nil { + Avatar(contactAvatarModel: meetingWaitingRoomViewModel.avatarModel!, avatarSize: 200, hidePresence: true) + } else { + Image("profil-picture-default") + .resizable() + .frame(width: 200, height: 200) + .clipShape(Circle()) + } + + if meetingWaitingRoomViewModel.isRemoteDeviceTrusted { + VStack { + Spacer() + HStack { + Image("trusted") + .resizable() + .frame(width: 25, height: 25) + .padding(.all, 15) + Spacer() + } + } + .frame(width: 200, height: 200) + } + } + } + + Spacer() + } + .frame( + width: + angleDegree == 0 + ? 120 * ceil((geometry.size.width - 20) / 120) + : 160 * ceil((geometry.size.height - 160) / 160), + height: + angleDegree == 0 + ? 160 * ceil((geometry.size.width - 20) / 120) + : 120 * ceil((geometry.size.height - 160) / 160) + ) + + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + core.nativePreviewWindow = view + } + } + .frame( + width: + angleDegree == 0 + ? 120 * ceil((geometry.size.width - 20) / 120) + : 160 * ceil((geometry.size.height - 160) / 160), + height: + angleDegree == 0 + ? 160 * ceil((geometry.size.width - 20) / 120) + : 120 * ceil((geometry.size.height - 160) / 160) + ) + + VStack { + HStack { + Spacer() + + if meetingWaitingRoomViewModel.videoDisplayed { + Button { + meetingWaitingRoomViewModel.switchCamera() + } label: { + Image("camera-rotate") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 30, height: 30) + } + } + } + + Spacer() + + HStack { + Text(meetingWaitingRoomViewModel.userName) + .foregroundStyle(.white) + .default_text_style_white(styleSize: 20) + Spacer() + } + } + .padding(.all, 10) + .frame(maxWidth: geometry.size.width - 20, maxHeight: geometry.size.height - (angleDegree == 0 ? 250 : 160)) + } + .background(Color.gray600) + .frame(maxWidth: geometry.size.width - 20, maxHeight: geometry.size.height - (angleDegree == 0 ? 250 : 160)) + .cornerRadius(20) + .padding(.horizontal, 10) + .onDisappear { + meetingWaitingRoomViewModel.disableVideoPreview() + } + + if angleDegree != 0 { + Spacer() + } + + HStack { + Spacer() + + Button { + !meetingWaitingRoomViewModel.videoDisplayed ? meetingWaitingRoomViewModel.enableVideoPreview() : meetingWaitingRoomViewModel.disableVideoPreview() + } label: { + HStack { + Image(meetingWaitingRoomViewModel.videoDisplayed ? "video-camera" : "video-camera-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + .padding(.horizontal, 5) + + Button { + meetingWaitingRoomViewModel.toggleMuteMicrophone() + } label: { + HStack { + Image(meetingWaitingRoomViewModel.micMutted ? "microphone-slash" : "microphone") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + .padding(.horizontal, 5) + + Button { + if AVAudioSession.sharedInstance().availableInputs != nil + && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { + + audioRouteSheet = true + } else { + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort( + AVAudioSession.sharedInstance().currentRoute.outputs.filter( + { $0.portType.rawValue == "Speaker" } + ).isEmpty ? .speaker : .none + ) + } catch _ { + + } + } + } label: { + HStack { + Image(meetingWaitingRoomViewModel.imageAudioRoute) + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + .onAppear(perform: meetingWaitingRoomViewModel.getAudioRouteImage) + .onReceive(pub) { _ in + self.meetingWaitingRoomViewModel.getAudioRouteImage() + } + } + } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + .padding(.horizontal, 5) + + Spacer() + + if angleDegree != 0 { + Button(action: { + meetingWaitingRoomViewModel.joinMeeting() + }, label: { + Text("Rejoindre") + .default_text_style_white_600(styleSize: 20) + .frame(height: 35) + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(Color.orangeMain500) + .cornerRadius(60) + .padding(.horizontal, 10) + .frame(width: (geometry.size.width - 20) / 2) + } + } + .padding(.all, 10) + + if angleDegree == 0 { + Spacer() + + Button(action: { + meetingWaitingRoomViewModel.joinMeeting() + }, label: { + Text("Rejoindre") + .default_text_style_white_600(styleSize: 20) + .frame(height: 35) + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(Color.orangeMain500) + .cornerRadius(60) + .padding(.bottom) + .padding(.horizontal, 10) + + } + } else { + VStack { + Spacer() + + Text("Connexion à la réunion") + .default_text_style_white_600(styleSize: 24) + .multilineTextAlignment(.center) + .padding(.bottom, 10) + + + Text("Vous allez rejoindre la réunion dans quelques instants...") + .default_text_style_white(styleSize: 16) + .multilineTextAlignment(.center) + .padding(.bottom, 20) + + + ActivityIndicator(color: Color.orangeMain500) + .frame(width: 35, height: 35) + + Spacer() + } + } + + Spacer() + } + .background(Color.gray900) + .onRotate { newOrientation in + let oldOrientation = orientation + orientation = newOrientation + if orientation == .portrait || orientation == .portraitUpsideDown { + angleDegree = 0 + } else { + if orientation == .landscapeLeft { + angleDegree = -90 + } else if orientation == .landscapeRight { + angleDegree = 90 + } else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { + angleDegree = 90 + } + } + + meetingWaitingRoomViewModel.orientationUpdate(orientation: orientation) + } + .onAppear { + if orientation == .portrait || orientation == .portraitUpsideDown { + angleDegree = 0 + } else { + if orientation == .landscapeLeft { + angleDegree = -90 + } else if orientation == .landscapeRight { + angleDegree = 90 + } else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { + angleDegree = 90 + } + } + + meetingWaitingRoomViewModel.orientationUpdate(orientation: orientation) + } + } + + @ViewBuilder + func innerBottomSheet() -> some View { + VStack(spacing: 0) { + Button(action: { + options = 1 + + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + if meetingWaitingRoomViewModel.isHeadPhoneAvailable() { + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Receiver") }).first) + } else { + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.first) + } + } catch _ { + + } + }, label: { + HStack { + Image(options == 1 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + + Text(!meetingWaitingRoomViewModel.isHeadPhoneAvailable() ? "Earpiece" : "Headphones") + .default_text_style_white(styleSize: 15) + + Spacer() + + Image(!meetingWaitingRoomViewModel.isHeadPhoneAvailable() ? "ear" : "headset") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + }) + .frame(maxHeight: .infinity) + + Button(action: { + options = 2 + + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + } catch _ { + + } + }, label: { + HStack { + Image(options == 2 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + + Text("Speaker") + .default_text_style_white(styleSize: 15) + + Spacer() + + Image("speaker-high") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + }) + .frame(maxHeight: .infinity) + + Button(action: { + options = 3 + + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first) + } catch _ { + + } + }, label: { + HStack { + Image(options == 3 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + + Text("Bluetooth") + .default_text_style_white(styleSize: 15) + + Spacer() + + Image("bluetooth") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + }) + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 20) + .background(Color.gray600) + .frame(maxHeight: .infinity) + } +} + +#Preview { + MeetingWaitingRoomFragment(meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel()) +} diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index 78d0af568..8d7a86722 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -53,7 +53,6 @@ class CallViewModel: ObservableObject { @Published var activeSpeakerName: String = "" @Published var myParticipantModel: ParticipantModel? = nil - private var mConferenceSuscriptions = Set() var calls: [Call] = [] @@ -98,19 +97,10 @@ class CallViewModel: ObservableObject { self.remoteAddressString = String(self.currentCall!.remoteAddress!.asStringUriOnly().dropFirst(4)) self.remoteAddress = self.currentCall!.remoteAddress! - let friend = ContactsManager.shared.getFriendWithAddress(address: self.currentCall!.remoteAddress!) - if friend != nil && friend!.address != nil && friend!.address!.displayName != nil { - self.displayName = friend!.address!.displayName! - } else { - if self.currentCall!.remoteAddress!.displayName != nil { - self.displayName = self.currentCall!.remoteAddress!.displayName! - } else if self.currentCall!.remoteAddress!.username != nil { - self.displayName = self.currentCall!.remoteAddress!.username! - } - } + self.displayName = self.currentCall?.conference?.subject ?? "" //self.avatarModel = ??? - self.micMutted = self.currentCall!.microphoneMuted + self.micMutted = self.currentCall!.microphoneMuted || !core.micEnabled self.isRecording = self.currentCall!.params!.isRecording self.isPaused = self.isCallPaused() self.timeElapsed = self.currentCall?.duration ?? 0 @@ -128,7 +118,7 @@ class CallViewModel: ObservableObject { } self.getCallsList() - self.getConference() + self.waitingForCreatedStateConference() } self.callSuscriptions.insert(self.currentCall!.publisher?.onEncryptionChanged?.postOnMainQueue {(cbVal: (call: Call, on: Bool, authenticationToken: String?)) in @@ -157,19 +147,15 @@ class CallViewModel: ObservableObject { if self.currentCall?.callLog?.localAddress != nil { self.myParticipantModel = ParticipantModel(address: self.currentCall!.callLog!.localAddress!) - print("ParticipantModelParticipantModel myParticipantModel \(self.currentCall!.callLog!.localAddress!.asStringUriOnly())") + print("ParticipantModelParticipantModel 1 \(conf.me?.address!.asStringUriOnly())") } if conf.activeSpeakerParticipantDevice?.address != nil { self.activeSpeakerParticipant = ParticipantModel(address: conf.activeSpeakerParticipantDevice!.address!) - print("ParticipantModelParticipantModel activeSpeakerParticipantDevice 1 \(conf.activeSpeakerParticipantDevice!.address!.asStringUriOnly())") + print("ParticipantModelParticipantModel 2 \(conf.activeSpeakerParticipantDevice!.address!.asStringUriOnly())") } else if conf.participantList.first?.address != nil { self.activeSpeakerParticipant = ParticipantModel(address: conf.participantList.first!.address!) - print("ParticipantModelParticipantModel activeSpeakerParticipantDevice 2 \(conf.participantList.first!.address!.asStringUriOnly())") - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.getConference() - } + print("ParticipantModelParticipantModel 3 \(conf.participantList.first!.address!.asStringUriOnly())") } if self.activeSpeakerParticipant != nil { @@ -187,7 +173,7 @@ class CallViewModel: ObservableObject { conf.participantDeviceList.forEach({ participantDevice in self.participantList.append(ParticipantModel(address: participantDevice.address!)) - print("ParticipantModelParticipantModel participantDevice \(participantDevice.address!.asStringUriOnly())") + print("ParticipantModelParticipantModel 4 \(participantDevice.address!.asStringUriOnly()) \(conf.isIn) \(conf.state) \(self.currentCall?.state)") }) //self.addConferenceCallBacks() @@ -198,6 +184,17 @@ class CallViewModel: ObservableObject { } } + func waitingForCreatedStateConference() { + self.mConferenceSuscriptions.insert( + self.currentCall?.conference?.publisher?.onStateChanged?.postOnMainQueue {(cbValue: (conference: Conference, state: Conference.State)) in + if cbValue.state == .Created { + print("ParticipantModelParticipantModel 0 \(cbValue.conference.isIn) \(cbValue.conference.state) \(self.currentCall?.state) \(cbValue.state)") + self.getConference() + } + } + ) + } + func addConferenceCallBacks() { coreContext.doOnCoreQueue { core in self.mConferenceSuscriptions.insert( @@ -298,10 +295,16 @@ class CallViewModel: ObservableObject { } func toggleMuteMicrophone() { - coreContext.doOnCoreQueue { _ in + coreContext.doOnCoreQueue { core in if self.currentCall != nil { - self.currentCall!.microphoneMuted = !self.currentCall!.microphoneMuted - self.micMutted = self.currentCall!.microphoneMuted + if !core.micEnabled && !self.currentCall!.microphoneMuted { + core.micEnabled = true + } else { + self.currentCall!.microphoneMuted = !self.currentCall!.microphoneMuted + } + + self.micMutted = self.currentCall!.microphoneMuted || !core.micEnabled + Log.info( "[CallViewModel] Microphone mute switch \(self.micMutted)" ) @@ -436,18 +439,6 @@ class CallViewModel: ObservableObject { return false } - func getAudioRoute() -> Int { - if AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty { - if AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { - return 1 - } else { - return 3 - } - } else { - return 2 - } - } - func orientationUpdate(orientation: UIDeviceOrientation) { coreContext.doOnCoreQueue { core in let oldLinphoneOrientation = core.deviceRotation diff --git a/Linphone/UI/Call/ViewModel/MeetingWaitingRoomViewModel.swift b/Linphone/UI/Call/ViewModel/MeetingWaitingRoomViewModel.swift new file mode 100644 index 000000000..150233219 --- /dev/null +++ b/Linphone/UI/Call/ViewModel/MeetingWaitingRoomViewModel.swift @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2010-2020 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 SwiftUI +import AVFAudio + +class MeetingWaitingRoomViewModel: ObservableObject { + var coreContext = CoreContext.shared + var telecomManager = TelecomManager.shared + + @Published var userName: String = "" + @Published var avatarModel: ContactAvatarModel? + @Published var micMutted: Bool = false + @Published var isRemoteDeviceTrusted: Bool = false + @Published var selectedCall: Call? + @Published var isConference: Bool = false + @Published var videoDisplayed: Bool = false + @Published var avatarDisplayed: Bool = true + @Published var imageAudioRoute: String = "" + + init() { + do { + try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth) + } catch _ { + + } + if !telecomManager.callStarted { + self.resetMeetingRoomView() + } + } + + func resetMeetingRoomView() { + if self.telecomManager.meetingWaitingRoomSelected != nil { + coreContext.doOnCoreQueue { core in + + let conf = core.findConferenceInformationFromUri(uri: self.telecomManager.meetingWaitingRoomSelected!) + + if conf != nil && conf!.uri != nil { + let confNameTmp = conf?.subject ?? "Conference" + var userNameTmp = "" + + let friend = core.defaultAccount != nil && core.defaultAccount!.contactAddress != nil + ? ContactsManager.shared.getFriendWithAddress(address: core.defaultAccount!.contactAddress!) + : nil + + if friend != nil && friend!.address != nil && friend!.address!.displayName != nil { + userNameTmp = friend!.address!.displayName! + } else { + if core.defaultAccount!.contactAddress!.displayName != nil { + userNameTmp = core.defaultAccount!.contactAddress!.displayName! + } else if core.defaultAccount!.contactAddress!.username != nil { + userNameTmp = core.defaultAccount!.contactAddress!.username! + } + } + + let avatarModelTmp = friend != nil + ? ContactsManager.shared.avatarListModel.first(where: { + $0.friend!.name == friend!.name + && $0.friend!.address!.asStringUriOnly() == core.defaultAccount!.contactAddress!.asStringUriOnly() + }) ?? ContactAvatarModel(friend: nil, name: userNameTmp, withPresence: false) + : ContactAvatarModel(friend: nil, name: userNameTmp, withPresence: false) + + if core.videoEnabled && !core.videoPreviewEnabled { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + core.videoPreviewEnabled = true + self.videoDisplayed = true + } + } + + core.micEnabled = true + + let micMuttedTmp = !core.micEnabled + + DispatchQueue.main.async { + if self.telecomManager.meetingWaitingRoomName.isEmpty || self.telecomManager.meetingWaitingRoomName != confNameTmp { + self.telecomManager.meetingWaitingRoomName = confNameTmp + } + + self.userName = userNameTmp + self.avatarModel = avatarModelTmp + self.micMutted = micMuttedTmp + } + + } + } + } + } + + func enableVideoPreview() { + self.coreContext.doOnCoreQueue { core in + if core.videoEnabled { + self.videoDisplayed = true + core.videoPreviewEnabled = true + } + } + } + + func disableVideoPreview() { + coreContext.doOnCoreQueue { core in + if core.videoEnabled { + self.videoDisplayed = false + core.videoPreviewEnabled = false + } + } + } + + func switchCamera() { + coreContext.doOnCoreQueue { core in + let currentDevice = core.videoDevice + Log.info("[CallViewModel] Current camera device is \(currentDevice)") + + core.videoDevicesList.forEach { camera in + if camera != currentDevice && camera != "StaticImage: Static picture" { + Log.info("[CallViewModel] New camera device will be \(camera)") + do { + try core.setVideodevice(newValue: camera) + } catch _ { + + } + } + } + } + } + + func orientationUpdate(orientation: UIDeviceOrientation) { + coreContext.doOnCoreQueue { core in + let oldLinphoneOrientation = core.deviceRotation + var newRotation = 0 + switch orientation { + case .portrait: + newRotation = 0 + case .portraitUpsideDown: + newRotation = 180 + case .landscapeRight: + newRotation = 90 + case .landscapeLeft: + newRotation = 270 + default: + newRotation = oldLinphoneOrientation + } + + if oldLinphoneOrientation != newRotation { + core.deviceRotation = newRotation + } + } + } + + func enableMicrophone() { + self.micMutted = false + } + + func toggleMuteMicrophone() { + self.micMutted = !self.micMutted + } + + func enableAVAudioSession() { + do { + try AVAudioSession.sharedInstance().setActive(true) + } catch _ { + + } + } + + func disableAVAudioSession() { + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch _ { + + } + } + + func getAudioRouteImage() { + print("AVAudioSessionAVAudioSession getAudioRouteImage \(imageAudioRoute)") + imageAudioRoute = AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty + ? ( + AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty + ? ( + isHeadPhoneAvailable() + ? "headset" + : "speaker-slash" + ) + : "bluetooth" + ) + : "speaker-high" + + print("AVAudioSessionAVAudioSession getAudioRouteImage \(imageAudioRoute)") + } + + func isHeadPhoneAvailable() -> Bool { + guard let availableInputs = AVAudioSession.sharedInstance().availableInputs else {return false} + for inputDevice in availableInputs { + if inputDevice.portType == .headsetMic || inputDevice.portType == .headphones { + return true + } + } + return false + } + + func joinMeeting() { + if self.telecomManager.meetingWaitingRoomSelected != nil { + if self.micMutted { + coreContext.doOnCoreQueue { core in + core.micEnabled = false + } + } + + let audioSession = imageAudioRoute + + telecomManager.doCallWithCore( + addr: self.telecomManager.meetingWaitingRoomSelected!, isVideo: self.videoDisplayed, isConference: true + ) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + switch audioSession { + case "bluetooth": + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first) + } catch _ { + + } + case "speaker-high": + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + } catch _ { + + } + default: + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + if self.isHeadPhoneAvailable() { + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Receiver") }).first) + } else { + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.first) + } + } catch _ { + + } + } + } + } + } +} diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 412541b28..7f7fcae8e 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -40,6 +40,7 @@ struct ContentView: View { @ObservedObject var historyListViewModel: HistoryListViewModel @ObservedObject var startCallViewModel: StartCallViewModel @ObservedObject var callViewModel: CallViewModel + @ObservedObject var meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel @ObservedObject var conversationsListViewModel: ConversationsListViewModel @ObservedObject var conversationViewModel: ConversationViewModel @@ -713,7 +714,7 @@ struct ContentView: View { isShowDismissPopup: $isShowDismissPopup ) .zIndex(3) - .transition(.move(edge: .bottom)) + .transition(.opacity.combined(with: .move(edge: .bottom))) .onAppear { contactViewModel.indexDisplayedFriend = nil } @@ -729,7 +730,7 @@ struct ContentView: View { resetCallView: {callViewModel.resetCallView()} ) .zIndex(4) - .transition(.move(edge: .bottom)) + .transition(.opacity.combined(with: .move(edge: .bottom))) .sheet(isPresented: $showingDialer) { DialerBottomSheet( startCallViewModel: startCallViewModel, @@ -748,7 +749,7 @@ struct ContentView: View { resetCallView: {callViewModel.resetCallView()} ) .zIndex(4) - .transition(.move(edge: .bottom)) + .transition(.opacity.combined(with: .move(edge: .bottom))) .halfSheet(showSheet: $showingDialer) { DialerBottomSheet( startCallViewModel: startCallViewModel, @@ -856,7 +857,16 @@ struct ContentView: View { } } - if telecomManager.callDisplayed && ((telecomManager.callInProgress && telecomManager.outgoingCallStarted) || telecomManager.callConnected) { + if telecomManager.meetingWaitingRoomDisplayed { + MeetingWaitingRoomFragment(meetingWaitingRoomViewModel: meetingWaitingRoomViewModel) + .zIndex(3) + .transition(.opacity.combined(with: .move(edge: .bottom))) + .onAppear { + meetingWaitingRoomViewModel.resetMeetingRoomView() + } + } + + if telecomManager.callDisplayed && ((telecomManager.callInProgress && telecomManager.outgoingCallStarted) || telecomManager.callConnected) && !telecomManager.meetingWaitingRoomDisplayed { CallView(callViewModel: callViewModel, fullscreenVideo: $fullscreenVideo, isShowCallsListFragment: $isShowCallsListFragment, isShowStartCallFragment: $isShowStartCallFragment) .zIndex(3) .transition(.scale.combined(with: .move(edge: .top))) @@ -911,6 +921,7 @@ struct ContentView: View { historyListViewModel: HistoryListViewModel(), startCallViewModel: StartCallViewModel(), callViewModel: CallViewModel(), + meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel(), conversationsListViewModel: ConversationsListViewModel(), conversationViewModel: ConversationViewModel() ) diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift index 87c80a253..8fbaac2e5 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -215,11 +215,18 @@ struct HistoryListFragment: View { if historyListViewModel.callLogs[index].toAddress!.asStringUriOnly().hasPrefix("sip:conference-focus@sip.linphone.org") { do { //let reudumatin = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=8~YNkpFOv;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135") + /* let reutest = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=iVs8XshC~;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135") telecomManager.doCallWithCore( addr: reutest, isVideo: false, isConference: true ) + */ + + let reutest = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=iVs8XshC~;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135") + + telecomManager.meetingWaitingRoomDisplayed = true + telecomManager.meetingWaitingRoomSelected = reutest } catch {} } else { telecomManager.doCallWithCore( @@ -230,10 +237,17 @@ struct HistoryListFragment: View { if historyListViewModel.callLogs[index].fromAddress!.asStringUriOnly().hasPrefix("sip:conference-focus@sip.linphone.org") { do { //let reudumatin = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=8~YNkpFOv;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135") + /* let reutest = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=iVs8XshC~;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135") telecomManager.doCallWithCore( addr: reutest, isVideo: false, isConference: true ) + */ + + let reutest = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=iVs8XshC~;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135") + + telecomManager.meetingWaitingRoomDisplayed = true + telecomManager.meetingWaitingRoomSelected = reutest } catch {} } else { telecomManager.doCallWithCore( diff --git a/Linphone/Utils/ActivityIndicator.swift b/Linphone/Utils/ActivityIndicator.swift index 862ff4ed6..d7b386fbe 100644 --- a/Linphone/Utils/ActivityIndicator.swift +++ b/Linphone/Utils/ActivityIndicator.swift @@ -23,15 +23,14 @@ struct ActivityIndicator: View { let style = StrokeStyle(lineWidth: 3, lineCap: .round) @State var animate = false - let color1 = Color.white - let color2 = Color.white.opacity(0.5) + let color: Color var body: some View { ZStack { Circle() .trim(from: 0, to: 0.7) .stroke( - AngularGradient(gradient: .init(colors: [color1, color2]), center: .center), style: style) + AngularGradient(gradient: .init(colors: [color, color.opacity(0.5)]), center: .center), style: style) .rotationEffect(Angle(degrees: animate ? 360: 0)) .animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false), value: UUID()) }.onAppear { @@ -41,5 +40,5 @@ struct ActivityIndicator: View { } #Preview { - ActivityIndicator() + ActivityIndicator(color: .white) }