mirror of https://github.com/oxen-io/session-ios
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
761 lines
27 KiB
761 lines
27 KiB
// |
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved. |
|
// |
|
|
|
import Foundation |
|
import SignalRingRTC |
|
|
|
// TODO: Eventually add 1:1 call support to this view |
|
// and replace CallViewController |
|
class GroupCallViewController: UIViewController { |
|
private let thread: TSGroupThread? |
|
private let call: SignalCall |
|
private var groupCall: GroupCall { call.groupCall } |
|
private lazy var callControls = CallControls(call: call, delegate: self) |
|
private lazy var callHeader = CallHeader(call: call, delegate: self) |
|
private lazy var notificationView = GroupCallNotificationView(call: call) |
|
|
|
private lazy var videoGrid = GroupCallVideoGrid(call: call) |
|
private lazy var videoOverflow = GroupCallVideoOverflow(call: call, delegate: self) |
|
|
|
private let localMemberView = GroupCallLocalMemberView() |
|
private let speakerView = GroupCallRemoteMemberView(mode: .speaker) |
|
|
|
private var didUserEverSwipeToSpeakerView = true |
|
private var didUserEverSwipeToScreenShare = true |
|
private let swipeToastView = GroupCallSwipeToastView() |
|
|
|
private var speakerPage = UIView() |
|
|
|
private let scrollView = UIScrollView() |
|
|
|
private var isCallMinimized = false { |
|
didSet { speakerView.isCallMinimized = isCallMinimized } |
|
} |
|
|
|
private var isAutoScrollingToScreenShare = false |
|
private var isAnyRemoteDeviceScreenSharing = false { |
|
didSet { |
|
guard oldValue != isAnyRemoteDeviceScreenSharing else { return } |
|
|
|
// Scroll to speaker view when presenting begins. |
|
if isAnyRemoteDeviceScreenSharing { |
|
isAutoScrollingToScreenShare = true |
|
scrollView.setContentOffset(CGPoint(x: 0, y: speakerPage.frame.origin.y), animated: true) |
|
} |
|
} |
|
} |
|
|
|
lazy var tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTouchRootView)) |
|
lazy var videoOverflowTopConstraint = videoOverflow.autoPinEdge(toSuperviewEdge: .top) |
|
lazy var videoOverflowTrailingConstraint = videoOverflow.autoPinEdge(toSuperviewEdge: .trailing) |
|
|
|
var shouldRemoteVideoControlsBeHidden = false { |
|
didSet { updateCallUI() } |
|
} |
|
var hasUnresolvedSafetyNumberMismatch = false |
|
|
|
private static let keyValueStore = SDSKeyValueStore(collection: "GroupCallViewController") |
|
private static let didUserSwipeToSpeakerViewKey = "didUserSwipeToSpeakerView" |
|
private static let didUserSwipeToScreenShareKey = "didUserSwipeToScreenShare" |
|
|
|
init(call: SignalCall) { |
|
// TODO: Eventually unify UI for group and individual calls |
|
owsAssertDebug(call.isGroupCall) |
|
|
|
self.call = call |
|
self.thread = call.thread as? TSGroupThread |
|
|
|
super.init(nibName: nil, bundle: nil) |
|
|
|
call.addObserverAndSyncState(observer: self) |
|
|
|
videoGrid.memberViewDelegate = self |
|
videoOverflow.memberViewDelegate = self |
|
speakerView.delegate = self |
|
localMemberView.delegate = self |
|
|
|
SDSDatabaseStorage.shared.asyncRead { readTx in |
|
self.didUserEverSwipeToSpeakerView = Self.keyValueStore.getBool( |
|
Self.didUserSwipeToSpeakerViewKey, |
|
defaultValue: false, |
|
transaction: readTx |
|
) |
|
self.didUserEverSwipeToScreenShare = Self.keyValueStore.getBool( |
|
Self.didUserSwipeToScreenShareKey, |
|
defaultValue: false, |
|
transaction: readTx |
|
) |
|
} completion: { |
|
self.updateSwipeToastView() |
|
} |
|
} |
|
|
|
@discardableResult |
|
@objc(presentLobbyForThread:) |
|
class func presentLobby(thread: TSGroupThread) -> Bool { |
|
guard let frontmostViewController = UIApplication.shared.frontmostViewController else { |
|
owsFailDebug("could not identify frontmostViewController") |
|
return false |
|
} |
|
|
|
frontmostViewController.ows_ask(forMicrophonePermissions: { granted in |
|
guard granted == true else { |
|
Logger.warn("aborting due to missing microphone permissions.") |
|
frontmostViewController.ows_showNoMicrophonePermissionActionSheet() |
|
return |
|
} |
|
|
|
frontmostViewController.ows_ask(forCameraPermissions: { granted in |
|
guard granted else { |
|
Logger.warn("aborting due to missing camera permissions.") |
|
return |
|
} |
|
|
|
guard let groupCall = Self.callService.buildAndConnectGroupCallIfPossible( |
|
thread: thread |
|
) else { |
|
return owsFailDebug("Failed to build group call") |
|
} |
|
|
|
let vc = GroupCallViewController(call: groupCall) |
|
vc.modalTransitionStyle = .crossDissolve |
|
|
|
OWSWindowManager.shared().startCall(vc) |
|
}) |
|
}) |
|
|
|
return true |
|
} |
|
|
|
required init?(coder: NSCoder) { |
|
fatalError("init(coder:) has not been implemented") |
|
} |
|
|
|
override func loadView() { |
|
view = UIView() |
|
view.clipsToBounds = true |
|
|
|
view.backgroundColor = .ows_black |
|
|
|
scrollView.delegate = self |
|
view.addSubview(scrollView) |
|
scrollView.isPagingEnabled = true |
|
scrollView.showsVerticalScrollIndicator = false |
|
scrollView.contentInsetAdjustmentBehavior = .never |
|
scrollView.alwaysBounceVertical = false |
|
scrollView.autoPinEdgesToSuperviewEdges() |
|
|
|
view.addSubview(callHeader) |
|
callHeader.autoPinWidthToSuperview() |
|
callHeader.autoPinEdge(toSuperviewEdge: .top) |
|
|
|
view.addSubview(notificationView) |
|
notificationView.autoPinEdgesToSuperviewEdges() |
|
|
|
view.addSubview(callControls) |
|
callControls.autoPinWidthToSuperview() |
|
callControls.autoPinEdge(toSuperviewEdge: .bottom) |
|
|
|
view.addSubview(videoOverflow) |
|
videoOverflow.autoPinEdge(toSuperviewEdge: .leading) |
|
|
|
scrollView.addSubview(videoGrid) |
|
scrollView.addSubview(speakerPage) |
|
|
|
scrollView.addSubview(swipeToastView) |
|
swipeToastView.autoPinEdge(.bottom, to: .bottom, of: videoGrid, withOffset: -22) |
|
swipeToastView.autoHCenterInSuperview() |
|
swipeToastView.autoPinEdge(toSuperviewMargin: .leading, relation: .greaterThanOrEqual) |
|
swipeToastView.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual) |
|
|
|
view.addGestureRecognizer(tapGesture) |
|
|
|
updateCallUI() |
|
} |
|
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { |
|
super.viewWillTransition(to: size, with: coordinator) |
|
|
|
let wasOnSpeakerPage = scrollView.contentOffset.y >= view.height() |
|
|
|
coordinator.animate(alongsideTransition: { _ in |
|
self.updateCallUI(size: size) |
|
self.videoGrid.reloadData() |
|
self.videoOverflow.reloadData() |
|
self.scrollView.contentOffset = wasOnSpeakerPage ? CGPoint(x: 0, y: size.height) : .zero |
|
}, completion: nil) |
|
} |
|
|
|
private var hasAppeared = false |
|
override func viewWillAppear(_ animated: Bool) { |
|
super.viewWillAppear(animated) |
|
|
|
guard !hasAppeared else { return } |
|
hasAppeared = true |
|
|
|
guard let splitViewSnapshot = SignalApp.shared().snapshotSplitViewController(afterScreenUpdates: false) else { |
|
return owsFailDebug("failed to snapshot rootViewController") |
|
} |
|
|
|
view.superview?.insertSubview(splitViewSnapshot, belowSubview: view) |
|
splitViewSnapshot.autoPinEdgesToSuperviewEdges() |
|
|
|
view.transform = .scale(1.5) |
|
view.alpha = 0 |
|
|
|
UIView.animate(withDuration: 0.2, animations: { |
|
self.view.alpha = 1 |
|
self.view.transform = .identity |
|
}) { _ in |
|
splitViewSnapshot.removeFromSuperview() |
|
} |
|
} |
|
|
|
private var hasOverflowMembers: Bool { videoGrid.maxItems < groupCall.remoteDeviceStates.count } |
|
|
|
private func updateScrollViewFrames(size: CGSize? = nil, controlsAreHidden: Bool) { |
|
view.layoutIfNeeded() |
|
|
|
let size = size ?? view.frame.size |
|
|
|
if groupCall.remoteDeviceStates.count < 2 || groupCall.localDeviceState.joinState != .joined { |
|
videoGrid.frame = .zero |
|
videoGrid.isHidden = true |
|
speakerPage.frame = CGRect( |
|
x: 0, |
|
y: 0, |
|
width: size.width, |
|
height: size.height |
|
) |
|
scrollView.contentSize = size |
|
scrollView.contentOffset = .zero |
|
scrollView.isScrollEnabled = false |
|
} else { |
|
let wasVideoGridHidden = videoGrid.isHidden |
|
|
|
scrollView.isScrollEnabled = true |
|
videoGrid.isHidden = false |
|
let height = size.height - view.safeAreaInsets.top - (controlsAreHidden ? 16 : callControls.height()) - (hasOverflowMembers ? videoOverflow.height() + 32 : 0) |
|
videoGrid.frame = CGRect( |
|
x: 0, |
|
y: view.safeAreaInsets.top, |
|
width: size.width, |
|
height: height |
|
) |
|
speakerPage.frame = CGRect( |
|
x: 0, |
|
y: size.height, |
|
width: size.width, |
|
height: size.height |
|
) |
|
scrollView.contentSize = CGSize(width: size.width, height: size.height * 2) |
|
|
|
if wasVideoGridHidden { |
|
scrollView.contentOffset = .zero |
|
} |
|
} |
|
} |
|
|
|
func updateVideoOverflowTrailingConstraint() { |
|
var trailingConstraintConstant = -(GroupCallVideoOverflow.itemHeight * ReturnToCallViewController.pipSize.aspectRatio + 4) |
|
if view.width() + trailingConstraintConstant > videoOverflow.contentSize.width { |
|
trailingConstraintConstant += 16 |
|
} |
|
videoOverflowTrailingConstraint.constant = trailingConstraintConstant |
|
view.layoutIfNeeded() |
|
} |
|
|
|
private func updateMemberViewFrames(size: CGSize? = nil, controlsAreHidden: Bool) { |
|
view.layoutIfNeeded() |
|
|
|
let size = size ?? view.frame.size |
|
|
|
let yMax = (controlsAreHidden ? size.height - 16 : callControls.frame.minY) - 16 |
|
|
|
videoOverflowTopConstraint.constant = yMax - videoOverflow.height() |
|
|
|
updateVideoOverflowTrailingConstraint() |
|
|
|
localMemberView.removeFromSuperview() |
|
speakerView.removeFromSuperview() |
|
|
|
switch groupCall.localDeviceState.joinState { |
|
case .joined: |
|
if groupCall.remoteDeviceStates.count > 0 { |
|
speakerPage.addSubview(speakerView) |
|
speakerView.autoPinEdgesToSuperviewEdges() |
|
|
|
view.addSubview(localMemberView) |
|
|
|
if groupCall.remoteDeviceStates.count > 1 { |
|
let pipWidth = GroupCallVideoOverflow.itemHeight * ReturnToCallViewController.pipSize.aspectRatio |
|
let pipHeight = GroupCallVideoOverflow.itemHeight |
|
localMemberView.frame = CGRect( |
|
x: size.width - pipWidth - 16, |
|
y: videoOverflow.frame.origin.y, |
|
width: pipWidth, |
|
height: pipHeight |
|
) |
|
} else { |
|
let pipWidth = ReturnToCallViewController.pipSize.width |
|
let pipHeight = ReturnToCallViewController.pipSize.height |
|
|
|
localMemberView.frame = CGRect( |
|
x: size.width - pipWidth - 16, |
|
y: yMax - pipHeight, |
|
width: pipWidth, |
|
height: pipHeight |
|
) |
|
} |
|
} else { |
|
speakerPage.addSubview(localMemberView) |
|
localMemberView.frame = CGRect(origin: .zero, size: size) |
|
} |
|
case .notJoined, .joining: |
|
speakerPage.addSubview(localMemberView) |
|
localMemberView.frame = CGRect(origin: .zero, size: size) |
|
} |
|
} |
|
|
|
func updateSwipeToastView() { |
|
let isSpeakerViewAvailable = groupCall.remoteDeviceStates.count >= 2 && groupCall.localDeviceState.joinState == .joined |
|
guard isSpeakerViewAvailable else { |
|
swipeToastView.isHidden = true |
|
return |
|
} |
|
|
|
if isAnyRemoteDeviceScreenSharing { |
|
if didUserEverSwipeToScreenShare { |
|
swipeToastView.isHidden = true |
|
return |
|
} |
|
} else if didUserEverSwipeToSpeakerView { |
|
swipeToastView.isHidden = true |
|
return |
|
} |
|
|
|
swipeToastView.alpha = 1.0 - (scrollView.contentOffset.y / view.height()) |
|
swipeToastView.text = isAnyRemoteDeviceScreenSharing |
|
? NSLocalizedString( |
|
"GROUP_CALL_SCREEN_SHARE_TOAST", |
|
comment: "Toast view text informing user about swiping to screen share" |
|
) |
|
: NSLocalizedString( |
|
"GROUP_CALL_SPEAKER_VIEW_TOAST", |
|
comment: "Toast view text informing user about swiping to speaker view" |
|
) |
|
|
|
if scrollView.contentOffset.y >= view.height() { |
|
swipeToastView.isHidden = true |
|
|
|
if isAnyRemoteDeviceScreenSharing { |
|
if !isAutoScrollingToScreenShare { |
|
didUserEverSwipeToScreenShare = true |
|
|
|
// TODO: Implement |
|
|
|
/* |
|
SDSDatabaseStorage.shared.asyncWrite { writeTx in |
|
Self.keyValueStore.setBool(true, key: Self.didUserSwipeToScreenShareKey, transaction: writeTx) |
|
} |
|
*/ |
|
} |
|
} else { |
|
didUserEverSwipeToSpeakerView = true |
|
|
|
// TODO: Implement |
|
|
|
/* |
|
SDSDatabaseStorage.shared.asyncWrite { writeTx in |
|
Self.keyValueStore.setBool(true, key: Self.didUserSwipeToSpeakerViewKey, transaction: writeTx) |
|
} |
|
*/ |
|
} |
|
|
|
} else if swipeToastView.isHidden { |
|
swipeToastView.alpha = 0 |
|
swipeToastView.isHidden = false |
|
UIView.animate(withDuration: 0.2, delay: 3.0, options: []) { |
|
self.swipeToastView.alpha = 1 |
|
} |
|
|
|
} |
|
} |
|
|
|
func updateCallUI(size: CGSize? = nil) { |
|
let localDevice = groupCall.localDeviceState |
|
|
|
localMemberView.configure( |
|
call: call, |
|
isFullScreen: localDevice.joinState != .joined || groupCall.remoteDeviceStates.isEmpty |
|
) |
|
|
|
if let speakerState = groupCall.remoteDeviceStates.sortedBySpeakerTime.first { |
|
speakerView.configure( |
|
call: call, |
|
device: speakerState |
|
) |
|
} else { |
|
speakerView.clearConfiguration() |
|
} |
|
|
|
// Setting the speakerphone before we join the call will fail, |
|
// but we can re-apply the setting here in case it did not work. |
|
if groupCall.isOutgoingVideoMuted && !callService.audioService.hasExternalInputs { |
|
callService.audioService.requestSpeakerphone(isEnabled: callControls.audioSourceButton.isSelected) |
|
} |
|
|
|
guard !isCallMinimized else { return } |
|
|
|
let hideRemoteControls = shouldRemoteVideoControlsBeHidden && !groupCall.remoteDeviceStates.isEmpty |
|
let remoteControlsAreHidden = callControls.isHidden && callHeader.isHidden |
|
if hideRemoteControls != remoteControlsAreHidden { |
|
callControls.isHidden = false |
|
callHeader.isHidden = false |
|
|
|
UIView.animate(withDuration: 0.15, animations: { |
|
self.callControls.alpha = hideRemoteControls ? 0 : 1 |
|
self.callHeader.alpha = hideRemoteControls ? 0 : 1 |
|
|
|
self.updateMemberViewFrames(size: size, controlsAreHidden: hideRemoteControls) |
|
self.updateScrollViewFrames(size: size, controlsAreHidden: hideRemoteControls) |
|
self.view.layoutIfNeeded() |
|
}) { _ in |
|
self.callControls.isHidden = hideRemoteControls |
|
self.callHeader.isHidden = hideRemoteControls |
|
} |
|
} else { |
|
updateMemberViewFrames(size: size, controlsAreHidden: hideRemoteControls) |
|
updateScrollViewFrames(size: size, controlsAreHidden: hideRemoteControls) |
|
} |
|
|
|
scheduleControlTimeoutIfNecessary() |
|
updateSwipeToastView() |
|
} |
|
|
|
func dismissCall() { |
|
callService.terminate(call: call) |
|
|
|
guard let splitViewSnapshot = SignalApp.shared().snapshotSplitViewController(afterScreenUpdates: false) else { |
|
OWSWindowManager.shared().endCall(self) |
|
return owsFailDebug("failed to snapshot rootViewController") |
|
} |
|
|
|
view.superview?.insertSubview(splitViewSnapshot, belowSubview: view) |
|
splitViewSnapshot.autoPinEdgesToSuperviewEdges() |
|
|
|
UIView.animate(withDuration: 0.2, animations: { |
|
self.view.alpha = 0 |
|
}) { _ in |
|
splitViewSnapshot.removeFromSuperview() |
|
OWSWindowManager.shared().endCall(self) |
|
} |
|
} |
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle { |
|
return .lightContent |
|
} |
|
|
|
override var prefersHomeIndicatorAutoHidden: Bool { |
|
return true |
|
} |
|
|
|
// MARK: - Video control timeout |
|
|
|
@objc func didTouchRootView(sender: UIGestureRecognizer) { |
|
shouldRemoteVideoControlsBeHidden = !shouldRemoteVideoControlsBeHidden |
|
} |
|
|
|
private var controlTimeoutTimer: Timer? |
|
private func scheduleControlTimeoutIfNecessary() { |
|
if groupCall.remoteDeviceStates.isEmpty || shouldRemoteVideoControlsBeHidden { |
|
controlTimeoutTimer?.invalidate() |
|
controlTimeoutTimer = nil |
|
} |
|
|
|
guard controlTimeoutTimer == nil else { return } |
|
controlTimeoutTimer = .weakScheduledTimer( |
|
withTimeInterval: 5, |
|
target: self, |
|
selector: #selector(timeoutControls), |
|
userInfo: nil, |
|
repeats: false |
|
) |
|
} |
|
|
|
@objc |
|
private func timeoutControls() { |
|
controlTimeoutTimer?.invalidate() |
|
controlTimeoutTimer = nil |
|
|
|
guard !isCallMinimized && !groupCall.remoteDeviceStates.isEmpty && !shouldRemoteVideoControlsBeHidden else { return } |
|
shouldRemoteVideoControlsBeHidden = true |
|
} |
|
} |
|
|
|
extension GroupCallViewController: CallViewControllerWindowReference { |
|
var localVideoViewReference: UIView { localMemberView } |
|
var remoteVideoViewReference: UIView { speakerView } |
|
|
|
var remoteVideoAddress: String { |
|
guard let firstMember = groupCall.remoteDeviceStates.sortedByAddedTime.first else { |
|
return getUserHexEncodedPublicKey() |
|
} |
|
return firstMember.address |
|
} |
|
|
|
@objc |
|
public func returnFromPip(pipWindow: UIWindow) { |
|
// The call "pip" uses our remote and local video views since only |
|
// one `AVCaptureVideoPreviewLayer` per capture session is supported. |
|
// We need to re-add them when we return to this view. |
|
guard speakerView.superview != speakerPage && localMemberView.superview != view else { |
|
return owsFailDebug("unexpectedly returned to call while we own the video views") |
|
} |
|
|
|
guard let splitViewSnapshot = SignalApp.shared().snapshotSplitViewController(afterScreenUpdates: false) else { |
|
return owsFailDebug("failed to snapshot rootViewController") |
|
} |
|
|
|
guard let pipSnapshot = pipWindow.snapshotView(afterScreenUpdates: false) else { |
|
return owsFailDebug("failed to snapshot pip") |
|
} |
|
|
|
isCallMinimized = false |
|
shouldRemoteVideoControlsBeHidden = false |
|
|
|
animateReturnFromPip(pipSnapshot: pipSnapshot, pipFrame: pipWindow.frame, splitViewSnapshot: splitViewSnapshot) |
|
} |
|
|
|
private func animateReturnFromPip(pipSnapshot: UIView, pipFrame: CGRect, splitViewSnapshot: UIView) { |
|
guard let window = view.window else { return owsFailDebug("missing window") } |
|
view.superview?.insertSubview(splitViewSnapshot, belowSubview: view) |
|
splitViewSnapshot.autoPinEdgesToSuperviewEdges() |
|
|
|
let originalContentOffset = scrollView.contentOffset |
|
|
|
view.frame = pipFrame |
|
view.addSubview(pipSnapshot) |
|
pipSnapshot.autoPinEdgesToSuperviewEdges() |
|
|
|
view.layoutIfNeeded() |
|
|
|
UIView.animate(withDuration: 0.2, animations: { |
|
pipSnapshot.alpha = 0 |
|
self.view.frame = window.frame |
|
self.updateCallUI() |
|
self.scrollView.contentOffset = originalContentOffset |
|
self.view.layoutIfNeeded() |
|
}) { _ in |
|
splitViewSnapshot.removeFromSuperview() |
|
pipSnapshot.removeFromSuperview() |
|
} |
|
} |
|
} |
|
|
|
extension GroupCallViewController: CallObserver { |
|
func groupCallLocalDeviceStateChanged(_ call: SignalCall) { |
|
AssertIsOnMainThread() |
|
owsAssertDebug(call.isGroupCall) |
|
|
|
updateCallUI() |
|
} |
|
|
|
func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) { |
|
AssertIsOnMainThread() |
|
owsAssertDebug(call.isGroupCall) |
|
|
|
isAnyRemoteDeviceScreenSharing = call.groupCall.remoteDeviceStates.values.first { $0.sharingScreen == true } != nil |
|
|
|
updateCallUI() |
|
} |
|
|
|
func groupCallPeekChanged(_ call: SignalCall) { |
|
AssertIsOnMainThread() |
|
owsAssertDebug(call.isGroupCall) |
|
|
|
updateCallUI() |
|
} |
|
|
|
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason) { |
|
// TODO: Implement |
|
|
|
/* |
|
AssertIsOnMainThread() |
|
owsAssertDebug(call.isGroupCall) |
|
|
|
defer { updateCallUI() } |
|
|
|
guard reason != .deviceExplicitlyDisconnected else { return } |
|
|
|
let title: String |
|
|
|
if reason == .hasMaxDevices { |
|
if let maxDevices = groupCall.maxDevices { |
|
let formatString = NSLocalizedString( |
|
"GROUP_CALL_HAS_MAX_DEVICES_FORMAT", |
|
comment: "An error displayed to the user when the group call ends because it has exceeded the max devices. Embeds {{max device count}}." |
|
) |
|
title = String(format: formatString, maxDevices) |
|
} else { |
|
title = NSLocalizedString( |
|
"GROUP_CALL_HAS_MAX_DEVICES_UNKNOWN_COUNT", |
|
comment: "An error displayed to the user when the group call ends because it has exceeded the max devices." |
|
) |
|
} |
|
} else { |
|
owsFailDebug("Group call ended with reason \(reason)") |
|
title = NSLocalizedString( |
|
"GROUP_CALL_UNEXPECTEDLY_ENDED", |
|
comment: "An error displayed to the user when the group call unexpectedly ends." |
|
) |
|
} |
|
|
|
let actionSheet = ActionSheetController(title: title) |
|
actionSheet.addAction(ActionSheetAction( |
|
title: CommonStrings.okButton, |
|
style: .default, |
|
handler: { [weak self] _ in |
|
guard reason == .hasMaxDevices else { return } |
|
self?.dismissCall() |
|
} |
|
)) |
|
presentActionSheet(actionSheet) |
|
*/ |
|
} |
|
|
|
func callMessageSendFailedUntrustedIdentity(_ call: SignalCall) { |
|
// TODO: Do something |
|
} |
|
} |
|
|
|
extension GroupCallViewController: CallControlsDelegate { |
|
func didPressHangup(sender: UIButton) { |
|
dismissCall() |
|
} |
|
|
|
func didPressAudioSource(sender: UIButton) { |
|
if callService.audioService.hasExternalInputs { |
|
callService.audioService.presentRoutePicker() |
|
} else { |
|
sender.isSelected = !sender.isSelected |
|
callService.audioService.requestSpeakerphone(isEnabled: sender.isSelected) |
|
} |
|
} |
|
|
|
func didPressMute(sender: UIButton) { |
|
sender.isSelected = !sender.isSelected |
|
callService.updateIsLocalAudioMuted(isLocalAudioMuted: sender.isSelected) |
|
} |
|
|
|
func didPressVideo(sender: UIButton) { |
|
sender.isSelected = !sender.isSelected |
|
|
|
callService.updateIsLocalVideoMuted(isLocalVideoMuted: !sender.isSelected) |
|
|
|
// When turning off video, default speakerphone to on. |
|
if !sender.isSelected && !callService.audioService.hasExternalInputs { |
|
callControls.audioSourceButton.isSelected = true |
|
callService.audioService.requestSpeakerphone(isEnabled: true) |
|
} |
|
} |
|
|
|
func didPressFlipCamera(sender: UIButton) { |
|
sender.isSelected = !sender.isSelected |
|
callService.updateCameraSource(call: call, isUsingFrontCamera: !sender.isSelected) |
|
} |
|
|
|
func didPressCancel(sender: UIButton) { |
|
dismissCall() |
|
} |
|
|
|
func didPressJoin(sender: UIButton) { |
|
self.callService.joinGroupCallIfNecessary(self.call) |
|
} |
|
} |
|
|
|
extension GroupCallViewController: CallHeaderDelegate { |
|
func didTapBackButton() { |
|
if groupCall.localDeviceState.joinState == .joined { |
|
isCallMinimized = true |
|
OWSWindowManager.shared().leaveCallView() |
|
} else { |
|
dismissCall() |
|
} |
|
} |
|
|
|
func didTapMembersButton() { |
|
// TODO: Implement |
|
/* |
|
let sheet = GroupCallMemberSheet(call: call) |
|
present(sheet, animated: true) |
|
*/ |
|
} |
|
} |
|
|
|
extension GroupCallViewController: GroupCallVideoOverflowDelegate { |
|
var firstOverflowMemberIndex: Int { |
|
if scrollView.contentOffset.y >= view.height() { |
|
return 1 |
|
} else { |
|
return videoGrid.maxItems |
|
} |
|
} |
|
} |
|
|
|
extension GroupCallViewController: UIScrollViewDelegate { |
|
func scrollViewDidScroll(_ scrollView: UIScrollView) { |
|
// If we changed pages, update the overflow view. |
|
if scrollView.contentOffset.y == 0 || scrollView.contentOffset.y == view.height() { |
|
videoOverflow.reloadData() |
|
updateCallUI() |
|
} |
|
|
|
if isAutoScrollingToScreenShare { |
|
isAutoScrollingToScreenShare = scrollView.contentOffset.y != speakerView.frame.origin.y |
|
} |
|
|
|
updateSwipeToastView() |
|
} |
|
} |
|
|
|
extension GroupCallViewController: GroupCallMemberViewDelegate { |
|
|
|
func memberView(_ view: GroupCallMemberView, userRequestedInfoAboutError error: GroupCallMemberView.ErrorState) { |
|
// TODO: Implement |
|
|
|
/* |
|
let title: String |
|
let message: String |
|
|
|
switch error { |
|
case let .blocked(address): |
|
message = NSLocalizedString( |
|
"GROUP_CALL_BLOCKED_ALERT_MESSAGE", |
|
comment: "Message body for alert explaining that a group call participant is blocked") |
|
|
|
let titleFormat = NSLocalizedString( |
|
"GROUP_CALL_BLOCKED_ALERT_TITLE_FORMAT", |
|
comment: "Title for alert explaining that a group call participant is blocked. Embeds {{ user's name }}") |
|
let displayName = Storage.shared.getContact(with: address)?.displayName(for: .regular) ?? address |
|
title = String(format: titleFormat, displayName) |
|
|
|
case let .noMediaKeys(address): |
|
message = NSLocalizedString( |
|
"GROUP_CALL_NO_KEYS_ALERT_MESSAGE", |
|
comment: "Message body for alert explaining that a group call participant cannot be displayed because of missing keys") |
|
|
|
let titleFormat = NSLocalizedString( |
|
"GROUP_CALL_NO_KEYS_ALERT_TITLE_FORMAT", |
|
comment: "Title for alert explaining that a group call participant cannot be displayed because of missing keys. Embeds {{ user's name }}") |
|
let displayName = Storage.shared.getContact(with: address)?.displayName(for: .regular) ?? address |
|
title = String(format: titleFormat, displayName) |
|
} |
|
|
|
let actionSheet = ActionSheetController(title: title, message: message, theme: .translucentDark) |
|
actionSheet.addAction(ActionSheetAction(title: CommonStrings.okButton)) |
|
presentActionSheet(actionSheet) |
|
*/ |
|
} |
|
}
|
|
|