diff --git a/Ring/Ring/Bridging/CallsAdapter.h b/Ring/Ring/Bridging/CallsAdapter.h index 9e77e3a97ae690401c39dae18def7138fc7d27a4..0db4575a2e7b21100de04c0ae1b36d81146b047b 100644 --- a/Ring/Ring/Bridging/CallsAdapter.h +++ b/Ring/Ring/Bridging/CallsAdapter.h @@ -49,4 +49,7 @@ - (void)setActiveParticipant:(NSString*)callId forConference:(NSString*)conferenceId; - (void)setConferenceLayout:(int)layout forConference:(NSString*)conferenceId; - (NSArray*)getConferenceInfo:(NSString*)conferenceId; +- (void)setConferenceModerator:(NSString*)participantId forConference:(NSString*)conferenceId active:(BOOL)isActive; +- (void)muteConferenceParticipant:(NSString*)participantId forConference:(NSString*)conferenceId active:(BOOL)isActive; +- (void)hangupConferenceParticipant:(NSString*)participantId forConference:(NSString*)conferenceId; @end diff --git a/Ring/Ring/Bridging/CallsAdapter.mm b/Ring/Ring/Bridging/CallsAdapter.mm index 17fbf5c040f3c64c82606d80f37836044454c88a..a6754e7b764115e270c3aae469524542b28f034f 100644 --- a/Ring/Ring/Bridging/CallsAdapter.mm +++ b/Ring/Ring/Bridging/CallsAdapter.mm @@ -221,6 +221,18 @@ static id <CallsAdapterDelegate> _delegate; setConferenceLayout(std::string([conferenceId UTF8String]), layout); } +- (void)setConferenceModerator:(NSString*)participantId forConference:(NSString*)conferenceId active:(BOOL)isActive { + setModerator(std::string([conferenceId UTF8String]), std::string([participantId UTF8String]), isActive); +} + +- (void)muteConferenceParticipant:(NSString*)participantId forConference:(NSString*)conferenceId active:(BOOL)isActive { + muteParticipant(std::string([conferenceId UTF8String]), std::string([participantId UTF8String]), isActive); +} + +- (void)hangupConferenceParticipant:(NSString*)participantId forConference:(NSString*)conferenceId { + hangupParticipant(std::string([conferenceId UTF8String]), std::string([participantId UTF8String])); +} + - (NSArray*)getConferenceInfo:(NSString*)conferenceId { auto result = getConferenceInfos(std::string([conferenceId UTF8String])); NSArray* arrayResult = [Utils vectorOfMapsToArray:result]; diff --git a/Ring/Ring/Calls/CallViewController.storyboard b/Ring/Ring/Calls/CallViewController.storyboard index cc7f026f88419fa585b0958ab068973719b3a143..71062456b020a55d994a02ac9321e70dcb1c1b7a 100644 --- a/Ring/Ring/Calls/CallViewController.storyboard +++ b/Ring/Ring/Calls/CallViewController.storyboard @@ -75,13 +75,13 @@ <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="z3c-S7-uGw"> <rect key="frame" x="0.0" y="0.0" width="375" height="110"/> <subviews> - <stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="MdN-dF-4x3"> - <rect key="frame" x="20" y="0.0" width="139.66666666666666" height="30"/> + <stackView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" distribution="equalSpacing" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="MdN-dF-4x3"> + <rect key="frame" x="20" y="0.0" width="30" height="30"/> <subviews> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GF7-hD-E63"> <rect key="frame" x="0.0" y="0.0" width="30" height="30"/> <constraints> - <constraint firstAttribute="width" constant="30" id="rKY-t8-TpZ"/> + <constraint firstAttribute="width" constant="30" id="hq4-4B-hxi"/> <constraint firstAttribute="height" constant="30" id="uyC-ZH-E3S"/> </constraints> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> @@ -99,17 +99,17 @@ </userDefinedRuntimeAttribute> </userDefinedRuntimeAttributes> </button> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cgd-Wa-clf"> - <rect key="frame" x="50.000000000000007" y="0.0" width="89.666666666666686" height="30"/> - <fontDescription key="fontDescription" type="system" pointSize="23"/> - <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <nil key="highlightedColor"/> - </label> </subviews> <constraints> <constraint firstAttribute="height" constant="30" id="7GR-7Z-Jln"/> </constraints> </stackView> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cgd-Wa-clf"> + <rect key="frame" x="70" y="15" width="0.0" height="0.0"/> + <fontDescription key="fontDescription" type="system" pointSize="23"/> + <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <nil key="highlightedColor"/> + </label> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fac-lR-4on"> <rect key="frame" x="19.999999999999996" y="50" width="52.666666666666657" height="24"/> <fontDescription key="fontDescription" type="system" weight="light" pointSize="20"/> @@ -121,7 +121,9 @@ <constraints> <constraint firstItem="fac-lR-4on" firstAttribute="top" secondItem="MdN-dF-4x3" secondAttribute="bottom" constant="20" id="1ja-6Q-FJk"/> <constraint firstItem="MdN-dF-4x3" firstAttribute="top" secondItem="z3c-S7-uGw" secondAttribute="top" id="TVu-Mv-p0D"/> + <constraint firstItem="cgd-Wa-clf" firstAttribute="leading" secondItem="MdN-dF-4x3" secondAttribute="trailing" constant="20" id="VIE-lK-GeA"/> <constraint firstItem="fac-lR-4on" firstAttribute="leading" secondItem="MdN-dF-4x3" secondAttribute="leading" id="cJ6-kf-a3T"/> + <constraint firstItem="cgd-Wa-clf" firstAttribute="centerY" secondItem="MdN-dF-4x3" secondAttribute="centerY" id="unp-V8-eN9"/> </constraints> </view> </subviews> @@ -198,6 +200,7 @@ <constraint firstItem="K0W-KI-Ul4" firstAttribute="centerY" secondItem="DMu-Or-dd7" secondAttribute="centerY" id="cTk-Hv-nxM"/> <constraint firstItem="Zmp-OX-Cez" firstAttribute="centerX" secondItem="DMu-Or-dd7" secondAttribute="centerX" id="f1S-rO-Hld"/> <constraint firstItem="K0W-KI-Ul4" firstAttribute="height" secondItem="DMu-Or-dd7" secondAttribute="height" id="grY-ie-rVw"/> + <constraint firstItem="CfE-DF-buX" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="cgd-Wa-clf" secondAttribute="trailing" priority="750" constant="7" id="hmN-pb-JlG"/> <constraint firstItem="K0W-KI-Ul4" firstAttribute="centerX" secondItem="DMu-Or-dd7" secondAttribute="centerX" id="oCc-Yp-7Ay"/> <constraint firstItem="ZK1-Be-lcD" firstAttribute="width" secondItem="ZVy-nB-bKJ" secondAttribute="width" id="uTd-rs-MJH"/> <constraint firstAttribute="trailing" secondItem="ZK1-Be-lcD" secondAttribute="trailing" id="ugJ-SF-Enn"/> @@ -206,10 +209,10 @@ </constraints> </view> <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" placeholderIntrinsicWidth="375" placeholderIntrinsicHeight="128" alwaysBounceHorizontal="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bij-Xb-EKH"> - <rect key="frame" x="0.0" y="856" width="375" height="300"/> + <rect key="frame" x="0.0" y="806" width="375" height="360"/> <subviews> <stackView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" distribution="equalSpacing" alignment="top" spacing="40" translatesAutoresizingMaskIntoConstraints="NO" id="7G0-Fp-Xc3"> - <rect key="frame" x="10" y="10" width="355" height="260"/> + <rect key="frame" x="10" y="10" width="355" height="320"/> </stackView> </subviews> <constraints> @@ -219,7 +222,7 @@ <constraint firstItem="7G0-Fp-Xc3" firstAttribute="centerX" secondItem="bij-Xb-EKH" secondAttribute="centerX" placeholder="YES" id="JZj-B5-YW6"/> <constraint firstItem="7G0-Fp-Xc3" firstAttribute="leading" secondItem="bij-Xb-EKH" secondAttribute="leading" constant="10" id="QC5-Mh-VcC"/> <constraint firstAttribute="bottom" secondItem="7G0-Fp-Xc3" secondAttribute="bottom" constant="30" id="lUV-jq-9bZ"/> - <constraint firstAttribute="height" constant="300" id="q8G-dB-syM"/> + <constraint firstAttribute="height" constant="360" id="q8G-dB-syM"/> <constraint firstItem="7G0-Fp-Xc3" firstAttribute="height" secondItem="bij-Xb-EKH" secondAttribute="height" constant="-40" id="xAL-yn-Zen"/> </constraints> </scrollView> @@ -334,7 +337,7 @@ <constraint firstItem="ZVy-nB-bKJ" firstAttribute="centerY" secondItem="QpJ-Sx-9dG" secondAttribute="centerY" id="bAN-gX-nPE"/> <constraint firstAttribute="bottom" secondItem="LK6-u0-eLU" secondAttribute="bottom" id="dXj-zI-fQb"/> <constraint firstItem="ZVy-nB-bKJ" firstAttribute="centerX" secondItem="lZI-X0-bkP" secondAttribute="centerX" id="ff0-Nw-f2Y"/> - <constraint firstItem="bij-Xb-EKH" firstAttribute="top" secondItem="ZK1-Be-lcD" secondAttribute="bottom" id="pgr-29-Lef"/> + <constraint firstItem="bij-Xb-EKH" firstAttribute="top" secondItem="ZK1-Be-lcD" secondAttribute="bottom" constant="-50" id="pgr-29-Lef"/> <constraint firstItem="5E0-lB-SkS" firstAttribute="width" secondItem="QpJ-Sx-9dG" secondAttribute="width" id="rOQ-In-yON"/> <constraint firstItem="ZVy-nB-bKJ" firstAttribute="width" secondItem="QpJ-Sx-9dG" secondAttribute="width" id="sCh-Gw-iu0"/> <constraint firstItem="MdN-dF-4x3" firstAttribute="leading" secondItem="lZI-X0-bkP" secondAttribute="leading" constant="20" id="tsz-q2-iDb"/> diff --git a/Ring/Ring/Calls/CallViewController.swift b/Ring/Ring/Calls/CallViewController.swift index e6112884b6a4df599239fe6ef16f3945169612fa..13d8e01e2004938b043ad8eeaa7682b67d2e84ed 100644 --- a/Ring/Ring/Calls/CallViewController.swift +++ b/Ring/Ring/Calls/CallViewController.swift @@ -156,6 +156,27 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased, Con self.updateconferenceLayoutSize() let participants = self.viewModel.getConferenceParticipants() self.conferenceLayout.setParticipants(participants: participants) + guard let unwrapParticipants = participants, self.viewModel.isCurrentModerator(), !self.viewModel.isHostCall else { return } + self.conferenceCalls.arrangedSubviews.forEach({ (view) in + view.removeFromSuperview() + }) + for participant in unwrapParticipants { + let callView = + ConferenceParticipantView(frame: + CGRect(x: 0, y: 0, + width: inConfViewWidth, height: inConfViewHeight)) + let injectionBag = self.viewModel.injectionBag + let isLocal = self.viewModel.isLocalCall(participantId: participant.uri ?? "") + let pendingCallViewModel = + ConferenceParticipantViewModel(with: nil, + injectionBag: injectionBag, + isLocal: isLocal, + participantId: participant.uri ?? "", + participantUserName: participant.displayName) + callView.viewModel = pendingCallViewModel + callView.delegate = self + self.conferenceCalls.addArrangedSubview(callView) + } }) .disposed(by: self.disposeBag) } @@ -447,17 +468,20 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased, Con .asObservable() .observeOn(MainScheduler.instance) .subscribe(onNext: { [weak self] enteredConference in - guard let call = self?.viewModel.call else { return } + guard let call = self?.viewModel.call, + let self = self else { return } if call.state != .current { return } - self?.updateconferenceLayoutSize() - self?.buttonsContainer.updateView() - self?.infoContainer.isHidden = enteredConference ? true : false - self?.resizeCapturedVideo(withInfoContainer: false) + self.updateconferenceLayoutSize() + self.buttonsContainer.updateView() + self.infoContainer.isHidden = enteredConference ? true : false + self.conferenceCallsTop.constant = enteredConference ? 0 : -50 + self.resizeCapturedVideo(withInfoContainer: false) + // for moderator participants will be added in layoutUpdated + if self.viewModel.isCurrentModerator() { return } // if entered conference add first participant to conference list if enteredConference { - self?.removeConferenceParticipantMenu() - guard let injectionBag = self?.viewModel.injectionBag - else { return } + self.removeConferenceParticipantMenu() + let injectionBag = self.viewModel.injectionBag // add self as a master call let mainCallView = ConferenceParticipantView(frame: CGRect(x: 0, @@ -466,24 +490,31 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased, Con height: inConfViewHeight)) let mainCallViewModel = ConferenceParticipantViewModel(with: nil, - injectionBag: injectionBag) + injectionBag: injectionBag, + isLocal: true, + participantId: "", + participantUserName: "") mainCallView.viewModel = mainCallViewModel mainCallView.delegate = self - self?.conferenceCalls.insertArrangedSubview(mainCallView, at: 0) + self.conferenceCalls.insertArrangedSubview(mainCallView, at: 0) let callView = ConferenceParticipantView(frame: CGRect(x: 0, y: 0, width: inConfViewWidth, height: inConfViewHeight)) + let name = call.displayName.isEmpty ? call.registeredName.isEmpty ? call.participantUri.filterOutHost() : call.registeredName : call.displayName let pendingCallViewModel = - ConferenceParticipantViewModel(with: call, - injectionBag: injectionBag) + ConferenceParticipantViewModel(with: call.callId, + injectionBag: injectionBag, + isLocal: false, + participantId: call.paricipantHash(), + participantUserName: name) callView.viewModel = pendingCallViewModel callView.delegate = self - self?.conferenceCalls.insertArrangedSubview(callView, at: 1) + self.conferenceCalls.insertArrangedSubview(callView, at: 1) } else { - self?.removeConferenceParticipantMenu() - self?.conferenceCalls.arrangedSubviews.forEach({ (view) in + self.removeConferenceParticipantMenu() + self.conferenceCalls.arrangedSubviews.forEach({ (view) in view.removeFromSuperview() }) } @@ -493,17 +524,24 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased, Con self.viewModel.callForConference .observeOn(MainScheduler.instance) .subscribe(onNext: { [weak self] call in + guard let self = self else { return } + // for moderator participants will be added in layoutUpdated + if self.viewModel.isCurrentModerator() { return } let callView = ConferenceParticipantView(frame: CGRect(x: 0, y: 0, width: inConfViewWidth, height: inConfViewHeight)) - guard let injectionBag = self?.viewModel.injectionBag else { return } + let injectionBag = self.viewModel.injectionBag + let name = call.displayName.isEmpty ? call.registeredName.isEmpty ? call.participantUri.filterOutHost() : call.registeredName : call.displayName let pendingCallViewModel = - ConferenceParticipantViewModel(with: call, - injectionBag: injectionBag) + ConferenceParticipantViewModel(with: call.callId, + injectionBag: injectionBag, + isLocal: false, + participantId: call.paricipantHash(), + participantUserName: name) callView.viewModel = pendingCallViewModel callView.delegate = self - self?.conferenceCalls.addArrangedSubview(callView) + self.conferenceCalls.addArrangedSubview(callView) }) .disposed(by: self.disposeBag) @@ -729,7 +767,8 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased, Con UIView.animate(withDuration: 0.2, animations: { [weak self] in self?.infoContainerTopConstraint.constant = -10 - self?.conferenceCallsTop.constant = 0 + let isConference: Bool = self?.viewModel.conferenceMode.value ?? true + self?.conferenceCallsTop.constant = isConference ? 0 : -50 if UIDevice.current.hasNotch && (self?.orientation == .landscapeRight || self?.orientation == .landscapeLeft) { self?.buttonsContainerBottomConstraint.constant = 1 } else if UIDevice.current.userInterfaceIdiom == .pad { @@ -786,26 +825,61 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased, Con } extension CallViewController: ConferenceParticipantViewDelegate { - func addConferenceParticipantMenu(origin: CGPoint, displayName: String, callId: String?, hangup: @escaping (() -> Void)) { + func addConferenceParticipantMenu(origin: CGPoint, displayName: String, participantId: String, callId: String?, hangup: @escaping (() -> Void)) { // remove menu if it is already present if self.conferenceParticipantMenu?.frame.origin == origin { self.removeConferenceParticipantMenu() return } let menuView = ConferenceActionMenu(frame: CGRect(origin: origin, size: CGSize(width: self.view.frame.size.width, height: self.view.frame.size.height))) - menuView.configureWith(mode: self.viewModel.getItemsForConferenceMenu(participantCallId: callId), displayName: displayName) + var muteEnabled = false + var muteText = "" + var moderatorText = "" + var isModerator = false + var isAudioMuted = false + var pending = true + + if let participant = self.viewModel.getConferencePartisipant(participantId: participantId) { + muteEnabled = !participant.isAudioLocalyMuted + muteText = participant.isAudioMuted ? L10n.Calls.unmuteAudio : L10n.Calls.muteAudio + moderatorText = participant.isModerator ? L10n.Calls.removeModerator : L10n.Calls.setModerator + isModerator = participant.isModerator + isAudioMuted = participant.isAudioMuted + pending = false + } + + menuView.configureWith(items: self.viewModel.getItemsForConferenceMenu(participantId: participantId, callId: callId ?? ""), + displayName: displayName, + muteText: muteText, + moderatorText: moderatorText, + muteEnabled: muteEnabled) menuView.addHangUpAction { [weak self] in - hangup() + if pending { + hangup() + } else { + self?.viewModel.hangupParticipant(participantId: participantId) + } self?.removeConferenceParticipantMenu() } menuView.addMaximizeAction { [weak self] in self?.removeConferenceParticipantMenu() - self?.viewModel.setActiveParticipant(callId: callId, maximize: true) + self?.viewModel.setActiveParticipant(jamiId: participantId, maximize: true) } menuView.addMinimizeAction { [weak self] in self?.removeConferenceParticipantMenu() - self?.viewModel.setActiveParticipant(callId: callId, maximize: false) + self?.viewModel.setActiveParticipant(jamiId: participantId, maximize: false) } + + menuView.addSetModeratorAction { [weak self] in + self?.removeConferenceParticipantMenu() + self?.viewModel.setModeratorParticipant(participantId: participantId, active: !isModerator) + } + + menuView.addMuteAction { [weak self] in + self?.removeConferenceParticipantMenu() + self?.viewModel.muteParticipant(participantId: participantId, active: !isAudioMuted) + } + let point = conferenceCallsScrolView.convert(menuView.frame.origin, to: self.view) let offset = self.view.frame.size.width - point.x - menuView.frame.size.width if offset < 0 { diff --git a/Ring/Ring/Calls/CallViewModel.swift b/Ring/Ring/Calls/CallViewModel.swift index d5ef37d0e2e120def164733d1c8af0fde05d12ff..151a3faa02ba7186af304614ceef8387d4dec791 100644 --- a/Ring/Ring/Calls/CallViewModel.swift +++ b/Ring/Ring/Calls/CallViewModel.swift @@ -48,6 +48,7 @@ class CallViewModel: Stateable, ViewModel { var isHeadsetConnected = false var isAudioOnly = false + var isHostCall = false private lazy var currentCallVariable: BehaviorRelay<CallModel> = { BehaviorRelay<CallModel>(value: self.call ?? CallModel()) @@ -86,23 +87,36 @@ class CallViewModel: Stateable, ViewModel { self.callService.currentConferenceEvent .asObservable() .filter({ [weak self] conference-> Bool in - return conference.calls.contains(self?.call?.callId ?? "") || - conference.conferenceID == self?.rendererId + guard let self = self else { return false } + return conference.calls.contains(self.call?.callId ?? "") || + conference.conferenceID == self.rendererId }) .subscribe(onNext: { [weak self] conf in + guard let self = self else { return } if conf.conferenceID.isEmpty { return } if conf.state == ConferenceState.infoUpdated.rawValue { - self?.layoutUpdated.accept(true) + self.layoutUpdated.accept(true) + guard let account = self.accountService.currentAccount, !self.isHostCall else { + return + } + let isModerator = self.callService.isModerator(participantId: account.jamiId, inConference: conf.conferenceID) + if isModerator != self.containerViewModel?.isConference { +// guard let updatedCall = self.callService.call(callID: call.callId) else { return } +// self.call = updatedCall + self.containerViewModel?.isConference = isModerator + self.conferenceMode.accept(isModerator) + } return } - guard let updatedCall = self?.callService.call(callID: call.callId) else { return } - self?.call = updatedCall + guard let updatedCall = self.callService.call(callID: call.callId) else { return } + self.call = updatedCall let conferenceCreated = conf.state == ConferenceState.conferenceCreated.rawValue - self?.rendererId = conferenceCreated ? conf.conferenceID : self!.call!.callId - self?.containerViewModel?.isConference = conferenceCreated - self?.conferenceMode.accept(conferenceCreated) + self.rendererId = conferenceCreated ? conf.conferenceID : self.call!.callId + self.isHostCall = conferenceCreated + self.containerViewModel?.isConference = conferenceCreated + self.conferenceMode.accept(conferenceCreated) }) .disposed(by: self.disposeBag) self.rendererId = call.callId @@ -627,15 +641,26 @@ extension CallViewModel { } // MARK: conference layout extension CallViewModel { - func setActiveParticipant(callId: String?, maximize: Bool) { - guard let jamiId = self.accountService.currentAccount?.jamiId else { return } - self.callService.setActiveParticipant(callId: callId, conferenceId: self.rendererId, maximixe: maximize, jamiId: jamiId) + func setActiveParticipant(jamiId: String, maximize: Bool) { + self.callService.setActiveParticipant(conferenceId: self.rendererId, maximixe: maximize, jamiId: jamiId.filterOutHost()) } func getConferenceVideoSize() -> CGSize { return self.videoService.getConferenceVideoSize(confId: self.rendererId) } + func muteParticipant(participantId: String, active: Bool) { + self.callService.muteParticipant(confId: self.rendererId, participantId: participantId.filterOutHost(), active: active) + } + + func setModeratorParticipant(participantId: String, active: Bool) { + self.callService.setModeratorParticipant(confId: self.rendererId, participantId: participantId.filterOutHost(), active: active) + } + + func hangupParticipant(participantId: String) { + self.callService.hangupParticipant(confId: self.rendererId, participantId: participantId.filterOutHost()) + } + func getConferenceParticipants() -> [ConferenceParticipant]? { guard let account = self.accountService.currentAccount, let participants = self.callService.getConferenceParticipants(for: self.rendererId), @@ -645,7 +670,7 @@ extension CallViewModel { // master call if uri.isEmpty { //check if master call is local or remote - if !self.conferenceMode.value { + if !self.isHostCall { participant.displayName = call.getDisplayName() } else { participant.displayName = L10n.Account.me @@ -673,15 +698,47 @@ extension CallViewModel { return participants } - func getItemsForConferenceMenu(participantCallId: String?) -> MenuMode { + func getConferencePartisipant(participantId: String) -> ConferenceParticipant? { + guard let participants = self.getConferenceParticipants() else { return nil } + return participants.filter { participant in + return participant.uri?.filterOutHost() == participantId.filterOutHost() + }.first + } + + func isLocalCall(participantId: String) -> Bool { + guard let account = self.accountService.currentAccount else { return false } + return account.jamiId == participantId.filterOutHost() + } + + func isHostCall(participantId: String) -> Bool { + guard let account = self.accountService.currentAccount else { return false } + if self.isHostCall { + return account.jamiId == participantId.filterOutHost() + } + return call?.participantUri.filterOutHost() == participantId.filterOutHost() + } + + func isCurrentModerator() -> Bool { + guard let account = self.accountService.currentAccount else { return false } + return self.callService.isModerator(participantId: account.jamiId, inConference: self.rendererId) + } + + func getItemsForConferenceMenu(participantId: String, callId: String) -> [MenuItem] { let conference = self.callService.call(callID: self.rendererId) - // menu for master call - guard let callId = participantCallId else { - let active = self.callService.isParticipant(participantURI: "", activeIn: self.rendererId) - return menuItemsManager.getMenuItemsForMasterCall(conference: conference, active: active) + let active = self.callService.isParticipant(participantURI: participantId, activeIn: self.rendererId) + // menu for local call + if self.isLocalCall(participantId: participantId) || participantId.isEmpty { + return menuItemsManager.getMenuItemsForLocalCall(conference: conference, active: active) + } + let isModerator = self.isCurrentModerator() + var role = RoleInCall.regular + let callIsHost = self.isHostCall(participantId: participantId) + if self.isHostCall { + role = RoleInCall.host + } else if isModerator { + role = RoleInCall.moderator } - let call = self.callService.call(callID: callId) - let active = self.callService.isParticipant(participantURI: call?.participantUri, activeIn: self.rendererId) - return menuItemsManager.getMenuItemsFor(call: call, conference: conference, active: active) + let participantCall = isModerator ? call : self.callService.call(callID: callId) + return menuItemsManager.getMenuItemsFor(call: participantCall, isHost: callIsHost, conference: conference, active: active, role: role) } } diff --git a/Ring/Ring/Calls/Conference/ConferenceMenuItemsManager.swift b/Ring/Ring/Calls/Conference/ConferenceMenuItemsManager.swift index 9aabd29eaa4e54e785469092fc80a1e24bebe5e4..3d6dd63282b629463162739b41509a652df8fcca 100644 --- a/Ring/Ring/Calls/Conference/ConferenceMenuItemsManager.swift +++ b/Ring/Ring/Calls/Conference/ConferenceMenuItemsManager.swift @@ -17,41 +17,110 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ - +enum RoleInCall { + case moderator + case host + case regular +} class ConferenceMenuItemsManager { - func getMenuItemsForMasterCall(conference: CallModel?, active: Bool?) -> MenuMode { + func getMenuItemsForLocalCall(conference: CallModel?, active: Bool?) -> [MenuItem] { + var menu = [MenuItem]() + menu.append(.name) guard let conference = conference else { - return MenuMode.onlyName + return menu } guard let active = active else { - return MenuMode.onlyName + return menu } switch conference.layout { case .grid: - return MenuMode.withoutHangUPAndMinimize + menu.append(.maximize) case .oneWithSmal: - return active ? MenuMode.withoutHangUp : MenuMode.withoutHangUPAndMinimize + menu.append(.maximize) + if active { + menu.append(.minimize) + } case .one: - return active ? MenuMode.withoutHangUPAndMaximize : MenuMode.withoutHangUPAndMinimize + if active { + menu.append(.minimize) + } else { + menu.append(.maximize) + } } + menu.append(.muteAudio) + return menu } - func getMenuItemsFor(call: CallModel?, conference: CallModel?, active: Bool?) -> MenuMode { + func getMenuItemsFor(call: CallModel?, isHost: Bool, conference: CallModel?, active: Bool?, role: RoleInCall) -> [MenuItem] { + var menu = [MenuItem]() + menu.append(.name) guard let conference = conference, let call = call else { - return MenuMode.onlyName + return menu } if call.state != CallState.current { - return MenuMode.withoutMaximizeAndMinimize + menu.append(.hangup) + return menu + } + guard let active = active else { + return menu } - guard let active = active else { return MenuMode.onlyName } switch conference.layout { case .grid: - return MenuMode.withoutMinimize + menu.append(.maximize) + switch role { + case .host: + menu.append(.muteAudio) + menu.append(.setModerator) + menu.append(.hangup) + case .moderator: + menu.append(.muteAudio) + if !isHost { + menu.append(.hangup) + } + case .regular: + break + } case .oneWithSmal: - return active ? MenuMode.all : MenuMode.withoutMinimize + if active { + menu.append(.maximize) + menu.append(.minimize) + } else { + menu.append(.maximize) + } + switch role { + case .host: + menu.append(.muteAudio) + menu.append(.setModerator) + menu.append(.hangup) + case .moderator: + menu.append(.muteAudio) + if !isHost { + menu.append(.hangup) + } + case .regular: + break + } case .one: - return active ? MenuMode.withoutMaximize : MenuMode.withoutMinimize + if active { + menu.append(.minimize) + } else { + menu.append(.maximize) + } + switch role { + case .host: + menu.append(.muteAudio) + menu.append(.setModerator) + menu.append(.hangup) + case .moderator: + menu.append(.muteAudio) + if !isHost { + menu.append(.hangup) + } + case .regular: + break + } } + return menu } } diff --git a/Ring/Ring/Calls/views/ConferenceActionsMenu.swift b/Ring/Ring/Calls/views/ConferenceActionsMenu.swift index 80cb87e9b34c7e58daf7b161d656aadae59448c5..8839fff803be7dcc9dd4afd1544e295102dacc20 100644 --- a/Ring/Ring/Calls/views/ConferenceActionsMenu.swift +++ b/Ring/Ring/Calls/views/ConferenceActionsMenu.swift @@ -21,54 +21,69 @@ import UIKit import RxSwift -enum MenuMode { - case withoutHangUp // for master call - case withoutMaximize - case withoutMinimize - case withoutMaximizeAndMinimize - case withoutHangUPAndMinimize - case withoutHangUPAndMaximize - case onlyName - case all +enum MenuItem { + case name + case hangup + case minimize + case maximize + case setModerator + case muteAudio } class ConferenceActionMenu: UIView { private let marginY: CGFloat = 20 private let marginX: CGFloat = 20 - private let maxWidth: CGFloat = 120 + private let maxWidth: CGFloat = 200 private let menuItemWidth: CGFloat = 80 private let menuItemHight: CGFloat = 30 private let textSize: CGFloat = 20 private var hangUpButton: UIButton? private var maximizeButton: UIButton? private var minimizeButton: UIButton? + private var setModeratorButton: UIButton? + private var muteAudioButton: UIButton? private let disposeBag = DisposeBag() + private var muteLabelText: String = "" + private var moderatorLabelText: String = "" + private var muteButtonEnabled: Bool = false + private var hasSetMute: Bool = false - func configureWith(mode: MenuMode, displayName: String) { + func configureWith(items: [MenuItem], displayName: String, muteText: String, moderatorText: String, muteEnabled: Bool) { self.addDisplayName(displayName: displayName) - switch mode { - case .withoutHangUp: - self.configureWithoutHangUP() - case .withoutMaximize: - self.configureWithoutMaximize() - case .withoutMinimize: - self.configureWithoutMinimize() - case .withoutMaximizeAndMinimize: - self.configureWithoutMaximizeAndMinimize() - case .withoutHangUPAndMinimize: - self.configureWithoutHangUPAndMinimize() - case .withoutHangUPAndMaximize: - self.configureWithoutHangUPAndMaximize() - case .all: - self.configureWithAllItems() - case .onlyName: - break + muteLabelText = muteText + moderatorLabelText = moderatorText + muteButtonEnabled = muteEnabled + let itemsWithoutName = items.filter { item in + item != .name + } + if !itemsWithoutName.isEmpty { + for index in 1...itemsWithoutName.count { + let position: CGFloat = self.marginY * CGFloat((index + 1)) + menuItemHight * CGFloat(index) + self.addItem(item: itemsWithoutName[index - 1], positionY: position) + } } self.updateWidth() self.updateHeight() self.addBackground() } + func addItem(item: MenuItem, positionY: CGFloat) { + switch item { + case .minimize: + self.addMinimizeButton(positionY: positionY) + case .maximize: + self.addMaximizeButton(positionY: positionY) + case .setModerator: + self.addSetModeratorButton(positionY: positionY) + case .muteAudio: + self.addMuteAudioButton(positionY: positionY) + case .hangup: + self.addHangUpButton(positionY: positionY) + case .name: + break + } + } + func addHangUpAction(hangup: @escaping (() -> Void)) { guard let button = hangUpButton else { return } button.rx.tap @@ -90,6 +105,20 @@ class ConferenceActionMenu: UIView { .disposed(by: self.disposeBag) } + func addSetModeratorAction(setModerator: @escaping (() -> Void)) { + guard let button = setModeratorButton else { return } + button.rx.tap + .subscribe(onNext: { setModerator() }) + .disposed(by: self.disposeBag) + } + + func addMuteAction(mute: @escaping (() -> Void)) { + guard let button = muteAudioButton else { return } + button.rx.tap + .subscribe(onNext: { mute() }) + .disposed(by: self.disposeBag) + } + private func addDisplayName(displayName: String) { let labelName = UILabel(frame: CGRect(x: marginX, y: marginY, width: menuItemWidth, height: menuItemHight)) labelName.font = labelName.font.withSize(self.textSize) @@ -142,6 +171,34 @@ class ConferenceActionMenu: UIView { self.addSubview(self.minimizeButton!) } + private func addSetModeratorButton(positionY: CGFloat) { + let setModeratotLabel = UILabel(frame: CGRect(x: marginX, y: positionY, width: menuItemWidth, height: menuItemHight)) + setModeratotLabel.font = setModeratotLabel.font.withSize(self.textSize) + setModeratotLabel.text = moderatorLabelText + setModeratotLabel.sizeToFit() + setModeratotLabel.textAlignment = .center + self.setModeratorButton = UIButton(frame: setModeratotLabel.frame) + self.addSubview(setModeratotLabel) + self.addSubview(self.setModeratorButton!) + } + + private func addMuteAudioButton(positionY: CGFloat) { + let muteAudioLabel = UILabel(frame: CGRect(x: marginX, y: positionY, width: menuItemWidth, height: menuItemHight)) + muteAudioLabel.font = muteAudioLabel.font.withSize(self.textSize) + muteAudioLabel.text = muteLabelText + muteAudioLabel.sizeToFit() + muteAudioLabel.textAlignment = .center + if #available(iOS 13.0, *) { + muteAudioLabel.textColor = muteButtonEnabled ? UIColor.label : UIColor.quaternaryLabel + } else { + muteAudioLabel.textColor = muteButtonEnabled ? UIColor.white : UIColor.lightText + } + self.addSubview(muteAudioLabel) + if !muteButtonEnabled { return } + self.muteAudioButton = UIButton(frame: muteAudioLabel.frame) + self.addSubview(self.muteAudioButton!) + } + private func updateHeight() { var numberOfLabels: CGFloat = 0 self.subviews.forEach { (childView) in @@ -167,49 +224,4 @@ class ConferenceActionMenu: UIView { childView.frame.size.width = finalWidth } } - - private func configureWithoutHangUP() { - let firstY: CGFloat = CGFloat(self.marginY * 2 + menuItemHight) - let secondY: CGFloat = CGFloat(self.marginY * 3 + menuItemHight * 2) - self.addMaximizeButton(positionY: firstY) - self.addMinimizeButton(positionY: secondY) - } - - private func configureWithoutMaximize() { - let firstY: CGFloat = CGFloat(self.marginY * 2 + menuItemHight) - let secondY: CGFloat = CGFloat(self.marginY * 3 + menuItemHight * 2) - self.addHangUpButton(positionY: firstY) - self.addMinimizeButton(positionY: secondY) - } - - private func configureWithoutMinimize() { - let firstY: CGFloat = CGFloat(self.marginY * 2 + menuItemHight) - let secondY: CGFloat = CGFloat(self.marginY * 3 + menuItemHight * 2) - self.addHangUpButton(positionY: firstY) - self.addMaximizeButton(positionY: secondY) - } - - private func configureWithoutMaximizeAndMinimize() { - let firstY: CGFloat = CGFloat(self.marginY * 2 + menuItemHight) - self.addHangUpButton(positionY: firstY) - } - - private func configureWithoutHangUPAndMinimize() { - let firstY: CGFloat = CGFloat(self.marginY * 2 + menuItemHight) - self.addMaximizeButton(positionY: firstY) - } - - private func configureWithoutHangUPAndMaximize() { - let firstY: CGFloat = CGFloat(self.marginY * 2 + menuItemHight) - self.addMinimizeButton(positionY: firstY) - } - - private func configureWithAllItems() { - let firstY: CGFloat = CGFloat(self.marginY * 2 + menuItemHight) - let secondY: CGFloat = CGFloat(self.marginY * 3 + menuItemHight * 2) - let thirdtY: CGFloat = CGFloat(self.marginY * 4 + menuItemHight * 3) - self.addHangUpButton(positionY: firstY) - self.addMaximizeButton(positionY: secondY) - self.addMinimizeButton(positionY: thirdtY) - } } diff --git a/Ring/Ring/Calls/views/ConferenceParticipantView.swift b/Ring/Ring/Calls/views/ConferenceParticipantView.swift index c51b8e4ff56ca936b1f5934426ff099387a46dd0..c747caad1de622221f00eb1306a021cda3c7c259 100644 --- a/Ring/Ring/Calls/views/ConferenceParticipantView.swift +++ b/Ring/Ring/Calls/views/ConferenceParticipantView.swift @@ -23,7 +23,7 @@ import Reusable import RxSwift protocol ConferenceParticipantViewDelegate: class { - func addConferenceParticipantMenu(origin: CGPoint, displayName: String, callId: String?, hangup: @escaping (() -> Void)) + func addConferenceParticipantMenu(origin: CGPoint, displayName: String, participantId: String, callId: String?, hangup: @escaping (() -> Void)) func removeConferenceParticipantMenu() } @@ -63,12 +63,14 @@ class ConferenceParticipantView: UIView { @objc func showMenu() { guard let name = self.viewModel?.getName() else { return } + let participantId: String = self.viewModel?.getParticipantId() ?? "" let callId = self.viewModel?.getCallId() let menu = UIView(frame: CGRect(x: 50, y: 50, width: 50, height: 50)) let frame = self.convert(menu.frame, to: self.superview) self.delegate? .addConferenceParticipantMenu(origin: frame.origin, displayName: name, + participantId: participantId, callId: callId, hangup: { [weak self] in diff --git a/Ring/Ring/Calls/views/ConferenceParticipantViewModel.swift b/Ring/Ring/Calls/views/ConferenceParticipantViewModel.swift index c2a8a785e92a44ee4b7e30cacf8f9e257617d5d8..3b4abb5f382c53bdc4f221a7a385b70402c30d5a 100644 --- a/Ring/Ring/Calls/views/ConferenceParticipantViewModel.swift +++ b/Ring/Ring/Calls/views/ConferenceParticipantViewModel.swift @@ -22,18 +22,20 @@ import RxSwift import RxCocoa class ConferenceParticipantViewModel { - private let call: CallModel? // for conference master call is nil + private let callId: String? // for conference master or for moderator call is nil + private let participantId: String + private let participantUserName: String private let callsSercive: CallsService private let profileService: ProfilesService private let accountService: AccountsService - private let isMasterCall: Bool + private let isLocal: Bool private let disposeBag = DisposeBag() private lazy var contactImageData: Observable<String?> = { guard let account = self.accountService.currentAccount else { return Observable.just(nil) } - guard let call = call else { + if isLocal { return self.profileService.getAccountProfile(accountId: account.id).map { profile in if let alias = profile.alias, !alias.isEmpty { self.displayName.accept(alias) @@ -43,14 +45,13 @@ class ConferenceParticipantViewModel { } let type = account.type == AccountType.sip ? URIType.sip : URIType.ring guard let uriString = JamiURI.init(schema: type, - infoHach: call.participantUri, + infoHach: participantId, account: account).uriString else { return Observable.just(nil) } return profileService.getProfile(uri: uriString, createIfNotexists: false, accountId: account.id) .map { profile in if let alias = profile.alias, !alias.isEmpty { - self.call?.displayName = alias self.displayName.accept(alias) } return profile.photo @@ -59,8 +60,8 @@ class ConferenceParticipantViewModel { private lazy var displayName: BehaviorRelay<String> = { var initialName = "" - if let call = call { - initialName = call.getDisplayName() + if !isLocal { + initialName = participantUserName } else if let account = self.accountService.currentAccount { initialName = account.registeredName } @@ -68,9 +69,8 @@ class ConferenceParticipantViewModel { }() lazy var removeView: Observable<Bool>? = { - guard let call = call else { return nil } - return self.callsSercive.currentCall(callId: call.callId ) - .startWith(call) + guard let callId = callId else { return nil } + return self.callsSercive.currentCall(callId: callId) .map({ callModel in return (callModel.state == .over || callModel.state == .failure || @@ -86,28 +86,34 @@ class ConferenceParticipantViewModel { } }() - init(with call: CallModel?, injectionBag: InjectionBag) { - self.call = call + init(with callId: String?, injectionBag: InjectionBag, isLocal: Bool, participantId: String, participantUserName: String) { + self.callId = callId self.callsSercive = injectionBag.callService self.profileService = injectionBag.profileService self.accountService = injectionBag.accountService - self.isMasterCall = call == nil + self.isLocal = isLocal + self.participantId = participantId + self.participantUserName = participantUserName } func getName() -> String { - guard call != nil else { + if isLocal { return L10n.Account.me } return self.displayName.value } func getCallId() -> String? { - return self.call?.callId + return self.callId + } + + func getParticipantId() -> String { + return participantId } func cancelCall() { - guard let call = self.call else { return } - self.callsSercive.hangUp(callId: call.callId) + guard let callId = self.callId else { return } + self.callsSercive.hangUp(callId: callId) .subscribe(onCompleted: { }) .disposed(by: disposeBag) } diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift index 17e9c272b6b564347cd6f63c01f17823c5badb44..e38caff060104aa48948109fa4fccba6ee8efdf5 100644 --- a/Ring/Ring/Constants/Generated/Strings.swift +++ b/Ring/Ring/Constants/Generated/Strings.swift @@ -297,12 +297,20 @@ internal enum L10n { internal static let maximize = L10n.tr("Localizable", "calls.maximize") /// minimize internal static let minimize = L10n.tr("Localizable", "calls.minimize") + /// mute audio + internal static let muteAudio = L10n.tr("Localizable", "calls.muteAudio") + /// unset moderator + internal static let removeModerator = L10n.tr("Localizable", "calls.removeModerator") /// Ringing… internal static let ringing = L10n.tr("Localizable", "calls.ringing") /// Searching… internal static let searching = L10n.tr("Localizable", "calls.searching") + /// set moderator + internal static let setModerator = L10n.tr("Localizable", "calls.setModerator") /// Unknown internal static let unknown = L10n.tr("Localizable", "calls.unknown") + /// unmute audio + internal static let unmuteAudio = L10n.tr("Localizable", "calls.unmuteAudio") } internal enum ContactPage { diff --git a/Ring/Ring/Features/Me/Me/MeViewModel.swift b/Ring/Ring/Features/Me/Me/MeViewModel.swift index c2c9f5ac5c26aa359e0f2cb250a764125d514aea..7b7e19b56b8a939c9536c16b29e7786b045f5aaf 100644 --- a/Ring/Ring/Features/Me/Me/MeViewModel.swift +++ b/Ring/Ring/Features/Me/Me/MeViewModel.swift @@ -128,7 +128,7 @@ class MeViewModel: ViewModel, Stateable { return Observable .combineLatest(userName.startWith(""), ringId.startWith("")) { (name, ringID) in var items: [SettingsSection.SectionRow] = [.sectionHeader(title: L10n.AccountPage.credentialsHeader), - .jamiID(label: ringID)] + .jamiID(label: ringID)] items.append(.jamiUserName(label: name)) items.append(.shareAccountDetails) return SettingsSection diff --git a/Ring/Ring/Models/ConferenceParticipant.swift b/Ring/Ring/Models/ConferenceParticipant.swift index a478bb6c5931c50c35c00adce6f5cb7b184327ab..1b5a54f04c4bd05bdc8e654d0acdb1ed5bf82a85 100644 --- a/Ring/Ring/Models/ConferenceParticipant.swift +++ b/Ring/Ring/Models/ConferenceParticipant.swift @@ -26,6 +26,10 @@ class ConferenceParticipant { var uri: String? var isActive: Bool = false var displayName: String = "" + var isModerator: Bool = false + var isAudioLocalyMuted: Bool = false + var isAudioMuted: Bool = false + var isVideoMuted: Bool = false init (info: [String: String], onlyURIAndActive: Bool) { self.uri = info["uri"] @@ -47,5 +51,17 @@ class ConferenceParticipant { if let participantHeight = info["h"] { self.height = CGFloat((participantHeight as NSString).doubleValue) } + if let videoMuted = info["videoMuted"] { + self.isVideoMuted = videoMuted.boolValue + } + if let audioLocalMuted = info["audioLocalMuted"] { + self.isAudioLocalyMuted = audioLocalMuted.boolValue + } + if let audioModeratorMuted = info["audioModeratorMuted"] { + self.isAudioMuted = audioModeratorMuted.boolValue + } + if let isModerator = info["isModerator"] { + self.isModerator = isModerator.boolValue + } } } diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings index 6eaee9da816850a47bf13b81a3b4147bcea81a51..19d32d258fa82a0fe755eb244752935ac2a1c621 100644 --- a/Ring/Ring/Resources/en.lproj/Localizable.strings +++ b/Ring/Ring/Resources/en.lproj/Localizable.strings @@ -185,6 +185,10 @@ "calls.haghUp" = "hang up"; "calls.maximize" = "maximize"; "calls.minimize" = "minimize"; +"calls.setModerator" = "set moderator"; +"calls.removeModerator" = "unset moderator"; +"calls.muteAudio" = "mute audio"; +"calls.unmuteAudio" = "unmute audio"; //Account Page "accountPage.devicesListHeader" = "Devices"; diff --git a/Ring/Ring/Services/CallsService.swift b/Ring/Ring/Services/CallsService.swift index be5cdcf58d41a6e554126cdcedaaf771ca9535b8..7cd69585c68d3c3fada5cdf417ea5ed0f3e1377a 100644 --- a/Ring/Ring/Services/CallsService.swift +++ b/Ring/Ring/Services/CallsService.swift @@ -174,7 +174,7 @@ class CallsService: CallsAdapterDelegate { guard let uri = participantURI, let participantsArray = self.callsAdapter.getConferenceInfo(conferenceId) as? [[String: String]] else { return nil } let participants = self.arrayToConferenceParticipants(participants: participantsArray, onlyURIAndActive: true) - for participant in participants where participant.uri == uri { + for participant in participants where participant.uri?.filterOutHost() == uri.filterOutHost() { return participant.isActive } return nil @@ -196,19 +196,24 @@ class CallsService: CallsAdapterDelegate { currentConferenceEvent.accept(ConferenceUpdates(conferenceID, ConferenceState.infoUpdated.rawValue, [""])) } + func isModerator(participantId: String, inConference confId: String) -> Bool { + let participants = self.conferenceInfos[confId] + let participant = participants?.filter({ confParticipant in + return confParticipant.uri?.filterOutHost() == participantId.filterOutHost() + }).first + return participant?.isModerator ?? false + } + func getConferenceParticipants(for conferenceId: String) -> [ConferenceParticipant]? { return conferenceInfos[conferenceId] } - func setActiveParticipant(callId: String?, conferenceId: String, maximixe: Bool, jamiId: String) { - let participantURI = callId == nil ? "" : self.call(callID: callId!)?.participantUri + func setActiveParticipant(conferenceId: String, maximixe: Bool, jamiId: String) { guard let conference = self.call(callID: conferenceId), - let uri = participantURI, - let isActive = self.isParticipant(participantURI: uri, activeIn: conferenceId) else { return } + let isActive = self.isParticipant(participantURI: jamiId, activeIn: conferenceId) else { return } let newLayout = isActive ? self.getNewLayoutForActiveParticipant(currentLayout: conference.layout, maximixe: maximixe) : .oneWithSmal conference.layout = newLayout - let participant = callId == nil ? jamiId : participantURI - self.callsAdapter.setActiveParticipant(participant, forConference: conferenceId) + self.callsAdapter.setActiveParticipant(jamiId, forConference: conferenceId) self.callsAdapter.setConferenceLayout(newLayout.rawValue, forConference: conferenceId) } @@ -667,4 +672,16 @@ class CallsService: CallsAdapterDelegate { self.call(callID: callID)?.participantsCallId = conferenceCalls } } + + func muteParticipant(confId: String, participantId: String, active: Bool) { + self.callsAdapter.muteConferenceParticipant(participantId, forConference: confId, active: active) + } + + func setModeratorParticipant(confId: String, participantId: String, active: Bool) { + self.callsAdapter.setConferenceModerator(participantId, forConference: confId, active: active) + } + + func hangupParticipant(confId: String, participantId: String) { + self.callsAdapter.hangupConferenceParticipant(participantId, forConference: confId) + } } diff --git a/Ring/RingTests/ConferenceMenuItemsManagerTest.swift b/Ring/RingTests/ConferenceMenuItemsManagerTest.swift index 302f61c746867b0468ff64ca328c5ac7595a6256..df50a1d09add88bf9adcbb695b73e718d4b5bf8b 100644 --- a/Ring/RingTests/ConferenceMenuItemsManagerTest.swift +++ b/Ring/RingTests/ConferenceMenuItemsManagerTest.swift @@ -31,58 +31,58 @@ class ConferenceMenuItemsManagerTest: XCTestCase { super.tearDown() } - func testGetMenuItemsForMasterCallNil() { + func testGetMenuItemsForLocalCallNil() { let manager = ConferenceMenuItemsManager() let conference: CallModel? = nil let active = true - XCTAssertTrue(manager.getMenuItemsForMasterCall(conference: conference, active: active) == MenuMode.onlyName) + XCTAssertTrue(manager.getMenuItemsForLocalCall(conference: conference, active: active) == [.name]) } - func testGetMenuItemsForeMasterCallWithoutActiveCall() { + func testGetMenuItemsForeLocalCallWithoutActiveCall() { let manager = ConferenceMenuItemsManager() let conference = CallModel() let active: Bool? = nil - XCTAssertTrue(manager.getMenuItemsForMasterCall(conference: conference, active: active) == MenuMode.onlyName) + XCTAssertTrue(manager.getMenuItemsForLocalCall(conference: conference, active: active) == [.name]) } - func testGetMenuItemsForMasterCallWithConferenceGridLayout() { + func testGetMenuItemsForLocalCallWithConferenceGridLayout() { let manager = ConferenceMenuItemsManager() let conference = CallModel() conference.layout = .grid let active = true - XCTAssertTrue(manager.getMenuItemsForMasterCall(conference: conference, active: active) == MenuMode.withoutHangUPAndMinimize) + XCTAssertTrue(manager.getMenuItemsForLocalCall(conference: conference, active: active) == [.name, .maximize, .muteAudio]) } - func testGetMenuItemsForActiveMasterCallWithConferenceOneWithSmalLayout() { + func testGetMenuItemsForActiveLocalCallWithConferenceOneWithSmalLayout() { let manager = ConferenceMenuItemsManager() let conference = CallModel() conference.layout = .oneWithSmal let active = true - XCTAssertTrue(manager.getMenuItemsForMasterCall(conference: conference, active: active) == MenuMode.withoutHangUp) + XCTAssertTrue(manager.getMenuItemsForLocalCall(conference: conference, active: active) == [.name, .maximize, .minimize, .muteAudio]) } - func testGetMenuItemsForNotActiveMasterCallWithConferenceOneWithSmalLayout() { + func testGetMenuItemsForNotActiveLocalCallWithConferenceOneWithSmalLayout() { let manager = ConferenceMenuItemsManager() let conference = CallModel() conference.layout = .oneWithSmal let active = false - XCTAssertTrue(manager.getMenuItemsForMasterCall(conference: conference, active: active) == MenuMode.withoutHangUPAndMinimize) + XCTAssertTrue(manager.getMenuItemsForLocalCall(conference: conference, active: active) == [.name, .maximize, .muteAudio]) } - func testGetMenuItemsForActiveMasterCallWithConferenceOneLayout() { + func testGetMenuItemsForActiveLocalCallWithConferenceOneLayout() { let manager = ConferenceMenuItemsManager() let conference = CallModel() conference.layout = .one let active = true - XCTAssertTrue(manager.getMenuItemsForMasterCall(conference: conference, active: active) == MenuMode.withoutHangUPAndMaximize) + XCTAssertTrue(manager.getMenuItemsForLocalCall(conference: conference, active: active) == [.name, .minimize, .muteAudio]) } - func testGetMenuItemsForNotActiveMasterCallWithConferenceOneLayout() { + func testGetMenuItemsForNotActiveLocalCallWithConferenceOneLayout() { let manager = ConferenceMenuItemsManager() let conference = CallModel() conference.layout = .one let active = false - XCTAssertTrue(manager.getMenuItemsForMasterCall(conference: conference, active: active) == MenuMode.withoutHangUPAndMinimize) + XCTAssertTrue(manager.getMenuItemsForLocalCall(conference: conference, active: active) == [.name, .maximize, .muteAudio]) } func testGetMenuItemsForNilConference() { @@ -90,7 +90,9 @@ class ConferenceMenuItemsManagerTest: XCTestCase { let conference: CallModel? = nil let call: CallModel? = CallModel() let active = true - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.onlyName) + let role = RoleInCall.host + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name]) } func testGetMenuItemsForNilCall() { @@ -98,7 +100,9 @@ class ConferenceMenuItemsManagerTest: XCTestCase { let conference: CallModel? = CallModel() let call: CallModel? = nil let active = true - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.onlyName) + let role = RoleInCall.host + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name]) } func testGetMenuItemsWithoutActiveCall() { @@ -106,8 +110,10 @@ class ConferenceMenuItemsManagerTest: XCTestCase { let conference: CallModel? = CallModel() let call: CallModel? = CallModel() call?.state = .current + let role = RoleInCall.host let active: Bool? = nil - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.onlyName) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name]) } func testGetMenuItemsForConnectingCall() { @@ -115,8 +121,10 @@ class ConferenceMenuItemsManagerTest: XCTestCase { let conference: CallModel? = CallModel() let call: CallModel? = CallModel() call?.state = .connecting + let role = RoleInCall.host let active: Bool? = true - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.withoutMaximizeAndMinimize) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name, .hangup]) } func testGetMenuItemsForRingingCall() { @@ -124,8 +132,10 @@ class ConferenceMenuItemsManagerTest: XCTestCase { let conference: CallModel? = CallModel() let call: CallModel? = CallModel() call?.state = .ringing + let role = RoleInCall.host let active: Bool? = true - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.withoutMaximizeAndMinimize) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name, .hangup]) } func testGetMenuItemsForHoldingCall() { @@ -133,8 +143,10 @@ class ConferenceMenuItemsManagerTest: XCTestCase { let conference: CallModel? = CallModel() let call: CallModel? = CallModel() call?.state = .hold + let role = RoleInCall.host let active: Bool? = true - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.withoutMaximizeAndMinimize) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name, .hangup]) } func testGetMenuItemsForCallWithConferenceGridLayout() { @@ -143,8 +155,10 @@ class ConferenceMenuItemsManagerTest: XCTestCase { conference?.layout = .grid let call: CallModel? = CallModel() call?.state = .current + let role = RoleInCall.host let active: Bool? = true - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.withoutMinimize) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name, .maximize, .muteAudio, .setModerator, .hangup]) } func testGetMenuItemsForActiveCallWithConferenceOneWithSmalLayout() { @@ -152,9 +166,11 @@ class ConferenceMenuItemsManagerTest: XCTestCase { let conference: CallModel? = CallModel() conference?.layout = .oneWithSmal let call: CallModel? = CallModel() + let role = RoleInCall.host call?.state = .current let active: Bool? = true - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.all) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name, .maximize, .minimize, .muteAudio, .setModerator, .hangup]) } func testGetMenuItemsForNotActiveCallWithConferenceOneWithSmalLayout() { @@ -162,9 +178,11 @@ class ConferenceMenuItemsManagerTest: XCTestCase { let conference: CallModel? = CallModel() conference?.layout = .oneWithSmal let call: CallModel? = CallModel() + let role = RoleInCall.host call?.state = .current let active: Bool? = false - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.withoutMinimize) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name, .maximize, .muteAudio, .setModerator, .hangup]) } func testGetMenuItemsForActiveCallWithConferenceOneLayout() { @@ -173,8 +191,10 @@ class ConferenceMenuItemsManagerTest: XCTestCase { conference?.layout = .one let call: CallModel? = CallModel() call?.state = .current + let role = RoleInCall.host let active: Bool? = true - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.withoutMaximize) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name, .minimize, .muteAudio, .setModerator, .hangup]) } func testGetMenuItemsForNotActiveCallWithConferenceOneLayout() { @@ -183,7 +203,9 @@ class ConferenceMenuItemsManagerTest: XCTestCase { conference?.layout = .one let call: CallModel? = CallModel() call?.state = .current + let role = RoleInCall.host let active: Bool? = false - XCTAssertTrue(manager.getMenuItemsFor(call: call, conference: conference, active: active) == MenuMode.withoutMinimize) + let isHost = false + XCTAssertTrue(manager.getMenuItemsFor(call: call, isHost: isHost, conference: conference, active: active, role: role) == [.name, .maximize, .muteAudio, .setModerator, .hangup]) } }