From ab3b88344222a9803af61575eefeb0d44055ffb9 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 18 Mar 2024 17:12:46 +0100 Subject: [PATCH] Init conference call view --- Linphone.xcodeproj/project.pbxproj | 12 + Linphone/Core/CoreContext.swift | 13 +- Linphone/Localizable.xcstrings | 14 +- Linphone/TelecomManager/TelecomManager.swift | 15 +- Linphone/UI/Call/CallView.swift | 829 +++++++++++------- Linphone/UI/Call/Model/ParticipantModel.swift | 58 ++ .../UI/Call/ViewModel/CallViewModel.swift | 73 ++ .../ConversationsListViewModel.swift | 27 - Linphone/Utils/Avatar.swift | 2 +- 9 files changed, 713 insertions(+), 330 deletions(-) create mode 100644 Linphone/UI/Call/Model/ParticipantModel.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 7a2f7f197..a2b10c1f1 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */; }; D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */; }; D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */; }; + D720E6AD2BAD822000DDFD87 /* ParticipantModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */; }; D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72250622ADE9615008FB426 /* HistoryViewModel.swift */; }; D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72250682ADFBF2D008FB426 /* SideMenu.swift */; }; D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723432F2ACEFEF8009AA24E /* QrCodeScannerFragment.swift */; }; @@ -180,6 +181,7 @@ D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListViewModel.swift; sourceTree = ""; }; D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListFragment.swift; sourceTree = ""; }; D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactFragment.swift; sourceTree = ""; }; + D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantModel.swift; sourceTree = ""; }; D72250622ADE9615008FB426 /* HistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewModel.swift; sourceTree = ""; }; D72250682ADFBF2D008FB426 /* SideMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenu.swift; sourceTree = ""; }; D723432F2ACEFEF8009AA24E /* QrCodeScannerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCodeScannerFragment.swift; sourceTree = ""; }; @@ -444,6 +446,14 @@ path = Viewmodel; sourceTree = ""; }; + D720E6AB2BAD81C800DDFD87 /* Model */ = { + isa = PBXGroup; + children = ( + D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */, + ); + path = Model; + sourceTree = ""; + }; D72250612ADE95E4008FB426 /* ViewModel */ = { isa = PBXGroup; children = ( @@ -597,6 +607,7 @@ isa = PBXGroup; children = ( D75759302B56D3CE00E7AC10 /* Fragments */, + D720E6AB2BAD81C800DDFD87 /* Model */, D7B99E972B29B37F00BE7BF2 /* ViewModel */, D7B5678D2B28888F00DE63EB /* CallView.swift */, ); @@ -866,6 +877,7 @@ D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */, D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */, D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */, + D720E6AD2BAD822000DDFD87 /* ParticipantModel.swift in Sources */, D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */, D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */, D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */, diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index bde91d495..98cb3105e 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -270,9 +270,11 @@ final class CoreContext: ObservableObject { } } - func updatePresence(core : Core, presence : ConsolidatedPresence) { + func updatePresence(core: Core, presence: ConsolidatedPresence) { if core.config!.getBool(section: "app", key: "publish_presence", defaultValue: true) { - core.consolidatedPresence = presence + DispatchQueue.main.async { + core.consolidatedPresence = presence + } } } @@ -283,7 +285,7 @@ final class CoreContext: ObservableObject { Log.info("App is in foreground, PUBLISHING presence as Online") self.updatePresence(core: self.mCore, presence: ConsolidatedPresence.Online) - try? self.mCore.start() + //try? self.mCore.start() } } @@ -297,7 +299,10 @@ final class CoreContext: ObservableObject { // Flexisip will handle the Busy status depending on other devices self.updatePresence(core: self.mCore, presence: ConsolidatedPresence.Offline) // self.mCore.iterate() - self.mCore.stop() + + if self.mCore.currentCall == nil { + //self.mCore.stop() + } } } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index b251f51a8..526a5e932 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -244,6 +244,9 @@ }, "Contacts" : { + }, + "Content" : { + }, "Continue" : { @@ -485,6 +488,12 @@ }, "Other actions" : { + }, + "Partage d'écran" : { + + }, + "Participants" : { + }, "password" : { "extractionState" : "manual", @@ -625,6 +634,9 @@ }, "This contact will be deleted definitively." : { + }, + "Title" : { + }, "TLS" : { @@ -693,4 +705,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index 0bf7b0cbc..3cfd68bc0 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -45,6 +45,7 @@ class TelecomManager: ObservableObject { @Published var callStarted: Bool = false @Published var outgoingCallStarted: Bool = false @Published var remoteVideo: Bool = false + @Published var remoteConfVideo: Bool = false @Published var isRecordingByRemote: Bool = false @Published var isPausedByRemote: Bool = false @Published var refreshCallViewModel: Bool = false @@ -374,7 +375,19 @@ class TelecomManager: ObservableObject { DispatchQueue.main.async { let oldRemoteVideo = self.remoteVideo - self.remoteVideo = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false) + //self.remoteVideo = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false) + + if call.conference != nil { + if call.conference!.activeSpeakerParticipantDevice != nil { + let direction = call.conference?.activeSpeakerParticipantDevice!.getStreamCapability(streamType: StreamType.Video) + self.remoteConfVideo = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly + } else { + self.remoteConfVideo = true + } + } else { + self.remoteVideo = call.currentParams!.videoEnabled && call.currentParams!.videoDirection != MediaDirection.Inactive + self.remoteConfVideo = false + } if self.remoteVideo && self.remoteVideo != oldRemoteVideo { do { diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index d198a1a7e..f5e01f466 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -17,13 +17,13 @@ * along with this program. If not, see . */ -// swiftlint:disable type_body_length -// swiftlint:disable line_length import SwiftUI import CallKit import AVFAudio import linphonesw +// swiftlint:disable type_body_length +// swiftlint:disable line_length struct CallView: View { @ObservedObject private var coreContext = CoreContext.shared @@ -38,7 +38,6 @@ struct CallView: View { let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification) @State var audioRouteSheet: Bool = false - @State var hideButtonsSheet: Bool = false @State var options: Int = 1 @State var imageAudioRoute: String = "" @State var angleDegree = 0.0 @@ -47,6 +46,7 @@ struct CallView: View { @State var maxBottomSheetHeight: CGFloat = 0.5 @State private var pointingUp: CGFloat = 0.0 @State private var currentOffset: CGFloat = 0.0 + @State private var viewIsDisplayed = false @Binding var fullscreenVideo: Bool @Binding var isShowCallsListFragment: Bool @@ -59,7 +59,6 @@ struct CallView: View { innerView(geometry: geo) .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false - hideButtonsSheet = false }) { innerBottomSheet() .presentationDetents([.fraction(0.3)]) @@ -77,7 +76,6 @@ struct CallView: View { innerView(geometry: geo) .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false - hideButtonsSheet = false }) { innerBottomSheet() .presentationDetents([.fraction(0.3)]) @@ -96,7 +94,6 @@ struct CallView: View { innerBottomSheet() } onDismiss: { audioRouteSheet = false - hideButtonsSheet = false } .halfSheet(showSheet: $showingDialer) { DialerBottomSheet( @@ -320,7 +317,18 @@ struct CallView: View { .padding(.all, 10) } - if telecomManager.remoteVideo { + if !callViewModel.isConference && telecomManager.remoteVideo { + Button { + callViewModel.switchCamera() + } label: { + Image("camera-rotate") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 30, height: 30) + .padding(.horizontal) + } + } else if callViewModel.isConference && callViewModel.videoDisplayed { Button { callViewModel.switchCamera() } label: { @@ -360,232 +368,7 @@ struct CallView: View { } } - ZStack { - VStack { - Spacer() - ZStack { - - if callViewModel.isRemoteDeviceTrusted { - Circle() - .fill(Color.blueInfo500) - .frame(width: 206, height: 206) - } - - if callViewModel.remoteAddress != nil { - let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!) - - 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) - } - } else { - if callViewModel.remoteAddress!.displayName != nil { - Image(uiImage: contactsManager.textToImage( - firstName: callViewModel.remoteAddress!.displayName!, - lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1 - ? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1] - : "")) - .resizable() - .frame(width: 200, height: 200) - .clipShape(Circle()) - - } else { - Image(uiImage: contactsManager.textToImage( - firstName: callViewModel.remoteAddress!.username ?? "Username Error", - lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1 - ? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1] - : "")) - .resizable() - .frame(width: 200, height: 200) - .clipShape(Circle()) - } - - } - } else { - Image("profil-picture-default") - .resizable() - .frame(width: 200, height: 200) - .clipShape(Circle()) - } - - if callViewModel.isRemoteDeviceTrusted { - VStack { - Spacer() - HStack { - Image("trusted") - .resizable() - .frame(width: 25, height: 25) - .padding(.all, 15) - Spacer() - } - } - .frame(width: 200, height: 200) - } - } - - Text(callViewModel.displayName) - .padding(.top) - .default_text_style_white(styleSize: 22) - - Text(callViewModel.remoteAddressString) - .default_text_style_white_300(styleSize: 16) - - Spacer() - } - - LinphoneVideoViewHolder { view in - coreContext.doOnCoreQueue { core in - core.nativeVideoWindow = view - } - } - .frame( - width: - angleDegree == 0 - ? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8) - : (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), - height: - angleDegree == 0 - ? (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) - : (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8) - ) - .scaledToFill() - .clipped() - .onTapGesture { - if telecomManager.remoteVideo { - fullscreenVideo.toggle() - } - } - - if telecomManager.remoteVideo { - HStack { - Spacer() - VStack { - Spacer() - 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) - .cornerRadius(20) - .padding(10) - .padding(.trailing, abs(angleDegree/2)) - } - } - .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 callViewModel.isRecording { - HStack { - VStack { - Image("record-fill") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.redDanger500) - .frame(width: 32, height: 32) - .padding(10) - .if(fullscreenVideo && !telecomManager.isPausedByRemote) { view in - view.padding(.top, 30) - } - Spacer() - } - 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.outgoingCallStarted { - VStack { - ActivityIndicator() - .frame(width: 20, height: 20) - .padding(.top, 60) - - Text(callViewModel.counterToMinutes()) - .onAppear { - callViewModel.timeElapsed = 0 - } - .onReceive(callViewModel.timer) { _ in - callViewModel.timeElapsed = callViewModel.currentCall?.duration ?? 0 - - } - .onDisappear { - callViewModel.timeElapsed = 0 - } - .padding(.top) - .foregroundStyle(.white) - - Spacer() - } - .background(.clear) - .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 - ) - } - } - .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 - ) - .background(Color.gray900) - .cornerRadius(20) - .padding(.horizontal, fullscreenVideo && !telecomManager.isPausedByRemote ? 0 : 4) - .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 - } - } - - if (oldOrientation != orientation && oldOrientation != .faceUp) || (oldOrientation == .faceUp && (orientation == .landscapeLeft || orientation == .landscapeRight)) { - telecomManager.callStarted = false - - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - telecomManager.callStarted = true - } - } - - callViewModel.orientationUpdate(orientation: orientation) - } - .onAppear { - if orientation == .portrait && orientation == .portraitUpsideDown { - angleDegree = 0 - } else { - if orientation == .landscapeLeft { - angleDegree = -90 - } else if orientation == .landscapeRight { - angleDegree = 90 - } - } - - telecomManager.callStarted = false - - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - telecomManager.callStarted = true - } - - callViewModel.orientationUpdate(orientation: orientation) - } + simpleCallView(geometry: geometry) Spacer() } @@ -619,6 +402,396 @@ struct CallView: View { } } + func simpleCallView(geometry: GeometryProxy) -> some View { + ZStack { + if !callViewModel.isConference { + VStack { + Spacer() + ZStack { + + if callViewModel.isRemoteDeviceTrusted { + Circle() + .fill(Color.blueInfo500) + .frame(width: 206, height: 206) + } + + if callViewModel.remoteAddress != nil { + let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!) + + 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) + } + } else { + if callViewModel.remoteAddress!.displayName != nil { + Image(uiImage: contactsManager.textToImage( + firstName: callViewModel.remoteAddress!.displayName!, + lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1 + ? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 200, height: 200) + .clipShape(Circle()) + + } else { + Image(uiImage: contactsManager.textToImage( + firstName: callViewModel.remoteAddress!.username ?? "Username Error", + lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1 + ? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 200, height: 200) + .clipShape(Circle()) + } + + } + } else { + Image("profil-picture-default") + .resizable() + .frame(width: 200, height: 200) + .clipShape(Circle()) + } + + if callViewModel.isRemoteDeviceTrusted { + VStack { + Spacer() + HStack { + Image("trusted") + .resizable() + .frame(width: 25, height: 25) + .padding(.all, 15) + Spacer() + } + } + .frame(width: 200, height: 200) + } + } + + Text(callViewModel.displayName) + .padding(.top) + .default_text_style_white(styleSize: 22) + + Text(callViewModel.remoteAddressString) + .default_text_style_white_300(styleSize: 16) + + Spacer() + } + } else { + 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) + } + } 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()) + + } 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()) + } + + } + } else { + Image("profil-picture-default") + .resizable() + .frame(width: 200, height: 200) + .clipShape(Circle()) + } + } + + Spacer() + } + } + + if !callViewModel.isConference { + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + core.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque()) + } + } + .frame( + width: + angleDegree == 0 + ? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8) + : (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), + height: + angleDegree == 0 + ? (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) + : (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8) + ) + .scaledToFill() + .clipped() + .onTapGesture { + if telecomManager.remoteVideo { + fullscreenVideo.toggle() + } + } + + if telecomManager.remoteVideo { + HStack { + Spacer() + VStack { + Spacer() + 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) + .cornerRadius(20) + .padding(10) + .padding(.trailing, abs(angleDegree/2)) + } + } + .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 { + /* + if !viewIsDisplayed { + VStack { + + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + viewIsDisplayed = true + } + } + } + + if viewIsDisplayed && (callViewModel.receiveVideo || telecomManager.remoteConfVideo) { + */ + if (callViewModel.receiveVideo || telecomManager.remoteConfVideo) { + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + //core.nativeVideoWindow = view + core.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque()) + } + } + .frame( + width: + angleDegree == 0 + ? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8) + : (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), + height: + angleDegree == 0 + ? (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) + : (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8) + ) + .scaledToFill() + .clipped() + .onTapGesture { + if telecomManager.remoteVideo { + fullscreenVideo.toggle() + } + } + + HStack { + Spacer() + VStack { + Spacer() + ScrollView(.horizontal) { + HStack { + ForEach(0.. 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom + ) + .padding(.bottom, 10) + /* + HStack { + Spacer() + VStack { + Spacer() + 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) + .cornerRadius(20) + .padding(10) + .padding(.trailing, abs(angleDegree/2)) + } + } + .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 callViewModel.isRecording { + HStack { + VStack { + Image("record-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.redDanger500) + .frame(width: 32, height: 32) + .padding(10) + .if(fullscreenVideo && !telecomManager.isPausedByRemote) { view in + view.padding(.top, 30) + } + Spacer() + } + 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.outgoingCallStarted { + VStack { + ActivityIndicator() + .frame(width: 20, height: 20) + .padding(.top, 60) + + Text(callViewModel.counterToMinutes()) + .onAppear { + callViewModel.timeElapsed = 0 + } + .onReceive(callViewModel.timer) { _ in + callViewModel.timeElapsed = callViewModel.currentCall?.duration ?? 0 + + } + .onDisappear { + callViewModel.timeElapsed = 0 + } + .padding(.top) + .foregroundStyle(.white) + + Spacer() + } + .background(.clear) + .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 + ) + } + } + .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 + ) + .background(Color.gray900) + .cornerRadius(20) + .padding(.horizontal, fullscreenVideo && !telecomManager.isPausedByRemote ? 0 : 4) + .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 + } + } + + if (oldOrientation != orientation && oldOrientation != .faceUp) || (oldOrientation == .faceUp && (orientation == .landscapeLeft || orientation == .landscapeRight)) { + telecomManager.callStarted = false + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + telecomManager.callStarted = true + } + } + + callViewModel.orientationUpdate(orientation: orientation) + } + .onAppear { + if orientation == .portrait && orientation == .portraitUpsideDown { + angleDegree = 0 + } else { + if orientation == .landscapeLeft { + angleDegree = -90 + } else if orientation == .landscapeRight { + angleDegree = 90 + } + } + + telecomManager.callStarted = false + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + telecomManager.callStarted = true + } + + callViewModel.orientationUpdate(orientation: orientation) + } + } + + // swiftlint:disable function_body_length func bottomSheetContent(geo: GeometryProxy) -> some View { GeometryReader { _ in VStack(spacing: 0) { @@ -658,22 +831,41 @@ struct CallView: View { Spacer() - Button { - callViewModel.toggleVideo() - } label: { - HStack { - Image(telecomManager.remoteVideo ? "video-camera" : "video-camera-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) - .frame(width: 32, height: 32) + if !callViewModel.isConference { + Button { + callViewModel.toggleVideo() + } label: { + HStack { + Image(telecomManager.remoteVideo ? "video-camera" : "video-camera-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .frame(width: 32, height: 32) + } } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? .white : Color.gray500) + .cornerRadius(40) + .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + } else { + Button { + callViewModel.displayMyVideo() + } label: { + HStack { + Image(callViewModel.videoDisplayed ? "video-camera" : "video-camera-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .frame(width: 32, height: 32) + } + } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? .white : Color.gray500) + .cornerRadius(40) + .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) } - .buttonStyle(PressedButtonStyle()) - .frame(width: 60, height: 60) - .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? .white : Color.gray500) - .cornerRadius(40) - .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) Button { callViewModel.toggleMuteMicrophone() @@ -695,8 +887,6 @@ struct CallView: View { if AVAudioSession.sharedInstance().availableInputs != nil && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { - hideButtonsSheet = true - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { audioRouteSheet = true } @@ -732,63 +922,109 @@ struct CallView: View { if orientation != .landscapeLeft && orientation != .landscapeRight { HStack(spacing: 0) { - VStack { - Button { - if callViewModel.calls.count < 2 { + if !callViewModel.isConference { + VStack { + Button { + if callViewModel.calls.count < 2 { + withAnimation { + callViewModel.isTransferInsteadCall = true + MagicSearchSingleton.shared.searchForSuggestions() + isShowStartCallFragment.toggle() + } + } else { + callViewModel.transferClicked() + } + } label: { + HStack { + Image("phone-transfer") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text(callViewModel.calls.count < 2 ? "Transfer" : "Attended transfer") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { withAnimation { - callViewModel.isTransferInsteadCall = true MagicSearchSingleton.shared.searchForSuggestions() isShowStartCallFragment.toggle() } - } else { - callViewModel.transferClicked() - } - } label: { - HStack { - Image("phone-transfer") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) + } label: { + HStack { + Image("phone-plus") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("New call") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) } - .buttonStyle(PressedButtonStyle()) - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text(callViewModel.calls.count < 2 ? "Transfer" : "Attended transfer") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - withAnimation { - MagicSearchSingleton.shared.searchForSuggestions() - isShowStartCallFragment.toggle() - } - } label: { - HStack { - Image("phone-plus") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + } else { + VStack { + Button { + } label: { + HStack { + Image("screencast") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.gray500) + .frame(width: 32, height: 32) + } } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(.white) + .cornerRadius(40) + .disabled(true) + + Text("Partage d'écran") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) } - .buttonStyle(PressedButtonStyle()) - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - Text("New call") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) + VStack { + Button { + } label: { + HStack { + Image("users") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + } + .buttonStyle(PressedButtonStyle()) + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Participants") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - VStack { ZStack { Button { @@ -1162,6 +1398,7 @@ struct CallView: View { .frame(maxHeight: .infinity, alignment: .top) } } + // swiftlint:enable function_body_length func getAudioRouteImage() { imageAudioRoute = AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty diff --git a/Linphone/UI/Call/Model/ParticipantModel.swift b/Linphone/UI/Call/Model/ParticipantModel.swift new file mode 100644 index 000000000..9f23870fb --- /dev/null +++ b/Linphone/UI/Call/Model/ParticipantModel.swift @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of Linphone + * + * 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 + +class ParticipantModel: ObservableObject { + + static let TAG = "[Participant Model]" + + let address: Address + @Published var sipUri: String + @Published var name: String + @Published var avatarModel: ContactAvatarModel + + init(address: Address) { + self.address = address + + self.sipUri = address.asStringUriOnly() + + let addressFriend = ContactsManager.shared.getFriendWithAddress(address: self.address) + + var nameTmp = "" + + if addressFriend != nil { + nameTmp = addressFriend!.name! + } else { + nameTmp = address.displayName != nil + ? address.displayName! + : address.username! + } + + self.name = nameTmp + + self.avatarModel = addressFriend != nil + ? ContactsManager.shared.avatarListModel.first(where: { + $0.friend!.name == addressFriend!.name + && $0.friend!.address!.asStringUriOnly() == address.asStringUriOnly() + }) ?? ContactAvatarModel(friend: nil, name: nameTmp, withPresence: false) + : ContactAvatarModel(friend: nil, name: nameTmp, withPresence: false) + } +} diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index 95f17f788..8f832665e 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -45,6 +45,14 @@ class CallViewModel: ObservableObject { @Published var isRemoteDeviceTrusted: Bool = false @Published var selectedCall: Call? @Published var isTransferInsteadCall: Bool = false + @Published var isConference: Bool = false + @Published var videoDisplayed: Bool = false + @Published var receiveVideo: Bool = false + @Published var participantList: [ParticipantModel] = [] + @Published var activeSpeakerParticipant: ParticipantModel? = nil + + + private var mConferenceSuscriptions = Set() var calls: [Call] = [] @@ -110,6 +118,7 @@ class CallViewModel: ObservableObject { self.isRemoteDeviceTrusted = self.telecomManager.callInProgress ? isDeviceTrusted : false self.getCallsList() + self.getConference() } self.callSuscriptions.insert(self.currentCall!.publisher?.onEncryptionChanged?.postOnMainQueue {(cbVal: (call: Call, on: Bool, authenticationToken: String?)) in @@ -127,6 +136,43 @@ class CallViewModel: ObservableObject { } } + func getConference() { + coreContext.doOnCoreQueue { core in + //conf = self.currentCall?.conference != nil ? self.currentCall!.conference! : core.findConferenceInformationFromUri(uri: (self.currentCall?.remoteContactAddress)!) + if self.currentCall?.remoteContactAddress != nil { + let conf = core.findConferenceInformationFromUri(uri: (self.currentCall?.remoteContactAddress)!) + DispatchQueue.main.async { + self.isConference = conf != nil + if self.isConference { + self.displayName = conf?.subject ?? "" + self.participantList = [] + conf?.participantInfos.forEach({ participantInfo in + if participantInfo.address != nil { + self.participantList.append(ParticipantModel(address: participantInfo.address!)) + } + }) + self.addConferenceCallBacks() + } + } + } + } + } + + func addConferenceCallBacks() { + coreContext.doOnCoreQueue { core in + self.mConferenceSuscriptions.insert( + self.currentCall?.conference?.publisher?.onActiveSpeakerParticipantDevice?.postOnMainQueue {(cbValue: (conference: Conference, participantDevice: ParticipantDevice)) in + let direction = cbValue.participantDevice.getStreamCapability(streamType: StreamType.Video) + + self.receiveVideo = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly + + if cbValue.participantDevice.address != nil { + self.activeSpeakerParticipant = ParticipantModel(address: cbValue.participantDevice.address!) + } + }) + } + } + func terminateCall() { coreContext.doOnCoreQueue { core in if self.currentCall != nil { @@ -186,6 +232,33 @@ class CallViewModel: ObservableObject { } } + func displayMyVideo() { + coreContext.doOnCoreQueue { core in + if self.currentCall != nil { + do { + let params = try core.createCallParams(call: self.currentCall) + + if params.videoEnabled { + if params.videoDirection == MediaDirection.SendRecv { + params.videoDirection = MediaDirection.RecvOnly + } else if params.videoDirection == MediaDirection.RecvOnly { + params.videoDirection = MediaDirection.SendRecv + } + } + + try self.currentCall!.update(params: params) + + let video = params.videoDirection == MediaDirection.SendRecv + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.videoDisplayed = video + } + } catch { + + } + } + } + } + func switchCamera() { coreContext.doOnCoreQueue { core in let currentDevice = core.videoDevice diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift index 6da0a683a..3f7cab4ad 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -46,7 +46,6 @@ class ConversationsListViewModel: ObservableObject { var conversationsListTmp: [ConversationModel] = [] chatRooms.forEach { chatRoom in - //let disabledBecauseNotSecured = (account?.isInSecureMode() == true && !chatRoom.hasCapability) ? Capabilities.Encrypted.toInt() : 0 if chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue) { } @@ -54,32 +53,6 @@ class ConversationsListViewModel: ObservableObject { let model = ConversationModel(chatRoom: chatRoom) conversationsListTmp.append(model) } - /* - else { - val participants = chatRoom.participants - val found = participants.find { - // Search in address but also in contact name if exists - val model = - coreContext.contactsManager.getContactAvatarModelForAddress(it.address) - model.contactName?.contains( - filter, - ignoreCase = true - ) == true || it.address.asStringUriOnly().contains( - filter, - ignoreCase = true - ) - } - if ( - found != null || - chatRoom.peerAddress.asStringUriOnly().contains(filter, ignoreCase = true) || - chatRoom.subject.orEmpty().contains(filter, ignoreCase = true) - ) { - val model = ConversationModel(chatRoom, disabledBecauseNotSecured) - list.add(model) - count += 1 - } - } - */ } if !self.conversationsList.isEmpty { diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift index aca33765f..768122219 100644 --- a/Linphone/Utils/Avatar.swift +++ b/Linphone/Utils/Avatar.swift @@ -80,7 +80,7 @@ struct Avatar: View { ? contactAvatarModel.name.components(separatedBy: " ")[1] : "")) .resizable() - .frame(width: 50, height: 50) + .frame(width: avatarSize, height: avatarSize) .clipShape(Circle()) } else { Image("profil-picture-default")