From 1f0c3fa5f72cce2b1ee61ba57f0803ba3064fd7a Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 29 Apr 2024 16:03:40 +0200 Subject: [PATCH] Add mosaic mode to conference call view --- .../picture-in-picture.imageset/Contents.json | 21 + .../picture-in-picture.svg | 1 + .../plus.imageset/Contents.json | 21 + .../Assets.xcassets/plus.imageset/plus.svg | 1 + .../squares-four.imageset/Contents.json | 21 + .../squares-four.imageset/squares-four.svg | 1 + .../waveform.imageset/Contents.json | 21 + .../waveform.imageset/waveform.svg | 1 + Linphone/Localizable.xcstrings | 9 + Linphone/UI/Call/CallView.swift | 1294 ++++++++++++----- .../Fragments/ParticipantsListFragment.swift | 21 + Linphone/UI/Call/Model/ParticipantModel.swift | 4 +- .../UI/Call/ViewModel/CallViewModel.swift | 119 +- 13 files changed, 1165 insertions(+), 370 deletions(-) create mode 100644 Linphone/Assets.xcassets/picture-in-picture.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/picture-in-picture.imageset/picture-in-picture.svg create mode 100644 Linphone/Assets.xcassets/plus.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/plus.imageset/plus.svg create mode 100644 Linphone/Assets.xcassets/squares-four.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/squares-four.imageset/squares-four.svg create mode 100644 Linphone/Assets.xcassets/waveform.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/waveform.imageset/waveform.svg diff --git a/Linphone/Assets.xcassets/picture-in-picture.imageset/Contents.json b/Linphone/Assets.xcassets/picture-in-picture.imageset/Contents.json new file mode 100644 index 000000000..44e023b13 --- /dev/null +++ b/Linphone/Assets.xcassets/picture-in-picture.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "picture-in-picture.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/picture-in-picture.imageset/picture-in-picture.svg b/Linphone/Assets.xcassets/picture-in-picture.imageset/picture-in-picture.svg new file mode 100644 index 000000000..4a7ab8304 --- /dev/null +++ b/Linphone/Assets.xcassets/picture-in-picture.imageset/picture-in-picture.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/plus.imageset/Contents.json b/Linphone/Assets.xcassets/plus.imageset/Contents.json new file mode 100644 index 000000000..16eddb498 --- /dev/null +++ b/Linphone/Assets.xcassets/plus.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "plus.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/plus.imageset/plus.svg b/Linphone/Assets.xcassets/plus.imageset/plus.svg new file mode 100644 index 000000000..79c378c6b --- /dev/null +++ b/Linphone/Assets.xcassets/plus.imageset/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/squares-four.imageset/Contents.json b/Linphone/Assets.xcassets/squares-four.imageset/Contents.json new file mode 100644 index 000000000..9fa7f7893 --- /dev/null +++ b/Linphone/Assets.xcassets/squares-four.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "squares-four.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/squares-four.imageset/squares-four.svg b/Linphone/Assets.xcassets/squares-four.imageset/squares-four.svg new file mode 100644 index 000000000..85f5689ef --- /dev/null +++ b/Linphone/Assets.xcassets/squares-four.imageset/squares-four.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/waveform.imageset/Contents.json b/Linphone/Assets.xcassets/waveform.imageset/Contents.json new file mode 100644 index 000000000..c9be92b8a --- /dev/null +++ b/Linphone/Assets.xcassets/waveform.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "waveform.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/waveform.imageset/waveform.svg b/Linphone/Assets.xcassets/waveform.imageset/waveform.svg new file mode 100644 index 000000000..ab8d0faff --- /dev/null +++ b/Linphone/Assets.xcassets/waveform.imageset/waveform.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index d26d8e3f5..2652bf8e4 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -218,6 +218,9 @@ }, "Attended transfer" : { + }, + "Audio seulement" : { + }, "Block the address" : { @@ -501,6 +504,9 @@ }, "Missed call" : { + }, + "Mosaïque" : { + }, "New call" : { @@ -555,6 +561,9 @@ }, "Partager le lien" : { + }, + "Participant actif" : { + }, "Participants" : { diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 1574c9dbe..2e52f23f4 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -25,6 +25,7 @@ import UniformTypeIdentifiers // swiftlint:disable type_body_length // swiftlint:disable line_length +// swiftlint:disable file_length struct CallView: View { @ObservedObject private var coreContext = CoreContext.shared @@ -39,7 +40,9 @@ struct CallView: View { let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification) @State var audioRouteSheet: Bool = false - @State var options: Int = 1 + @State var changeLayoutSheet: Bool = false + @State var optionsAudioRoute: Int = 1 + @State var optionsChangeLayout: Int = 2 @State var imageAudioRoute: String = "" @State var angleDegree = 0.0 @State var showingDialer = false @@ -64,7 +67,13 @@ struct CallView: View { .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false }) { - innerBottomSheet() + audioRouteBottomSheet() + .presentationDetents([.fraction(0.3)]) + } + .sheet(isPresented: $changeLayoutSheet, onDismiss: { + changeLayoutSheet = false + }) { + changeLayoutBottomSheet() .presentationDetents([.fraction(0.3)]) } .sheet(isPresented: $showingDialer) { @@ -81,7 +90,13 @@ struct CallView: View { .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false }) { - innerBottomSheet() + audioRouteBottomSheet() + .presentationDetents([.fraction(0.3)]) + } + .sheet(isPresented: $changeLayoutSheet, onDismiss: { + changeLayoutSheet = false + }) { + changeLayoutBottomSheet() .presentationDetents([.fraction(0.3)]) } .sheet(isPresented: $showingDialer) { @@ -95,10 +110,15 @@ struct CallView: View { } else { innerView(geometry: geo) .halfSheet(showSheet: $audioRouteSheet) { - innerBottomSheet() + audioRouteBottomSheet() } onDismiss: { audioRouteSheet = false } + .halfSheet(showSheet: $changeLayoutSheet) { + changeLayoutBottomSheet() + } onDismiss: { + changeLayoutSheet = false + } .halfSheet(showSheet: $showingDialer) { DialerBottomSheet( startCallViewModel: StartCallViewModel(), @@ -149,10 +169,10 @@ struct CallView: View { } @ViewBuilder - func innerBottomSheet() -> some View { + func audioRouteBottomSheet() -> some View { VStack(spacing: 0) { Button(action: { - options = 1 + optionsAudioRoute = 1 do { try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) @@ -166,7 +186,7 @@ struct CallView: View { } }, label: { HStack { - Image(options == 1 ? "radio-button-fill" : "radio-button") + Image(optionsAudioRoute == 1 ? "radio-button-fill" : "radio-button") .renderingMode(.template) .resizable() .foregroundStyle(.white) @@ -189,7 +209,7 @@ struct CallView: View { .frame(maxHeight: .infinity) Button(action: { - options = 2 + optionsAudioRoute = 2 do { try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) @@ -198,7 +218,7 @@ struct CallView: View { } }, label: { HStack { - Image(options == 2 ? "radio-button-fill" : "radio-button") + Image(optionsAudioRoute == 2 ? "radio-button-fill" : "radio-button") .renderingMode(.template) .resizable() .foregroundStyle(.white) @@ -221,7 +241,7 @@ struct CallView: View { .frame(maxHeight: .infinity) Button(action: { - options = 3 + optionsAudioRoute = 3 do { try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) @@ -231,7 +251,7 @@ struct CallView: View { } }, label: { HStack { - Image(options == 3 ? "radio-button-fill" : "radio-button") + Image(optionsAudioRoute == 3 ? "radio-button-fill" : "radio-button") .renderingMode(.template) .resizable() .foregroundStyle(.white) @@ -258,6 +278,97 @@ struct CallView: View { .frame(maxHeight: .infinity) } + @ViewBuilder + func changeLayoutBottomSheet() -> some View { + VStack(spacing: 0) { + Button(action: { + optionsChangeLayout = 1 + changeLayoutSheet = false + }, label: { + HStack { + Image(optionsChangeLayout == 1 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(callViewModel.participantList.count > 5 ? Color.gray500 : .white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + + Text("Mosaïque") + .foregroundStyle(callViewModel.participantList.count > 5 ? Color.gray500 : .white) + .default_text_style_white(styleSize: 15) + + Spacer() + + Image("squares-four") + .renderingMode(.template) + .resizable() + .foregroundStyle(callViewModel.participantList.count > 5 ? Color.gray500 : .white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + }) + .disabled(callViewModel.participantList.count > 5) + .frame(maxHeight: .infinity) + + Button(action: { + optionsChangeLayout = 2 + changeLayoutSheet = false + }, label: { + HStack { + Image(optionsChangeLayout == 2 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + + Text("Participant actif") + .default_text_style_white(styleSize: 15) + + Spacer() + + Image("picture-in-picture") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + }) + .frame(maxHeight: .infinity) + + Button(action: { + optionsChangeLayout = 3 + changeLayoutSheet = false + }, label: { + HStack { + Image(optionsChangeLayout == 3 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + + Text("Audio seulement") + .default_text_style_white(styleSize: 15) + + Spacer() + + Image("waveform") + .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) + } + @ViewBuilder // swiftlint:disable:next cyclomatic_complexity func innerView(geometry: GeometryProxy) -> some View { @@ -563,336 +674,10 @@ struct CallView: View { ) } } else if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil { - if callViewModel.activeSpeakerParticipant!.onPause { - VStack { - VStack { - Spacer() - - Image("pause") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 40, height: 40) - - Text("En pause") - .frame(maxWidth: .infinity, alignment: .center) - .foregroundStyle(Color.white) - .default_text_style_500(styleSize: 14) - .lineLimit(1) - .padding(.horizontal, 10) - - Spacer() - } - .frame( - width: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, - height: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 - 160 + geometry.safeAreaInsets.bottom - ) - - Spacer() - } - .frame( - maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, - maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom - ) + if optionsChangeLayout == 1 && callViewModel.participantList.count <= 5 { + mosaicMode(geometry: geometry, height: (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom)) } else { - VStack { - VStack { - Spacer() - ZStack { - if callViewModel.activeSpeakerParticipant?.address != nil { - let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.activeSpeakerParticipant!.address) - - let contactAvatarModel = addressFriend != nil - ? ContactsManager.shared.avatarListModel.first(where: { - ($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy) - && $0.friend!.name == addressFriend!.name - && $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly() - }) - : ContactAvatarModel(friend: nil, name: "", withPresence: false) - - if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { - if contactAvatarModel != nil { - Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 200, hidePresence: true) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - displayVideo = true - } - } - } - } else { - if callViewModel.activeSpeakerParticipant!.address.displayName != nil { - Image(uiImage: contactsManager.textToImage( - firstName: callViewModel.activeSpeakerParticipant!.address.displayName!, - lastName: callViewModel.activeSpeakerParticipant!.address.displayName!.components(separatedBy: " ").count > 1 - ? callViewModel.activeSpeakerParticipant!.address.displayName!.components(separatedBy: " ")[1] - : "")) - .resizable() - .frame(width: 200, height: 200) - .clipShape(Circle()) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - displayVideo = true - } - } - - } else { - Image(uiImage: contactsManager.textToImage( - firstName: callViewModel.activeSpeakerParticipant!.address.username ?? "Username Error", - lastName: callViewModel.activeSpeakerParticipant!.address.username!.components(separatedBy: " ").count > 1 - ? callViewModel.activeSpeakerParticipant!.address.username!.components(separatedBy: " ")[1] - : "")) - .resizable() - .frame(width: 200, height: 200) - .clipShape(Circle()) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - displayVideo = true - } - } - } - - } - } else { - Image("profil-picture-default") - .resizable() - .frame(width: 200, height: 200) - .clipShape(Circle()) - } - } - - Spacer() - } - .frame( - width: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, - height: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 - 160 + geometry.safeAreaInsets.bottom - ) - - Spacer() - } - .frame( - maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, - maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom - ) - - if telecomManager.remoteConfVideo && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil && displayVideo { - VStack { - VStack { - LinphoneVideoViewHolder { view in - coreContext.doOnCoreQueue { core in - core.nativeVideoWindow = view - } - } - .onTapGesture { - if callViewModel.videoDisplayed { - fullscreenVideo.toggle() - } - } - } - .frame( - width: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, - height: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 - 160 + geometry.safeAreaInsets.bottom - ) - .cornerRadius(20) - - Spacer() - } - .frame( - width: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, - height: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom - ) - } - } - - if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil && callViewModel.activeSpeakerParticipant!.isMuted { - VStack { - HStack { - Spacer() - - HStack(alignment: .center) { - Image("microphone-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c800) - .frame(width: 20, height: 20) - } - .padding(5) - .background(.white) - .cornerRadius(40) - } - Spacer() - } - .frame(maxWidth: .infinity) - .padding(.all, 20) - } - - if callViewModel.isConference { - HStack { - Spacer() - VStack { - Spacer() - - Text(callViewModel.activeSpeakerName) - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundStyle(Color.white) - .default_text_style_500(styleSize: 20) - .lineLimit(1) - .padding(.horizontal, 10) - .padding(.bottom, 6) - - ScrollView(.horizontal) { - HStack { - ZStack { - VStack { - Spacer() - - if callViewModel.myParticipantModel != nil { - Avatar(contactAvatarModel: callViewModel.myParticipantModel!.avatarModel, avatarSize: 50, hidePresence: true) - } - - Spacer() - } - .frame(width: 140, height: 140) - - if callViewModel.videoDisplayed { - LinphoneVideoViewHolder { view in - coreContext.doOnCoreQueue { core in - core.nativePreviewWindow = view - } - } - .frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2) - .scaledToFill() - .clipped() - } - - VStack(alignment: .leading) { - Spacer() - - if callViewModel.myParticipantModel != nil { - Text(callViewModel.myParticipantModel!.name) - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundStyle(Color.white) - .default_text_style_500(styleSize: 14) - .lineLimit(1) - .padding(.horizontal, 10) - .padding(.bottom, 6) - } - } - .frame(width: 140, height: 140) - } - .frame(width: 140, height: 140) - .background(Color.gray600) - .cornerRadius(20) - - ForEach(0.. 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom - ) - .padding(.bottom, 10) - .padding(.leading, -10) + activeSpeakerMode(geometry: geometry) } } else if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.participantList.isEmpty { VStack { @@ -1034,6 +819,832 @@ struct CallView: View { } // swiftlint:enable function_body_length + func activeSpeakerMode(geometry: GeometryProxy) -> some View { + ZStack { + if callViewModel.activeSpeakerParticipant!.onPause { + VStack { + VStack { + Spacer() + + Image("pause") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 40, height: 40) + + Text("En pause") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 14) + .lineLimit(1) + .padding(.horizontal, 10) + + Spacer() + } + .frame( + width: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, + height: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 - 160 + geometry.safeAreaInsets.bottom + ) + + Spacer() + } + .frame( + maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, + maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom + ) + } else { + VStack { + VStack { + Spacer() + ZStack { + if callViewModel.activeSpeakerParticipant?.address != nil { + let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.activeSpeakerParticipant!.address) + + let contactAvatarModel = addressFriend != nil + ? ContactsManager.shared.avatarListModel.first(where: { + ($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy) + && $0.friend!.name == addressFriend!.name + && $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly() + }) + : ContactAvatarModel(friend: nil, name: "", withPresence: false) + + if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { + if contactAvatarModel != nil { + Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 200, hidePresence: true) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + displayVideo = true + } + } + } + } else { + if callViewModel.activeSpeakerParticipant!.address.displayName != nil { + Image(uiImage: contactsManager.textToImage( + firstName: callViewModel.activeSpeakerParticipant!.address.displayName!, + lastName: callViewModel.activeSpeakerParticipant!.address.displayName!.components(separatedBy: " ").count > 1 + ? callViewModel.activeSpeakerParticipant!.address.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 200, height: 200) + .clipShape(Circle()) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + displayVideo = true + } + } + + } else { + Image(uiImage: contactsManager.textToImage( + firstName: callViewModel.activeSpeakerParticipant!.address.username ?? "Username Error", + lastName: callViewModel.activeSpeakerParticipant!.address.username!.components(separatedBy: " ").count > 1 + ? callViewModel.activeSpeakerParticipant!.address.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 200, height: 200) + .clipShape(Circle()) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + displayVideo = true + } + } + } + + } + } else { + Image("profil-picture-default") + .resizable() + .frame(width: 200, height: 200) + .clipShape(Circle()) + } + } + + Spacer() + } + .frame( + width: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, + height: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 - 160 + geometry.safeAreaInsets.bottom + ) + + Spacer() + } + .frame( + maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, + maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom + ) + + if telecomManager.remoteConfVideo && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil && displayVideo { + VStack { + VStack { + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + core.nativeVideoWindow = view + } + } + .onTapGesture { + if callViewModel.videoDisplayed { + fullscreenVideo.toggle() + } + } + } + .frame( + width: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, + height: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 - 160 + geometry.safeAreaInsets.bottom + ) + .cornerRadius(20) + + Spacer() + } + .frame( + width: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, + height: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom + ) + } + } + + if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil && callViewModel.activeSpeakerParticipant!.isMuted { + VStack { + HStack { + Spacer() + + HStack(alignment: .center) { + Image("microphone-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c800) + .frame(width: 20, height: 20) + } + .padding(5) + .background(.white) + .cornerRadius(40) + } + Spacer() + } + .frame(maxWidth: .infinity) + .padding(.all, 20) + } + + if callViewModel.isConference { + HStack { + Spacer() + VStack { + Spacer() + + Text(callViewModel.activeSpeakerName) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 20) + .lineLimit(1) + .padding(.horizontal, 10) + .padding(.bottom, 6) + + ScrollView(.horizontal) { + HStack { + ZStack { + VStack { + Spacer() + + if callViewModel.myParticipantModel != nil { + Avatar(contactAvatarModel: callViewModel.myParticipantModel!.avatarModel, avatarSize: 50, hidePresence: true) + } + + Spacer() + } + .frame(width: 140, height: 140) + + if callViewModel.videoDisplayed { + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + core.nativePreviewWindow = view + } + } + .frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2) + .scaledToFill() + .clipped() + } + + VStack(alignment: .leading) { + Spacer() + + if callViewModel.myParticipantModel != nil { + Text(callViewModel.myParticipantModel!.name) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 14) + .lineLimit(1) + .padding(.horizontal, 10) + .padding(.bottom, 6) + } + } + .frame(width: 140, height: 140) + } + .frame(width: 140, height: 140) + .background(Color.gray600) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(callViewModel.myParticipantModel != nil && callViewModel.myParticipantModel!.isSpeaking ? .white : Color.gray600, lineWidth: 4) + ) + .cornerRadius(20) + + ForEach(0.. 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom + ) + .padding(.bottom, 10) + .padding(.leading, -10) + } + } + .onAppear { + optionsChangeLayout = 2 + } + } + + // swiftlint:disable:next cyclomatic_complexity + func mosaicMode(geometry: GeometryProxy, height: Double) -> some View { + VStack { + if geometry.size.width < geometry.size.height { + let maxValue = max( + ((geometry.size.width/2) - 10.0) * ceil(Double(callViewModel.participantList.count + 1) / 2.0) > height ? ((height / 3) - 10.0) : ((geometry.size.width/2) - 10.0), + ((height / Double(callViewModel.participantList.count + 1)) - 10.0) + ) + + LazyVGrid(columns: [ + GridItem(.adaptive( + minimum: maxValue + )) + ], spacing: 10) { + if callViewModel.myParticipantModel != nil { + ZStack { + if callViewModel.myParticipantModel!.isJoining { + VStack { + Spacer() + + ActivityIndicator(color: .white) + .frame(width: maxValue/4, height: maxValue/4) + .padding(.bottom, 5) + + Text("Joining...") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 14) + .lineLimit(1) + .padding(.horizontal, 10) + + Spacer() + } + } else if callViewModel.myParticipantModel!.onPause { + VStack { + Spacer() + + Image("pause") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: maxValue/4, height: maxValue/4) + + Text("En pause") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 14) + .lineLimit(1) + .padding(.horizontal, 10) + + Spacer() + } + } else { + VStack { + Spacer() + + if callViewModel.myParticipantModel != nil { + Avatar(contactAvatarModel: callViewModel.myParticipantModel!.avatarModel, avatarSize: maxValue/2, hidePresence: true) + } + + Spacer() + } + .frame(width: maxValue, height: maxValue) + + if callViewModel.videoDisplayed { + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + core.nativePreviewWindow = view + } + } + .frame( + width: 120 * ceil(maxValue / 120), + height: 160 * ceil(maxValue / 120) + ) + .scaledToFill() + .clipped() + } + + if callViewModel.myParticipantModel!.isMuted { + VStack { + HStack { + Spacer() + + HStack(alignment: .center) { + Image("microphone-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c800) + .frame(width: 12, height: 12) + } + .padding(2) + .background(.white) + .cornerRadius(40) + } + Spacer() + } + .frame(maxWidth: .infinity) + .padding(.all, 10) + } + } + + VStack(alignment: .leading) { + Spacer() + + if callViewModel.myParticipantModel != nil { + Text(callViewModel.myParticipantModel!.name) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 14) + .lineLimit(1) + .padding(.horizontal, 10) + .padding(.bottom, 6) + } + } + .frame(width: maxValue, height: maxValue) + } + .frame( + width: maxValue, + height: maxValue, + alignment: .center + ) + .background(Color.gray600) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(callViewModel.myParticipantModel!.isSpeaking ? .white : Color.gray600, lineWidth: 4) + ) + .cornerRadius(20) + } + + ForEach(0.. height ? ((height / 2) - 10.0) : ((geometry.size.width/3) - 10.0), + ((geometry.size.width/Double(callViewModel.participantList.count + 1)) - 10.0) > height ? height - 20 : ((geometry.size.width/Double(callViewModel.participantList.count + 1)) - 10.0) + ) + + LazyHGrid(rows: [ + GridItem(.adaptive( + minimum: maxValue + )) + ], spacing: 10) { + if callViewModel.myParticipantModel != nil { + ZStack { + if callViewModel.myParticipantModel!.isJoining { + VStack { + Spacer() + + ActivityIndicator(color: .white) + .frame(width: maxValue/4, height: maxValue/4) + .padding(.bottom, 5) + + Text("Joining...") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 14) + .lineLimit(1) + .padding(.horizontal, 10) + + Spacer() + } + } else if callViewModel.myParticipantModel!.onPause { + VStack { + Spacer() + + Image("pause") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: maxValue/4, height: maxValue/4) + + Text("En pause") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 14) + .lineLimit(1) + .padding(.horizontal, 10) + + Spacer() + } + } else { + VStack { + Spacer() + + if callViewModel.myParticipantModel != nil { + Avatar(contactAvatarModel: callViewModel.myParticipantModel!.avatarModel, avatarSize: maxValue/2, hidePresence: true) + } + + Spacer() + } + .frame(width: maxValue, height: maxValue) + + if callViewModel.videoDisplayed { + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + core.nativePreviewWindow = view + } + } + .frame( + width: 160 * ceil(maxValue / 120), + height: 120 * ceil(maxValue / 120) + ) + .scaledToFill() + .clipped() + } + + if callViewModel.myParticipantModel!.isMuted { + VStack { + HStack { + Spacer() + + HStack(alignment: .center) { + Image("microphone-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c800) + .frame(width: 12, height: 12) + } + .padding(2) + .background(.white) + .cornerRadius(40) + } + Spacer() + } + .frame(maxWidth: .infinity) + .padding(.all, 10) + } + } + + VStack(alignment: .leading) { + Spacer() + + if callViewModel.myParticipantModel != nil { + Text(callViewModel.myParticipantModel!.name) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(Color.white) + .default_text_style_500(styleSize: 14) + .lineLimit(1) + .padding(.horizontal, 10) + .padding(.bottom, 6) + } + } + .frame(width: maxValue, height: maxValue) + } + .frame( + width: maxValue, + height: maxValue, + alignment: .center + ) + .background(Color.gray600) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(callViewModel.myParticipantModel!.isSpeaking ? .white : Color.gray600, lineWidth: 4) + ) + .cornerRadius(20) + } + + ForEach(0.. some View { GeometryReader { _ in @@ -1326,6 +1937,7 @@ struct CallView: View { } else { VStack { Button { + changeLayoutSheet = true } label: { HStack { Image("notebook") @@ -1651,6 +2263,7 @@ struct CallView: View { } else { VStack { Button { + changeLayoutSheet = true } label: { HStack { Image("notebook") @@ -1894,3 +2507,4 @@ struct PressedButtonStyle: ButtonStyle { } // swiftlint:enable type_body_length // swiftlint:enable line_length +// swiftlint:enable file_length diff --git a/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift b/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift index 301d1de99..31152edb7 100644 --- a/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift +++ b/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift @@ -74,6 +74,27 @@ struct ParticipantsListFragment: View { .background(.white) participantsList + + HStack { + Spacer() + + NavigationLink(destination: { + //AddParticipantsFragment() + }, label: { + Image("plus") + .resizable() + .renderingMode(.template) + .frame(width: 25, height: 25) + .foregroundStyle(.white) + .padding() + .background(Color.orangeMain500) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + + }) + .padding() + } + .padding(.trailing, 10) } .background(.white) diff --git a/Linphone/UI/Call/Model/ParticipantModel.swift b/Linphone/UI/Call/Model/ParticipantModel.swift index e68da99cf..0a1ec61d4 100644 --- a/Linphone/UI/Call/Model/ParticipantModel.swift +++ b/Linphone/UI/Call/Model/ParticipantModel.swift @@ -32,8 +32,9 @@ class ParticipantModel: ObservableObject { @Published var onPause: Bool @Published var isMuted: Bool @Published var isAdmin: Bool + @Published var isSpeaking: Bool - init(address: Address, isJoining: Bool = false, onPause: Bool = false, isMuted: Bool = false, isAdmin: Bool = false) { + init(address: Address, isJoining: Bool = false, onPause: Bool = false, isMuted: Bool = false, isAdmin: Bool = false, isSpeaking: Bool = false) { self.address = address self.sipUri = address.asStringUriOnly() @@ -50,5 +51,6 @@ class ParticipantModel: ObservableObject { self.onPause = onPause self.isMuted = isMuted self.isAdmin = isAdmin + self.isSpeaking = isSpeaking } } diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index 24299823f..72a08548a 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -305,6 +305,7 @@ class CallViewModel: ObservableObject { ) } + // swiftlint:disable:next cyclomatic_complexity func addConferenceCallBacks() { coreContext.doOnCoreQueue { core in self.mConferenceSuscriptions.insert( @@ -386,7 +387,52 @@ class CallViewModel: ObservableObject { } }) + var activeSpeakerParticipantTmp: ParticipantModel? = nil + var activeSpeakerNameTmp = "" + + if self.activeSpeakerParticipant == nil { + if cbValue.conference.activeSpeakerParticipantDevice?.address != nil { + activeSpeakerParticipantTmp = ParticipantModel( + address: cbValue.conference.activeSpeakerParticipantDevice!.address!, + isJoining: false, + onPause: cbValue.conference.activeSpeakerParticipantDevice!.state == .OnHold, + isMuted: cbValue.conference.activeSpeakerParticipantDevice!.isMuted + ) + } else if cbValue.conference.participantList.first?.address != nil && cbValue.conference.participantList.first!.address!.clone()!.equal(address2: (cbValue.conference.me?.address)!) { + activeSpeakerParticipantTmp = ParticipantModel( + address: cbValue.conference.participantDeviceList.first!.address!, + isJoining: false, + onPause: cbValue.conference.participantDeviceList.first!.state == .OnHold, + isMuted: cbValue.conference.participantDeviceList.first!.isMuted + ) + } else if cbValue.conference.participantList.last?.address != nil { + activeSpeakerParticipantTmp = ParticipantModel( + address: cbValue.conference.participantDeviceList.last!.address!, + isJoining: false, + onPause: cbValue.conference.participantDeviceList.last!.state == .OnHold, + isMuted: cbValue.conference.participantDeviceList.last!.isMuted + ) + } + + if activeSpeakerParticipantTmp != nil { + let friend = ContactsManager.shared.getFriendWithAddress(address: activeSpeakerParticipantTmp!.address) + if friend != nil && friend!.address != nil && friend!.address!.displayName != nil { + activeSpeakerNameTmp = friend!.address!.displayName! + } else { + if activeSpeakerParticipantTmp!.address.displayName != nil { + activeSpeakerNameTmp = activeSpeakerParticipantTmp!.address.displayName! + } else if activeSpeakerParticipantTmp!.address.username != nil { + activeSpeakerNameTmp = activeSpeakerParticipantTmp!.address.username! + } + } + } + } + DispatchQueue.main.async { + if self.activeSpeakerParticipant == nil { + self.activeSpeakerParticipant = activeSpeakerParticipantTmp + self.activeSpeakerName = activeSpeakerNameTmp + } self.participantList = participantListTmp } } @@ -435,17 +481,16 @@ class CallViewModel: ObservableObject { DispatchQueue.main.async { self.activeSpeakerParticipant!.isMuted = isMutedTmp } - } else { - self.participantList.forEach({ participantDevice in - if participantDevice.address.equal(address2: cbValue.participantDevice.address!) { - let isMutedTmp = cbValue.isMuted - - DispatchQueue.main.async { - participantDevice.isMuted = isMutedTmp - } - } - }) } + self.participantList.forEach({ participantDevice in + if participantDevice.address.equal(address2: cbValue.participantDevice.address!) { + let isMutedTmp = cbValue.isMuted + + DispatchQueue.main.async { + participantDevice.isMuted = isMutedTmp + } + } + }) } ) @@ -461,18 +506,17 @@ class CallViewModel: ObservableObject { self.activeSpeakerParticipant!.onPause = activeSpeakerParticipantOnPauseTmp self.activeSpeakerParticipant!.isJoining = activeSpeakerParticipantIsJoiningTmp } - } else { - self.participantList.forEach({ participantDevice in - if participantDevice.address.equal(address2: cbValue.device.address!) { - let participantDeviceOnPauseTmp = cbValue.state == .OnHold - let participantDeviceIsJoiningTmp = cbValue.state == .Joining || cbValue.state == .Alerting - DispatchQueue.main.async { - participantDevice.onPause = participantDeviceOnPauseTmp - participantDevice.isJoining = participantDeviceIsJoiningTmp - } - } - }) } + self.participantList.forEach({ participantDevice in + if participantDevice.address.equal(address2: cbValue.device.address!) { + let participantDeviceOnPauseTmp = cbValue.state == .OnHold + let participantDeviceIsJoiningTmp = cbValue.state == .Joining || cbValue.state == .Alerting + DispatchQueue.main.async { + participantDevice.onPause = participantDeviceOnPauseTmp + participantDevice.isJoining = participantDeviceIsJoiningTmp + } + } + }) } ) @@ -483,15 +527,32 @@ class CallViewModel: ObservableObject { DispatchQueue.main.async { self.myParticipantModel!.isAdmin = isAdmin } - } else { - self.participantList.forEach({ participantDevice in - if participantDevice.address.clone()!.equal(address2: cbValue.participant.address!) { - DispatchQueue.main.async { - participantDevice.isAdmin = isAdmin - } - } - }) } + self.participantList.forEach({ participantDevice in + if participantDevice.address.clone()!.equal(address2: cbValue.participant.address!) { + DispatchQueue.main.async { + participantDevice.isAdmin = isAdmin + } + } + }) + } + ) + + self.mConferenceSuscriptions.insert( + self.currentCall?.conference?.publisher?.onParticipantDeviceIsSpeakingChanged?.postOnMainQueue {(cbValue: (conference: Conference, participantDevice: ParticipantDevice, isSpeaking: Bool)) in + let isSpeaking = cbValue.participantDevice.isSpeaking + if self.myParticipantModel != nil && self.myParticipantModel!.address.clone()!.equal(address2: cbValue.participantDevice.address!) { + DispatchQueue.main.async { + self.myParticipantModel!.isSpeaking = isSpeaking + } + } + self.participantList.forEach({ participantDeviceList in + if participantDeviceList.address.clone()!.equal(address2: cbValue.participantDevice.address!) { + DispatchQueue.main.async { + participantDeviceList.isSpeaking = isSpeaking + } + } + }) } ) }