diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index c12826d17..9c3a5b1c4 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -286,9 +286,13 @@ import AVFoundation lcallParams.mediaEncryption = .ZRTP } if (isConference) { - lcallParams.videoEnabled = true - lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly - lcallParams.conferenceVideoLayout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! + if (ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! != .AudioOnly) { + lcallParams.videoEnabled = true + lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly + lcallParams.conferenceVideoLayout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! == .Grid ? .Grid : .ActiveSpeaker + } else { + lcallParams.videoEnabled = false + } } else { lcallParams.videoEnabled = isVideo } diff --git a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift index 4b00f24dd..14b34a5eb 100644 --- a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift +++ b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift @@ -28,7 +28,7 @@ class ConferenceWaitingRoomViewModel: ControlsViewModel { static let sharedModel = ConferenceWaitingRoomViewModel() - let joinLayout = MutableLiveData() + let joinLayout = MutableLiveData() let joinInProgress = MutableLiveData(false) let showLayoutPicker = MutableLiveData() @@ -39,12 +39,12 @@ class ConferenceWaitingRoomViewModel: ControlsViewModel { } func reset() { - joinLayout.value = .ActiveSpeaker // TODO add setting + joinLayout.value = Core.get().defaultConferenceLayout == .Grid ? .Grid : .ActiveSpeaker joinInProgress.value = false isMicrophoneMuted.value = !micAuthorized() isMuteMicrophoneEnabled.value = true isSpeakerSelected.value = true - isVideoEnabled.value = true + isVideoEnabled.value = false isVideoAvailable.value = core.videoCaptureEnabled showLayoutPicker.value = false } diff --git a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift index 4962b173a..a7c6f0de9 100644 --- a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift @@ -80,7 +80,10 @@ import linphonesw switch (layout!) { case .Grid: icon = "voip_conference_mosaic"; break case .ActiveSpeaker: icon = "voip_conference_active_speaker"; break - // Todo audio only case .Legacy: icon = "voip_conference_audio_only"; break + case .AudioOnly: + icon = "voip_conference_audio_only" + ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value = false + break } layoutPicker.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: icon ,tintColor: LightDarkColor(.white,.white))]) } diff --git a/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift index d4aff4ef7..39bd741f8 100644 --- a/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift +++ b/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift @@ -39,9 +39,7 @@ extension Address { } func addressBookEnhancedDisplayName() -> String? { - if (username == Core.get().defaultAccount?.contactAddress?.username) { - return VoipTexts.me - } else if let contact = FastAddressBook.getContactWith(getCobject) { + if let contact = FastAddressBook.getContactWith(getCobject) { return contact.displayName } else if (!displayName.isEmpty) { return displayName diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift index 303d1aa2e..da85fae6c 100644 --- a/Classes/Swift/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -23,7 +23,6 @@ import UIKit @objc class VoipTexts : NSObject { // From android key names. Added intentionnally with NSLocalizedString calls for each key, so it can be picked up by translation system (Weblate or Xcode). static let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as! String - static let me = NSLocalizedString("Me",comment:"") // Calls static let call_incoming_title = NSLocalizedString("Incoming Call",comment:"") @@ -95,6 +94,7 @@ import UIKit static let conference_invite_participants_count = NSLocalizedString("%d participants",comment:"") static let conference_display_mode_mosaic = NSLocalizedString("Mosaic mode",comment:"") static let conference_display_mode_active_speaker = NSLocalizedString("Active speaker mode",comment:"") + static let conference_display_mode_audio_only = NSLocalizedString("Audio only mode",comment:"") 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:"") diff --git a/Classes/Swift/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift index eace5a9a9..029ef1e62 100644 --- a/Classes/Swift/Voip/Theme/VoipTheme.swift +++ b/Classes/Swift/Voip/Theme/VoipTheme.swift @@ -27,7 +27,7 @@ class VoipTheme { // Names & values replicated from Android static let voip_light_gray = UIColor(hex:"#D0D8DE") static let voip_dark_gray = UIColor(hex:"#4B5964") static let voip_gray = UIColor(hex:"#96A5B1") - static let voip_gray_background = UIColor(hex:"#D8D8D8") + static let voip_gray_background = UIColor(hex:"#AFAFAF") static let voip_call_record_background = UIColor(hex:"#EBEBEB") static let voip_calls_list_inactive_background = UIColor(hex:"#F0F1F2") static let voip_translucent_popup_background = UIColor(hex:"#A64B5964") @@ -115,6 +115,8 @@ class VoipTheme { // Names & values replicated from Android static let conference_participant_sip_uri_font = TextStyle(fgColor: LightDarkColor(primary_color,primary_color), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 12.0) static let conference_participant_name_font_grid = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 15.0) static let conference_participant_name_font_as = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 12.0) + static let conference_participant_name_font_audio_only = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName, size: 14.0) + static let conference_mode_title = TextStyle(fgColor: LightDarkColor(dark_grey_color,dark_grey_color), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 17.0) static let conference_mode_title_selected = conference_mode_title.boldEd() static let conference_scheduling_font = TextStyle(fgColor: voipTextColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 17.0) diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift index 4180a406e..948622017 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift @@ -27,10 +27,12 @@ class ConferenceParticipantDeviceData { let videoEnabled = MutableLiveData() let activeSpeaker = MutableLiveData() + let micMuted = MutableLiveData() + let isInConference = MutableLiveData() - + var core : Core { get { Core.get() } } - + private var participantDeviceDelegate : ParticipantDeviceDelegate? init (participantDevice:ParticipantDevice, isMe:Bool) { @@ -40,30 +42,38 @@ class ConferenceParticipantDeviceData { onIsSpeakingChanged: { (participantDevice, isSpeaking) in Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) isspeaking = \(isSpeaking)") self.activeSpeaker.value = isSpeaking - }, onConferenceJoined: { (participantDevice) in + }, + onIsMuted: { (participantDevice, isMuted) in + Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) muted = \(isMuted)") + self.micMuted.value = isMuted + }, + onConferenceJoined: { (participantDevice) in Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) has joined the conference") self.isInConference.value = true - }, onConferenceLeft: { (participantDevice) in + }, + onConferenceLeft: { (participantDevice) in Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) has left the conference") self.isInConference.value = false - }, onStreamCapabilityChanged: { (participantDevice, direction, streamType) in + }, + onStreamCapabilityChanged: { (participantDevice, direction, streamType) in Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) video stream direction changed: \(direction)") self.videoEnabled.value = direction == MediaDirection.SendOnly || direction == MediaDirection.SendRecv if (streamType == StreamType.Video) { Log.i("[Conference Participant Device] Participant [\(participantDevice.address?.asStringUriOnly())] video capability changed to \(direction)") } - }, onStreamAvailabilityChanged: { (participantDevice, available, streamType) in + }, + onStreamAvailabilityChanged: { (participantDevice, available, streamType) in if (streamType == StreamType.Video) { Log.i("[Conference Participant Device] Participant [\(participantDevice.address?.asStringUriOnly())] video availability changed to \(available)") self.videoEnabled.value = available } } - + ) participantDevice.addDelegate(delegate: participantDeviceDelegate!) activeSpeaker.value = false - + micMuted.value = participantDevice.isMuted videoEnabled.value = participantDevice.getStreamAvailability(streamType: .Video) diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index edba6f0e9..5363943e9 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -33,6 +33,8 @@ import linphonesw var currentCallView : ActiveCallView? = nil var conferenceGridView: VoipConferenceGridView? = nil var conferenceActiveSpeakerView: VoipConferenceActiveSpeakerView? = nil + var conferenceAudioOnlyView: VoipConferenceAudioOnlyView? = nil + let conferenceJoinSpinner = RotatingSpinner() @@ -116,18 +118,17 @@ import linphonesw fullScreenMutableContainerView.addSubview(conferenceGridView!) conferenceGridView?.matchParentDimmensions().done() conferenceGridView?.isHidden = true - ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (isInConference) in + ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (exists) in self.updateNavigation() - if (isInConference == true) { + if (exists == true) { self.currentCallView!.isHidden = true self.extraButtonsView.isHidden = true self.conferencePausedView?.isHidden = true - let conferenceMode = ConferenceViewModel.shared.conferenceDisplayMode.value - self.conferenceGridView!.isHidden = conferenceMode != .Grid - self.conferenceActiveSpeakerView?.isHidden = conferenceMode != .ActiveSpeaker - self.conferenceGridView?.conferenceViewModel = ConferenceViewModel.shared + self.displaySelectedConferenceLayout() } else { self.conferenceGridView?.isHidden = true + self.conferenceActiveSpeakerView?.isHidden = true + self.conferenceActiveSpeakerView?.isHidden = true } } @@ -148,22 +149,20 @@ import linphonesw conferenceActiveSpeakerView?.matchParentDimmensions().done() conferenceActiveSpeakerView?.isHidden = true - // Conference mode switching + + // Conference audio only + conferenceAudioOnlyView = VoipConferenceAudioOnlyView() + fullScreenMutableContainerView.addSubview(conferenceAudioOnlyView!) + conferenceAudioOnlyView?.matchParentDimmensions().done() + conferenceAudioOnlyView?.isHidden = true + ConferenceViewModel.shared.conferenceDisplayMode.readCurrentAndObserve { (conferenceMode) in if (ConferenceViewModel.shared.conferenceExists.value == true) { - self.conferenceGridView!.isHidden = conferenceMode != .Grid - self.conferenceActiveSpeakerView!.isHidden = conferenceMode != .ActiveSpeaker - self.conferenceActiveSpeakerView?.conferenceViewModel = ConferenceViewModel.shared - } else { - self.conferenceActiveSpeakerView?.isHidden = true + self.displaySelectedConferenceLayout() } } - ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (isInConference) in - self.updateNavigation() - } - // Calls List ControlsViewModel.shared.goToCallsListEvent.observe { (_) in self.dismissableView = CallsListView() @@ -189,9 +188,8 @@ import linphonesw self.view.addSubview(self.dismissableView!) self.dismissableView?.matchParentDimmensions().done() let activeDisplayMode = ConferenceViewModel.shared.conferenceDisplayMode.value! - let indexPath = IndexPath(row: activeDisplayMode == .Grid ? 0 : 1, section: 0) + let indexPath = IndexPath(row: activeDisplayMode == .Grid ? 0 : activeDisplayMode == .ActiveSpeaker ? 1 : 2, section: 0) (self.dismissableView as! VoipConferenceDisplayModeSelectionView).optionsListView.selectRow(at:indexPath, animated: true, scrollPosition: .bottom) - } // Shading mask, everything before will be shaded upon displaying of the mask @@ -292,6 +290,22 @@ import linphonesw } + func displaySelectedConferenceLayout() { + let conferenceMode = ConferenceViewModel.shared.conferenceDisplayMode.value + self.conferenceGridView!.isHidden = conferenceMode != .Grid + self.conferenceActiveSpeakerView!.isHidden = conferenceMode != .ActiveSpeaker + self.conferenceAudioOnlyView!.isHidden = conferenceMode != .AudioOnly + if (conferenceMode == .Grid) { + self.conferenceGridView?.conferenceViewModel = ConferenceViewModel.shared + } + if (conferenceMode == .AudioOnly) { + self.conferenceAudioOnlyView?.conferenceViewModel = ConferenceViewModel.shared + } + if (conferenceMode == .ActiveSpeaker) { + self.conferenceActiveSpeakerView?.conferenceViewModel = ConferenceViewModel.shared + } + } + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(true) extraButtonsView.refresh() diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipAudioOnlyParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipAudioOnlyParticipantCell.swift new file mode 100644 index 000000000..133a297f5 --- /dev/null +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipAudioOnlyParticipantCell.swift @@ -0,0 +1,110 @@ +/* + * 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 UIKit +import Foundation +import SnapKit +import linphonesw + +class VoipAudioOnlyParticipantCell: UICollectionViewCell { + + // Layout Constants + static let cell_height = 80.0 + static let avatar_size = 40.0 + static let mute_size = 30.0 + let corner_radius = 6.7 + let common_margin = 10.0 + + + let avatar = Avatar(diameter:VoipCallCell.avatar_size,color:VoipTheme.voipBackgroundColor, textStyle: VoipTheme.call_generated_avatar_small) + let paused = UIImageView(image: UIImage(named: "voip_pause")?.tinted(with: .white)) + let muted = UIImageView(image: UIImage(named: "voip_micro_off")?.tinted(with: .white)) + + let displayName = StyledLabel(VoipTheme.conference_participant_name_font_as) + + var participantData: ConferenceParticipantDeviceData? = nil { + didSet { + if let data = participantData { + self.displayName.text = "" + data.isInConference.clearObservers() + data.isInConference.readCurrentAndObserve { (isIn) in + self.avatar.isHidden = isIn != true + self.paused.isHidden = isIn == true + data.participantDevice.address.map { + self.avatar.fillFromAddress(address: $0) + if let displayName = $0.addressBookEnhancedDisplayName() { + self.displayName.text = displayName + (isIn == true ? "" : " \(VoipTexts.conference_participant_paused)") + } + } + } + if (data.participantDevice.address == nil) { + avatar.isHidden = true + } + data.activeSpeaker.clearObservers() + data.activeSpeaker.readCurrentAndObserve { (active) in + if (active == true) { + self.layer.borderWidth = 2 + } else { + self.layer.borderWidth = 0 + } + } + data.micMuted.clearObservers() + data.micMuted.readCurrentAndObserve { (muted) in + self.muted.isHidden = muted != true + } + } + } + } + + + override init(frame:CGRect) { + super.init(frame:.zero) + contentView.height(VoipAudioOnlyParticipantCell.cell_height).matchParentSideBorders().done() + + layer.cornerRadius = corner_radius + clipsToBounds = true + contentView.backgroundColor = VoipTheme.voipParticipantBackgroundColor.get() + layer.borderColor = VoipTheme.primary_color.cgColor + + contentView.addSubview(avatar) + avatar.size(w: VoipCallCell.avatar_size, h: VoipCallCell.avatar_size).centerY().alignParentLeft(withMargin: common_margin).done() + + contentView.addSubview(paused) + paused.layer.cornerRadius = VoipAudioOnlyParticipantCell.avatar_size/2 + paused.clipsToBounds = true + paused.backgroundColor = VoipTheme.voip_gray + paused.size(w: VoipAudioOnlyParticipantCell.avatar_size, h: VoipAudioOnlyParticipantCell.avatar_size).alignParentLeft(withMargin: common_margin).centerY().done() + + contentView.addSubview(displayName) + displayName.centerY().toRightOf(avatar,withLeftMargin: common_margin).done() + displayName.numberOfLines = 3 + + contentView.addSubview(muted) + muted.layer.cornerRadius = VoipAudioOnlyParticipantCell.avatar_size/2 + muted.clipsToBounds = true + muted.backgroundColor = VoipTheme.voip_dark_gray + muted.size(w: VoipAudioOnlyParticipantCell.mute_size, h: VoipAudioOnlyParticipantCell.mute_size).alignParentRight(withMargin: common_margin).toRightOf(displayName,withLeftMargin: common_margin).centerY().done() + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceAudioOnlyView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceAudioOnlyView.swift new file mode 100644 index 000000000..664fb7aa6 --- /dev/null +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceAudioOnlyView.swift @@ -0,0 +1,216 @@ +/* + * 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 UIKit +import Foundation +import SnapKit +import linphonesw + +class VoipConferenceAudioOnlyView: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { + + // Layout constants : + let inter_cell = 5.0 + let record_pause_button_margin = 10.0 + let duration_margin_top = 4.0 + let record_pause_button_size = 40 + let record_pause_button_inset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7) + + + let subjectLabel = StyledLabel(VoipTheme.call_display_name_duration) + let duration = CallTimer(nil, VoipTheme.call_display_name_duration) + + let remotelyRecording = RemotelyRecordingView(height: ActiveCallView.remote_recording_height,text: VoipTexts.call_remote_recording) + var recordCallButtons : [CallControlButton] = [] + var pauseCallButtons : [CallControlButton] = [] + var grid : UICollectionView + var gridContainer = UIView() + + + var conferenceViewModel: ConferenceViewModel? = nil { + didSet { + if let model = conferenceViewModel { + model.subject.clearObservers() + model.subject.readCurrentAndObserve { (subject) in + self.subjectLabel.text = subject + } + duration.conference = model.conference.value + self.remotelyRecording.isRemotelyRecorded = model.isRemotelyRecorded + model.conferenceParticipantDevices.clearObservers() + model.conferenceParticipantDevices.readCurrentAndObserve { (_) in + self.reloadData() + } + model.isConferenceLocallyPaused.clearObservers() + model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in + self.pauseCallButtons.forEach { + $0.isSelected = paused == true + } + } + model.isRecording.clearObservers() + model.isRecording.readCurrentAndObserve { (selected) in + self.recordCallButtons.forEach { + $0.isSelected = selected == true + } + } + } + self.reloadData() + } + } + + init() { + + let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() + layout.minimumInteritemSpacing = 0 + layout.minimumLineSpacing = 0 + layout.estimatedItemSize = .zero + grid = UICollectionView(frame:.zero, collectionViewLayout: layout) + + super.init(frame: .zero) + + let headerView = UIStackView() + addSubview(headerView) + + headerView.distribution = .equalSpacing + headerView.alignment = .bottom + headerView.spacing = record_pause_button_margin + headerView.axis = .vertical + + let subjectDuration = UIView() + + subjectDuration.addSubview(subjectLabel) + subjectLabel.alignParentLeft().done() + + subjectDuration.addSubview(duration) + duration.alignParentLeft().alignUnder(view: subjectLabel,withMargin:duration_margin_top).done() + + let upperSection = UIStackView() + upperSection.distribution = .equalSpacing + upperSection.alignment = .center + upperSection.spacing = record_pause_button_margin + upperSection.axis = .horizontal + + upperSection.addArrangedSubview(subjectDuration) + subjectDuration.wrapContentY().done() + + // Record (with video) + let recordCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_record, onClickAction: { + self.conferenceViewModel?.toggleRecording() + }) + + let recordPauseView = UIStackView() + recordPauseView.spacing = record_pause_button_margin + recordCallButtons.append(recordCall) + recordPauseView.addArrangedSubview(recordCall) + + // Pause (with video) + let pauseCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_pause, onClickAction: { + self.conferenceViewModel?.togglePlayPause() + + }) + pauseCallButtons.append(pauseCall) + recordPauseView.addArrangedSubview(pauseCall) + + upperSection.addArrangedSubview(recordPauseView) + + headerView.addArrangedSubview(upperSection) + upperSection.matchParentSideBorders().alignParentTop(withMargin:ActiveCallView.top_displayname_margin_top).done() + + headerView.addArrangedSubview(remotelyRecording) + remotelyRecording.matchParentSideBorders().alignUnder(view:upperSection, withMargin:ActiveCallView.remote_recording_margin_top).height(CGFloat(ActiveCallView.remote_recording_height)).done() + + // CollectionView + grid.dataSource = self + grid.delegate = self + grid.register(VoipAudioOnlyParticipantCell.self, forCellWithReuseIdentifier: "VoipAudioOnlyParticipantCell") + grid.backgroundColor = .clear + grid.isScrollEnabled = false + addSubview(gridContainer) + gridContainer.addSubview(grid) + gridContainer.backgroundColor = VoipTheme.voipBackgroundColor.get() + + gridContainer.matchParentSideBorders(insetedByDx: inter_cell).alignUnder(view:headerView,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom(withMargin: inter_cell).done() + grid.matchParentDimmensions().done() + + headerView.matchParentSideBorders().alignParentTop().done() + + } + + + // UICollectionView related delegates + + func reloadData() { + conferenceViewModel?.conferenceParticipantDevices.value?.forEach { + $0.clearObservers() + } + if (self.isHidden) { + self.grid.reloadData() + return + } + self.grid.reloadData() + } + + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return inter_cell + } + + func collectionView(_ collectionView: UICollectionView, layout + collectionViewLayout: UICollectionViewLayout, + minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return inter_cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + if (self.isHidden) { + return 0 + } + guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else { + return .zero + } + return participantsCount + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell:VoipAudioOnlyParticipantCell = collectionView.dequeueReusableCell(withReuseIdentifier: "VoipAudioOnlyParticipantCell", for: indexPath) as! VoipAudioOnlyParticipantCell + guard let participantData = conferenceViewModel?.conferenceParticipantDevices.value?[indexPath.row] else { + return cell + } + cell.participantData = participantData + return cell + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + + guard let participantsCount:Int = conferenceViewModel?.conferenceParticipantDevices.value?.count else { + return .zero + } + + return participantsCount == 1 ? CGSize(width:collectionView.frame.size.width,height:VoipAudioOnlyParticipantCell.cell_height) : CGSize(width:collectionView.frame.size.width / 2.0 - inter_cell / 2.0,height:VoipAudioOnlyParticipantCell.cell_height) + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + +} diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift index f4de96559..b9317b81b 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift @@ -46,7 +46,7 @@ import linphonesw // TableView datasource delegate func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 2 + return 3 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -66,6 +66,14 @@ import linphonesw }, image:(UIImage(named: "voip_conference_active_speaker")?.tinted(with: VoipTheme.voipDrawableColor.get())!)!) cell.isUserInteractionEnabled = true } + + if (indexPath.row == 2) { + cell.setOption(title: VoipTexts.conference_display_mode_audio_only, onSelectAction: { + ConferenceViewModel.shared.changeLayout(layout: .AudioOnly) + ConferenceViewModel.shared.conferenceDisplayMode.value = .AudioOnly + }, image:(UIImage(named: "voip_conference_audio_only")?.tinted(with: VoipTheme.voipDrawableColor.get())!)!) + cell.isUserInteractionEnabled = true + } cell.separatorInset = .zero cell.selectionStyle = .none @@ -78,9 +86,15 @@ import linphonesw cell.isSelected = true if (indexPath.row == 0) { tableView.deselectRow(at: IndexPath(row: 1, section: 0), animated: false) + tableView.deselectRow(at: IndexPath(row: 2, section: 0), animated: false) } if (indexPath.row == 1) { tableView.deselectRow(at: IndexPath(row: 0, section: 0), animated: false) + tableView.deselectRow(at: IndexPath(row: 2, section: 0), animated: false) + } + if (indexPath.row == 2) { + tableView.deselectRow(at: IndexPath(row: 0, section: 0), animated: false) + tableView.deselectRow(at: IndexPath(row: 1, section: 0), animated: false) } } diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index 37ccb0416..dcccdf8c7 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -46,19 +46,23 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi var conferenceViewModel: ConferenceViewModel? = nil { didSet { if let model = conferenceViewModel { + model.subject.clearObservers() model.subject.readCurrentAndObserve { (subject) in self.subjectLabel.text = subject } duration.conference = model.conference.value self.remotelyRecording.isRemotelyRecorded = model.isRemotelyRecorded + model.conferenceParticipantDevices.clearObservers() model.conferenceParticipantDevices.readCurrentAndObserve { (_) in self.reloadData() } + model.isConferenceLocallyPaused.clearObservers() model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in self.pauseCallButtons.forEach { $0.isSelected = paused == true } } + model.isRecording.clearObservers() model.isRecording.readCurrentAndObserve { (selected) in self.recordCallButtons.forEach { $0.isSelected = selected == true @@ -178,7 +182,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi conferenceViewModel?.conferenceParticipantDevices.value?.forEach { $0.clearObservers() } - if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { + if (self.isHidden) { self.grid.reloadData() return } @@ -205,7 +209,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { + if (self.isHidden) { return 0 } guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else { diff --git a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift index c95241313..dfa77a918 100644 --- a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift @@ -57,7 +57,7 @@ class ConferenceLayoutPickerView: UIStackView { 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 = .Grid + ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value = .AudioOnly ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = false }) audioOnly.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_audio_only" ,tintColor: LightDarkColor(.white,.white))]) @@ -66,13 +66,12 @@ class ConferenceLayoutPickerView: UIStackView { ConferenceWaitingRoomViewModel.sharedModel.joinLayout.readCurrentAndObserve { layout in grid.isSelected = layout == .Grid activeSpeaker.isSelected = layout == .ActiveSpeaker - audioOnly.isSelected = false // Todo when doing auioonly layout == .Grid + audioOnly.isSelected = layout == .AudioOnly } 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()