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 000000000..fd57a3f27 Binary files /dev/null and b/Resources/images/voip_conference_audio_only.png differ diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 32681ee96..aa112396e 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -684,6 +684,8 @@ C65A5D3F27216E3A005BA038 /* CallData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65A5D3E27216E3A005BA038 /* CallData.swift */; }; C65A5D45272196AE005BA038 /* OptionalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65A5D44272196AE005BA038 /* OptionalExtensions.swift */; }; C662D0A927EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C662D0A827EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift */; }; + C662D0AD27EB874E00C02D4A /* voip_conference_audio_only.png in Resources */ = {isa = PBXBuildFile; fileRef = C662D0AC27EB874E00C02D4A /* voip_conference_audio_only.png */; }; + C662D0AF27EB894300C02D4A /* ConferenceLayoutPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C662D0AE27EB894300C02D4A /* ConferenceLayoutPickerView.swift */; }; C666756F264C925800A0273C /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DA657B261C950C0020CB43 /* VFSUtil.swift */; }; C6667571264C925B00A0273C /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DA657B261C950C0020CB43 /* VFSUtil.swift */; }; C66B03BB26E8EB1A009B5EDC /* UIChatReplyBubbleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C66B03BD26E8EB1A009B5EDC /* UIChatReplyBubbleView.xib */; }; @@ -1854,6 +1856,8 @@ C65A5D3E27216E3A005BA038 /* CallData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallData.swift; sourceTree = ""; }; C65A5D44272196AE005BA038 /* OptionalExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalExtensions.swift; sourceTree = ""; }; C662D0A827EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceWaitingRoomViewModel.swift; sourceTree = ""; }; + C662D0AC27EB874E00C02D4A /* voip_conference_audio_only.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_audio_only.png; sourceTree = ""; }; + C662D0AE27EB894300C02D4A /* ConferenceLayoutPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ConferenceLayoutPickerView.swift; path = Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift; sourceTree = SOURCE_ROOT; }; C66B03BC26E8EB1A009B5EDC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UIChatReplyBubbleView.xib; sourceTree = ""; }; C66B03C126E8EB82009B5EDC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/UIChatReplyBubbleView.strings; sourceTree = ""; }; C66B03C326E8EB87009B5EDC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/UIChatReplyBubbleView.strings; sourceTree = ""; }; @@ -2666,6 +2670,7 @@ 633FEBE11D3CD5570014B822 /* images */ = { isa = PBXGroup; children = ( + C662D0AC27EB874E00C02D4A /* voip_conference_audio_only.png */, C6AF9225275F3D890087ACDE /* voip_conference_new_selected.png */, C6AF921D275E51860087ACDE /* conference_schedule_calendar_default.png */, C6AF9213275D67EB0087ACDE /* conference_schedule_time_default.png */, @@ -3554,16 +3559,17 @@ C6EA2F492752237C008E60F8 /* Conference */ = { isa = PBXGroup; children = ( - C6EA2F4C2754C691008E60F8 /* data */, - C6EA2F4B2754C683008E60F8 /* views */, - C6EA2F4D2754C69F008E60F8 /* models */, + C6EA2F4C2754C691008E60F8 /* Data */, + C6EA2F4B2754C683008E60F8 /* Views */, + C6EA2F4D2754C69F008E60F8 /* ViewModels */, ); path = Conference; sourceTree = ""; }; - C6EA2F4B2754C683008E60F8 /* views */ = { + C6EA2F4B2754C683008E60F8 /* Views */ = { isa = PBXGroup; children = ( + C662D0AE27EB894300C02D4A /* ConferenceLayoutPickerView.swift */, C690CCB0275764CD00609077 /* ConferenceSchedulingView.swift */, C61E409A275A20A300CCE602 /* ConferenceSchedulingSummaryView.swift */, C6AF920D275D38090087ACDE /* ScheduledConferencesView.swift */, @@ -3572,27 +3578,27 @@ C6AF9229275F6BA10087ACDE /* ConferenceHistoryDetailsView.swift */, C6AF9219275E2E010087ACDE /* ConferenceWaitingRoomFragment.swift */, ); - path = views; + path = Views; sourceTree = ""; }; - C6EA2F4C2754C691008E60F8 /* data */ = { + C6EA2F4C2754C691008E60F8 /* Data */ = { isa = PBXGroup; children = ( C6EA2F6A2754DDF9008E60F8 /* Duration.swift */, C6EA2F622754DBBE008E60F8 /* TimeZoneData.swift */, C6EA2F582754C91C008E60F8 /* ScheduledConferenceData.swift */, ); - path = data; + path = Data; sourceTree = ""; }; - C6EA2F4D2754C69F008E60F8 /* models */ = { + C6EA2F4D2754C69F008E60F8 /* ViewModels */ = { isa = PBXGroup; children = ( - C6EA2F662754DC45008E60F8 /* ConferenceSchedulingViewModel.swift */, C662D0A827EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift */, + C6EA2F662754DC45008E60F8 /* ConferenceSchedulingViewModel.swift */, C6AF9217275E13790087ACDE /* ScheduledConferencesViewModel.swift */, ); - path = models; + path = ViewModels; sourceTree = ""; }; D326483415887D4400930C67 /* Utils */ = { @@ -3978,6 +3984,7 @@ 633FEF1F1D3CD55A0014B822 /* presence_online@2x.png in Resources */, 633FEE641D3CD5590014B822 /* footer_chat_disabled.png in Resources */, 633FEE9C1D3CD55A0014B822 /* numpad_0_default@2x.png in Resources */, + C662D0AD27EB874E00C02D4A /* voip_conference_audio_only.png in Resources */, 633FEE3C1D3CD5590014B822 /* contacts_all_default.png in Resources */, 633FEE171D3CD5590014B822 /* chat_send_disabled@2x.png in Resources */, C61B1BF22667D075001A4E4A /* menu_security_default.png in Resources */, @@ -5016,6 +5023,7 @@ D3807FC315C28940005BE9BC /* DCRoundSwitchOutlineLayer.m in Sources */, CF7602E221086EB200749F76 /* RecordingsListTableView.m in Sources */, D3807FC515C28940005BE9BC /* DCRoundSwitchToggleLayer.m in Sources */, + C662D0AF27EB894300C02D4A /* ConferenceLayoutPickerView.swift in Sources */, C6586149273E595700A0DBFC /* VoipExtraButtonsView.swift in Sources */, 633E41821D74259000320475 /* AssistantLinkView.m in Sources */, C6710F5727229DEE00ED888F /* TextStyle.swift in Sources */,