From 5adf3d8a3fb486945ef5e180e158977c098f0f6d Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 23 Mar 2022 10:25:23 +0100 Subject: [PATCH] - Waiting room preferences - Asymetric video - Refactorisation - Conference layout picker in waiting room --- Classes/Swift/CallManager.swift | 2 +- .../Conference/{data => Data}/Duration.swift | 0 .../ScheduledConferenceData.swift | 0 .../{data => Data}/TimeZoneData.swift | 0 .../ConferenceSchedulingViewModel.swift | 0 .../ConferenceWaitingRoomViewModel.swift | 75 +++++++++++++++ .../ScheduledConferencesViewModel.swift | 0 .../ConferenceHistoryDetailsView.swift | 0 .../ConferenceSchedulingSummaryView.swift | 0 .../ConferenceSchedulingView.swift | 0 .../ConferenceWaitingRoomFragment.swift | 66 +++++++++++-- .../{views => Views}/ICSBubbleView.swift | 0 .../ScheduledConferencesCell.swift | 0 .../ScheduledConferencesView.swift | 0 .../ConferenceWaitingRoomViewModel.swift | 40 -------- .../Extensions/IOS/UIVIewExtensions.swift | 7 ++ .../LinphoneCore/ConferenceExtensions.swift | 1 + Classes/Swift/Voip/Theme/VoipTexts.swift | 2 + Classes/Swift/Voip/Theme/VoipTheme.swift | 10 +- Classes/Swift/Voip/ViewModels/CallData.swift | 6 +- .../Voip/ViewModels/CallsViewModel.swift | 2 +- .../Voip/ViewModels/ConferenceViewModel.swift | 13 +++ .../Voip/ViewModels/ControlsViewModel.swift | 9 +- .../ActiveCallOrConferenceView.swift | 8 +- .../OutgoingCallView.swift | 2 +- .../ConferenceLayoutPickerView.swift | 87 ++++++++++++++++++ .../Voip/Views/Fragments/ControlsView.swift | 34 +++---- .../images/voip_conference_audio_only.png | Bin 0 -> 1260 bytes linphone.xcodeproj/project.pbxproj | 28 ++++-- 29 files changed, 302 insertions(+), 90 deletions(-) rename Classes/Swift/Conference/{data => Data}/Duration.swift (100%) rename Classes/Swift/Conference/{data => Data}/ScheduledConferenceData.swift (100%) rename Classes/Swift/Conference/{data => Data}/TimeZoneData.swift (100%) rename Classes/Swift/Conference/{models => ViewModels}/ConferenceSchedulingViewModel.swift (100%) create mode 100644 Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift rename Classes/Swift/Conference/{models => ViewModels}/ScheduledConferencesViewModel.swift (100%) rename Classes/Swift/Conference/{views => Views}/ConferenceHistoryDetailsView.swift (100%) rename Classes/Swift/Conference/{views => Views}/ConferenceSchedulingSummaryView.swift (100%) rename Classes/Swift/Conference/{views => Views}/ConferenceSchedulingView.swift (100%) rename Classes/Swift/Conference/{views => Views}/ConferenceWaitingRoomFragment.swift (63%) rename Classes/Swift/Conference/{views => Views}/ICSBubbleView.swift (100%) rename Classes/Swift/Conference/{views => Views}/ScheduledConferencesCell.swift (100%) rename Classes/Swift/Conference/{views => Views}/ScheduledConferencesView.swift (100%) delete mode 100644 Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift create mode 100644 Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift create mode 100644 Resources/images/voip_conference_audio_only.png diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index 7d3a0eec4..a99fb228b 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -274,7 +274,7 @@ import AVFoundation } if (isConference) { lcallParams.videoEnabled = true - lcallParams.videoDirection = isVideo ? .SendRecv : .RecvOnly + lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly } else { lcallParams.videoEnabled = isVideo } diff --git a/Classes/Swift/Conference/data/Duration.swift b/Classes/Swift/Conference/Data/Duration.swift similarity index 100% rename from Classes/Swift/Conference/data/Duration.swift rename to Classes/Swift/Conference/Data/Duration.swift diff --git a/Classes/Swift/Conference/data/ScheduledConferenceData.swift b/Classes/Swift/Conference/Data/ScheduledConferenceData.swift similarity index 100% rename from Classes/Swift/Conference/data/ScheduledConferenceData.swift rename to Classes/Swift/Conference/Data/ScheduledConferenceData.swift diff --git a/Classes/Swift/Conference/data/TimeZoneData.swift b/Classes/Swift/Conference/Data/TimeZoneData.swift similarity index 100% rename from Classes/Swift/Conference/data/TimeZoneData.swift rename to Classes/Swift/Conference/Data/TimeZoneData.swift diff --git a/Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift similarity index 100% rename from Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift rename to Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift diff --git a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift new file mode 100644 index 000000000..246f5f905 --- /dev/null +++ b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * 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 + * aDouble with this program. If not, see . + */ + + +import Foundation +import linphonesw + +class ConferenceWaitingRoomViewModel: ControlsViewModel { + + + static let sharedModel = ConferenceWaitingRoomViewModel() + + + let joinLayout = MutableLiveData() + let joinInProgress = MutableLiveData(false) + let showLayoutPicker = MutableLiveData() + + + override init() { + super.init() + self.reset() + } + + func reset() { + joinLayout.value = .Grid + joinInProgress.value = false + isMicrophoneMuted.value = !micAuthorized() + isMuteMicrophoneEnabled.value = true + isSpeakerSelected.value = true + isVideoEnabled.value = true + isVideoAvailable.value = core.videoCaptureEnabled + showLayoutPicker.value = false + } + + override func toggleMuteMicrophone() { + if (!micAuthorized()) { + AVAudioSession.sharedInstance().requestRecordPermission { granted in + if granted { + self.isMicrophoneMuted.value = self.isMicrophoneMuted.value != true + } + } + } + self.isMicrophoneMuted.value = self.isMicrophoneMuted.value != true + } + + override func toggleSpeaker() { + isSpeakerSelected.value = isSpeakerSelected.value != true + } + + override func toggleVideo() { + isVideoEnabled.value = isVideoEnabled.value != true + } + + override func updateUI() { + + } + +} diff --git a/Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift b/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift similarity index 100% rename from Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift rename to Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift diff --git a/Classes/Swift/Conference/views/ConferenceHistoryDetailsView.swift b/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift similarity index 100% rename from Classes/Swift/Conference/views/ConferenceHistoryDetailsView.swift rename to Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift diff --git a/Classes/Swift/Conference/views/ConferenceSchedulingSummaryView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift similarity index 100% rename from Classes/Swift/Conference/views/ConferenceSchedulingSummaryView.swift rename to Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift diff --git a/Classes/Swift/Conference/views/ConferenceSchedulingView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift similarity index 100% rename from Classes/Swift/Conference/views/ConferenceSchedulingView.swift rename to Classes/Swift/Conference/Views/ConferenceSchedulingView.swift diff --git a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift similarity index 63% rename from Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift rename to Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift index 5a6857164..c2d1d6f12 100644 --- a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift @@ -38,6 +38,8 @@ import linphonesw let subject = StyledLabel(VoipTheme.conference_preview_subject_font) let localVideo = UIView() let switchCamera = UIImageView(image: UIImage(named:"voip_change_camera")?.tinted(with:.white)) + let noVideoLabel = StyledLabel(VoipTheme.conference_waiting_room_no_video_font, VoipTexts.conference_waiting_room_video_disabled) + let buttonsView = UIStackView() let cancel = FormButton(title: VoipTexts.cancel.uppercased(), backgroundStateColors: VoipTheme.primary_colors_background_gray, bold:false) let start = FormButton(title: VoipTexts.conference_waiting_room_start_call.uppercased(), backgroundStateColors: VoipTheme.primary_colors_background) @@ -46,7 +48,6 @@ import linphonesw var conferenceUrl : String? = nil let conferenceSubject = MutableLiveData() - static let compositeDescription = UICompositeViewDescription(ConferenceWaitingRoomFragment.self, statusBar: StatusBarView.self, tabBar: nil, sideMenu: nil, fullscreen: false, isLeftFragment: false,fragmentWith: nil) static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription } func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription } @@ -63,10 +64,38 @@ import linphonesw } // Controls - let controlsView = ControlsView(showVideo: true) + let controlsView = ControlsView(showVideo: true, controlsViewModel: ConferenceWaitingRoomViewModel.sharedModel) view.addSubview(controlsView) controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() + // Layoout picker + let layoutPicker = CallControlButton(imageInset : UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8),buttonTheme: VoipTheme.conf_waiting_room_layout_picker, onClickAction: { + ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value != true + }) + view.addSubview(layoutPicker) + layoutPicker.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).alignParentRight(withMargin:SharedLayoutConstants.buttons_bottom_margin).done() + + ConferenceWaitingRoomViewModel.sharedModel.joinLayout.readCurrentAndObserve { layout in + var icon = "" + switch (layout!) { + case .Grid: icon = "voip_conference_mosaic"; break + case .ActiveSpeaker: icon = "voip_conference_active_speaker"; break + case .Legacy: icon = "voip_conference_audio_only"; break + } + layoutPicker.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: icon ,tintColor: LightDarkColor(.white,.white))]) + } + + let layoutPickerView = ConferenceLayoutPickerView() + view.addSubview(layoutPickerView) + layoutPickerView.alignAbove(view:layoutPicker,withMargin:button_spacing).alignVerticalCenterWith(layoutPicker).done() + + ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.readCurrentAndObserve { show in + layoutPicker.isSelected = show == true + layoutPickerView.isHidden = show != true + if (show == true) { + self.view.bringSubviewToFront(layoutPickerView) + } + } // Form buttons buttonsView.axis = .horizontal @@ -86,25 +115,29 @@ import linphonesw CallManager.instance().terminateCall(call: call.getCobject) } } - ConferenceWaitingRoomViewModel.shared.joinInProgress.value = false + ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.value = false PhoneMainView.instance().popView(self.compositeViewDescription()) } start.onClick { - ConferenceWaitingRoomViewModel.shared.joinInProgress.value = true - self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false, isVideo: true, isConference: true) } + ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.value = true + self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false, isVideo: ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value!, isConference: true) } } - ConferenceWaitingRoomViewModel.shared.joinInProgress.readCurrentAndObserve { joining in + ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.readCurrentAndObserve { joining in self.start.isEnabled = joining != true - self.localVideo.isHidden = joining == true + //self.localVideo.isHidden = joining == true (UX question as video window goes black by the core, better black or hidden ?) + self.noVideoLabel.isHidden = joining == true + layoutPicker.isHidden = joining == true if (joining == true) { self.view.addSubview(self.conferenceJoinSpinner) self.conferenceJoinSpinner.square(IncomingOutgoingCommonView.spinner_size).center().done() self.conferenceJoinSpinner.startRotation() + controlsView.isHidden = true } else { self.conferenceJoinSpinner.stopRotation() self.conferenceJoinSpinner.removeFromSuperview() + controlsView.isHidden = false } } @@ -126,11 +159,22 @@ import linphonesw Core.get().videoPreviewEnabled = true } + self.view.addSubview(noVideoLabel) + noVideoLabel.center().done() + + ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.readCurrentAndObserve { videoEnabled in + Core.get().videoPreviewEnabled = videoEnabled == true + self.localVideo.isHidden = videoEnabled != true + self.switchCamera.isHidden = videoEnabled != true + self.noVideoLabel.isHidden = videoEnabled == true + } + + // Audio Routes audioRoutesView = AudioRoutesView() view.addSubview(audioRoutesView!) audioRoutesView!.alignBottomWith(otherView: controlsView).done() - ControlsViewModel.shared.audioRoutesSelected.readCurrentAndObserve { (audioRoutesSelected) in + ConferenceWaitingRoomViewModel.sharedModel.audioRoutesSelected.readCurrentAndObserve { (audioRoutesSelected) in self.audioRoutesView!.isHidden = audioRoutesSelected != true } audioRoutesView!.alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() @@ -140,15 +184,17 @@ import linphonesw override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(true) - ControlsViewModel.shared.audioRoutesSelected.value = false + ConferenceWaitingRoomViewModel.sharedModel.audioRoutesSelected.value = false + ConferenceWaitingRoomViewModel.sharedModel.reset() Core.get().nativePreviewWindow = localVideo - Core.get().videoPreviewEnabled = true + Core.get().videoPreviewEnabled = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true } override func viewWillDisappear(_ animated: Bool) { ControlsViewModel.shared.fullScreenMode.value = false Core.get().nativePreviewWindow = nil Core.get().videoPreviewEnabled = false + ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.value = false super.viewWillDisappear(animated) } diff --git a/Classes/Swift/Conference/views/ICSBubbleView.swift b/Classes/Swift/Conference/Views/ICSBubbleView.swift similarity index 100% rename from Classes/Swift/Conference/views/ICSBubbleView.swift rename to Classes/Swift/Conference/Views/ICSBubbleView.swift diff --git a/Classes/Swift/Conference/views/ScheduledConferencesCell.swift b/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift similarity index 100% rename from Classes/Swift/Conference/views/ScheduledConferencesCell.swift rename to Classes/Swift/Conference/Views/ScheduledConferencesCell.swift diff --git a/Classes/Swift/Conference/views/ScheduledConferencesView.swift b/Classes/Swift/Conference/Views/ScheduledConferencesView.swift similarity index 100% rename from Classes/Swift/Conference/views/ScheduledConferencesView.swift rename to Classes/Swift/Conference/Views/ScheduledConferencesView.swift diff --git a/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift deleted file mode 100644 index bf428bf79..000000000 --- a/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2010-2021 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * 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 - * aDouble with this program. If not, see . - */ - - -import Foundation -import linphonesw - -class ConferenceWaitingRoomViewModel { - - var core : Core { get { Core.get() } } - static let shared = ConferenceWaitingRoomViewModel() - - let joinWithVideo = MutableLiveData() - let layout = MutableLiveData() - let joinInProgress = MutableLiveData(false) - - init () { - - } - - - -} diff --git a/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift index 46008f213..03ccbff0a 100644 --- a/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift +++ b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift @@ -285,6 +285,13 @@ extension UIView { return self } + func alignVerticalCenterWith(_ view:UIView) -> UIView { + snp.makeConstraints { (make) in + make.centerX.equalTo(view) + } + return self + } + func toLeftOf(_ view:UIView) -> UIView { snp.makeConstraints { (make) in diff --git a/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift index 483f3bc16..986c44007 100644 --- a/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift +++ b/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift @@ -29,5 +29,6 @@ extension Conference : CustomStringConvertible { } return "" } + } diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift index f8e1d2649..60e5bdafd 100644 --- a/Classes/Swift/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -95,6 +95,8 @@ import UIKit static let conference_display_no_active_speaker = NSLocalizedString("No active speaker",comment:"") static let conference_waiting_room_start_call = NSLocalizedString("Start",comment:"") static let conference_waiting_room_cancel_call = NSLocalizedString("Cancel",comment:"") + static let conference_waiting_room_video_disabled = NSLocalizedString("Video is currently disabled",comment:"") + static let conference_scheduled = NSLocalizedString("Conferences",comment:"") static let conference_too_many_participants_for_mosaic_layout = NSLocalizedString("You can't change conference layout as there is too many participants",comment:"") static let conference_participant_paused = NSLocalizedString("(paused)",comment:"") diff --git a/Classes/Swift/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift index 72d6ae29e..47ddd4ca6 100644 --- a/Classes/Swift/Voip/Theme/VoipTheme.swift +++ b/Classes/Swift/Voip/Theme/VoipTheme.swift @@ -123,7 +123,8 @@ class VoipTheme { // Names & values replicated from Android static let conference_invite_subject_font = TextStyle(fgColor: LightDarkColor(voip_dark_gray,voip_dark_gray), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 14.0) static let conference_invite_title_font = TextStyle(fgColor: LightDarkColor(dark_grey_color,dark_grey_color), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 16.0) static let conference_preview_subject_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 24.0) - + static let conference_waiting_room_no_video_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 16.0) + static let empty_list_font = TextStyle(fgColor: primaryTextColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 18.0) @@ -203,7 +204,6 @@ class VoipTheme { // Names & values replicated from Android // Buttons Icons (State colors) + Background colors - static let call_terminate = ButtonTheme( tintableStateIcons:[UIButton.State.normal.rawValue : TintableIcon(name: "voip_hangup",tintColor: LightDarkColor(.white,.white))], backgroundStateColors: [ @@ -268,6 +268,12 @@ class VoipTheme { // Names & values replicated from Android ], backgroundStateColors: button_background) + // Waiting room layout picker + + static let conf_waiting_room_layout_picker = ButtonTheme( + tintableStateIcons:[:], + backgroundStateColors: button_toggle_background_reverse) + // AUdio routes static let route_bluetooth = ButtonTheme( tintableStateIcons:[ diff --git a/Classes/Swift/Voip/ViewModels/CallData.swift b/Classes/Swift/Voip/ViewModels/CallData.swift index 988c79729..a15fa2270 100644 --- a/Classes/Swift/Voip/ViewModels/CallData.swift +++ b/Classes/Swift/Voip/ViewModels/CallData.swift @@ -88,7 +88,7 @@ class CallData { isRemotelyPaused.value = isCallRemotelyPaused() canBePaused.value = canCallBePaused() let conference = call.conference - isInRemoteConference.value = conference != nil || CallManager.getAppData(call: call.getCobject!)?.isConference == true + isInRemoteConference.value = conference != nil || isCallingAConference() if (conference != nil) { remoteConferenceSubject.value = conference?.subject != nil && (conference?.subject.count)! > 0 ? conference!.subject : VoipTexts.conference_default_title } @@ -199,4 +199,8 @@ class CallData { isPaused.value = isCallPaused() } + func isCallingAConference() -> Bool { + return CallManager.getAppData(call: call.getCobject!)?.isConference == true + } + } diff --git a/Classes/Swift/Voip/ViewModels/CallsViewModel.swift b/Classes/Swift/Voip/ViewModels/CallsViewModel.swift index 294491b9e..284868eb0 100644 --- a/Classes/Swift/Voip/ViewModels/CallsViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/CallsViewModel.swift @@ -185,6 +185,6 @@ class CallsViewModel { updateUnreadChatCount() } - + } diff --git a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift index 4a1aeb2c1..345e9f552 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift @@ -181,6 +181,19 @@ class ConferenceViewModel { conference.addDelegate(delegate: self.conferenceDelegate!) isRecording.value = conference.isRecording updateConferenceLayout(conference: conference) + + if let call = core.currentCall, CallManager.getAppData(call: call.getCobject!)?.isConference == true { // Apply waiting room preference + if (ConferenceWaitingRoomViewModel.sharedModel.isSpeakerSelected.value == true) { + ControlsViewModel.shared.forceSpeakerAudioRoute() + } else { + ControlsViewModel.shared.forceEarpieceAudioRoute() + ControlsViewModel.shared.updateUI() + } + Core.get().micEnabled = ConferenceWaitingRoomViewModel.sharedModel.isMicrophoneMuted.value != true + conference.layout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! + updateConferenceLayout(conference: conference) + } + } func configureConference(_ conference: Conference) { diff --git a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift index 5ee71fd59..35d9f9036 100644 --- a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift @@ -137,10 +137,9 @@ class ControlsViewModel { func toggleVideo() { if let conference = core.conference, conference.isIn { - if let params = try?core.createConferenceParams(conference:conference) { - let videoEnabled = conference.currentParams?.videoEnabled == true - params.videoEnabled = !videoEnabled - _ = conference.updateParams(params: params) + if let currentCall = core.currentCall, let params = try?core.createCallParams(call: currentCall) { + params.videoDirection = params.videoDirection == MediaDirection.RecvOnly ? MediaDirection.SendRecv : MediaDirection.RecvOnly + try?currentCall.update(params: params) } } else if let currentCall = core.currentCall { let state = currentCall.state @@ -159,7 +158,7 @@ class ControlsViewModel { } - private func updateUI() { + func updateUI() { updateVideoAvailable() updateVideoEnabled() updateMicState() diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index e75519c6f..8583bef83 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -65,7 +65,7 @@ import linphonesw // Controls - let controlsView = ControlsView(showVideo: true) + let controlsView = ControlsView(showVideo: true, controlsViewModel: ControlsViewModel.shared) view.addSubview(controlsView) controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() @@ -315,7 +315,11 @@ import linphonesw if (data?.isOutgoing.value == true || data?.isIncoming.value == true) { PhoneMainView.instance().popView(self.compositeViewDescription()) } else { - PhoneMainView.instance().changeCurrentView(self.compositeViewDescription()) + if (data!.isCallingAConference()) { + PhoneMainView.instance().pop(toView: self.compositeViewDescription()) + } else { + PhoneMainView.instance().changeCurrentView(self.compositeViewDescription()) + } } } else { PhoneMainView.instance().changeCurrentView(self.compositeViewDescription()) diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift index 31286af27..613f05eb4 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift @@ -47,7 +47,7 @@ import linphonesw cancelCall.alignParentLeft(withMargin:SharedLayoutConstants.margin_call_view_side_controls_buttons).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done() // Controls - let controlsView = ControlsView(showVideo: false) + let controlsView = ControlsView(showVideo: false, controlsViewModel: ControlsViewModel.shared) view.addSubview(controlsView) controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() diff --git a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift new file mode 100644 index 000000000..b9c010cc0 --- /dev/null +++ b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift @@ -0,0 +1,87 @@ +/* + * 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 UIKit + +class ConferenceLayoutPickerView: UIStackView { + + // Layout constants + let corner_radius = 6.7 + let margin = 10.0 + + init () { + super.init(frame: .zero) + axis = .vertical + distribution = .equalCentering + alignment = .center + spacing = ControlsView.controls_button_spacing + backgroundColor = VoipTheme.voip_gray + layer.cornerRadius = corner_radius + clipsToBounds = true + + let padding = UIView() + padding.height(margin/2).done() + addArrangedSubview(padding) + + + let grid = CallControlButton(imageInset : UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),buttonTheme: VoipTheme.conf_waiting_room_layout_picker, onClickAction: { + ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value = .Grid + ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = false + + }) + grid.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_mosaic" ,tintColor: LightDarkColor(.white,.white))]) + addArrangedSubview(grid) + + let activeSpeaker = CallControlButton(imageInset : UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),buttonTheme: VoipTheme.conf_waiting_room_layout_picker, onClickAction: { + ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value = .ActiveSpeaker + ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = false + }) + activeSpeaker.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_active_speaker" ,tintColor: LightDarkColor(.white,.white))]) + addArrangedSubview(activeSpeaker) + + let audioOnly = CallControlButton(imageInset : UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),buttonTheme: VoipTheme.conf_waiting_room_layout_picker, onClickAction: { + ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value = .Legacy + ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = false + }) + audioOnly.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_audio_only" ,tintColor: LightDarkColor(.white,.white))]) + addArrangedSubview(audioOnly) + + ConferenceWaitingRoomViewModel.sharedModel.joinLayout.readCurrentAndObserve { layout in + grid.isSelected = layout == .Grid + activeSpeaker.isSelected = layout == .ActiveSpeaker + audioOnly.isSelected = layout == .Legacy + } + + let padding2 = UIView() + padding2.height(margin/2).done() + addArrangedSubview(padding2) + + + size(w:CGFloat(CallControlButton.default_size)+margin, h : 3*CGFloat(CallControlButton.default_size)+3*CGFloat(ControlsView.controls_button_spacing)+2*margin).done() + + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + + diff --git a/Classes/Swift/Voip/Views/Fragments/ControlsView.swift b/Classes/Swift/Voip/Views/Fragments/ControlsView.swift index 58c285aa5..b091a049c 100644 --- a/Classes/Swift/Voip/Views/Fragments/ControlsView.swift +++ b/Classes/Swift/Voip/Views/Fragments/ControlsView.swift @@ -25,7 +25,7 @@ class ControlsView: UIStackView { // Layout constants static let controls_button_spacing = 5.0 - init (showVideo:Bool) { + init (showVideo:Bool, controlsViewModel:ControlsViewModel) { super.init(frame: .zero) axis = .horizontal distribution = .equalSpacing @@ -34,35 +34,35 @@ class ControlsView: UIStackView { // Mute let mute = CallControlButton(buttonTheme: VoipTheme.call_mute, onClickAction: { - ControlsViewModel.shared.toggleMuteMicrophone() + controlsViewModel.toggleMuteMicrophone() }) addArrangedSubview(mute) - ControlsViewModel.shared.isMicrophoneMuted.readCurrentAndObserve { (muted) in + controlsViewModel.isMicrophoneMuted.readCurrentAndObserve { (muted) in mute.isSelected = muted == true } - ControlsViewModel.shared.isMuteMicrophoneEnabled.readCurrentAndObserve { (enabled) in + controlsViewModel.isMuteMicrophoneEnabled.readCurrentAndObserve { (enabled) in mute.isEnabled = enabled == true } // Speaker let speaker = CallControlButton(buttonTheme: VoipTheme.call_speaker, onClickAction: { - ControlsViewModel.shared.toggleSpeaker() + controlsViewModel.toggleSpeaker() }) addArrangedSubview(speaker) - ControlsViewModel.shared.isSpeakerSelected.readCurrentAndObserve { (selected) in + controlsViewModel.isSpeakerSelected.readCurrentAndObserve { (selected) in speaker.isSelected = selected == true } // Audio routes let routes = CallControlButton(buttonTheme: VoipTheme.call_audio_route, onClickAction: { - ControlsViewModel.shared.toggleRoutesMenu() + controlsViewModel.toggleRoutesMenu() }) addArrangedSubview(routes) - ControlsViewModel.shared.audioRoutesSelected.readCurrentAndObserve { (selected) in + controlsViewModel.audioRoutesSelected.readCurrentAndObserve { (selected) in routes.isSelected = selected == true } - ControlsViewModel.shared.audioRoutesEnabled.readCurrentAndObserve { (routesEnabled) in + controlsViewModel.audioRoutesEnabled.readCurrentAndObserve { (routesEnabled) in speaker.isHidden = routesEnabled == true routes.isHidden = !speaker.isHidden } @@ -71,11 +71,11 @@ class ControlsView: UIStackView { if (showVideo) { let video = CallControlButton(buttonTheme: VoipTheme.call_video, onClickAction: { if AVCaptureDevice.authorizationStatus(for: .video) == .authorized { - ControlsViewModel.shared.toggleVideo() + controlsViewModel.toggleVideo() } else { AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in if granted { - ControlsViewModel.shared.toggleVideo() + controlsViewModel.toggleVideo() } else { VoipDialog(message:VoipTexts.camera_required_for_video).show() } @@ -83,15 +83,15 @@ class ControlsView: UIStackView { } }) addArrangedSubview(video) - video.showActivityIndicatorDataSource = ControlsViewModel.shared.isVideoUpdateInProgress - ControlsViewModel.shared.isVideoEnabled.readCurrentAndObserve { (selected) in + video.showActivityIndicatorDataSource = controlsViewModel.isVideoUpdateInProgress + controlsViewModel.isVideoEnabled.readCurrentAndObserve { (selected) in video.isSelected = selected == true } - ControlsViewModel.shared.isVideoAvailable.readCurrentAndObserve { (available) in - video.isEnabled = available == true && ControlsViewModel.shared.isVideoUpdateInProgress.value != true + controlsViewModel.isVideoAvailable.readCurrentAndObserve { (available) in + video.isEnabled = available == true && controlsViewModel.isVideoUpdateInProgress.value != true } - ControlsViewModel.shared.isVideoUpdateInProgress.readCurrentAndObserve { (updateInProgress) in - video.isEnabled = updateInProgress != true && ControlsViewModel.shared.isVideoAvailable.value == true + controlsViewModel.isVideoUpdateInProgress.readCurrentAndObserve { (updateInProgress) in + video.isEnabled = updateInProgress != true && controlsViewModel.isVideoAvailable.value == true } } diff --git a/Resources/images/voip_conference_audio_only.png b/Resources/images/voip_conference_audio_only.png new file mode 100644 index 0000000000000000000000000000000000000000..fd57a3f27e6395ad6cbf7ed19ea01cc010864ba9 GIT binary patch literal 1260 zcmVEX>4Tx04R}tkv&MmKp2MKri!9f2Rn#3WT?7W5EXIMDionYs1;guFuC*(nlvOS zE{=k0!NH%!s)LKOt`4q(Aov5~=;Wm6A|-y86k5c1aCZ;yeecWNcYshUG0kcl2Q=L_ zlku3C&a8?ZuMp6UURpk71x=7pPYq=lj@k>L)<(8MxA${&EeN`6RvC z)FMYf-!^b@-PDvl;Bp5Tcrs*DcBLRqA(sQ*&*+;nK>sb!wdVD$agNgmAVs}O+yDoM zz(}65*L~jI+1j^%Ya0Fg0T}OcgK~Phpa1{>24YJ`L;z<1l>oV$G+_|{000SaNLh0L z01ejw01ejxLMWSf00007bV*G`2j&6`4g&@7(F_;>00QVqL_t(|+U?xWOB7)o$MMg1 zW^MP7WCev1MhFSsN`fFdgaD3kkHlgz`+F`t18ALyVD9awg8(k^mgSKZtD*+by1bX7+(SHFvl9N}l`!{^ z|DZ9Rth8OX1%g+Zp}v_?cyo@ob?&u{)np(qG9#&HfmtCZJ31;tu9*`^kEl#Hx1Npr zJw{_ZSt%;dRrHpKnLW|k84XO3T-H7UplbcV44m=(t;0190CxO7QX0tVzywjTB83NL zMEo8^p`Khb4~BfoA_id)Vh{!)24N6l5C$OzAqHU(@^27Plg%G1koEf;BM?unxhO%( zBJL9a3mcQgeZL2BY-0!F!kATtOE+| z64Aq%dw0f*@pp^%nR+JO{{=v*lC^`Rx4EfvQ}@vY{je4bH&-k&h=<10J)cBy z^Ve`ypjQ~g>nKCL=R@jNuQ^ub*l^$IW(wllueAB`>t@{486@Box%{DR3dammY$8*6 zao#VK;~n=951FONMcKCcQh?ndxIOc!-W-E?Xky~V8i4EELum5^5$LyqjasnGAYKMh z^kL((iq{b*{J6CL!2%!}OIDjQ-Ji3wv!(t6W<*3pL_|bHL_|bHL_|bHL_|bHf%X$c W+0(d@F1m350000