mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
11 KiB
Swift
292 lines
11 KiB
Swift
//
|
|
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalRingRTC
|
|
|
|
@objc
|
|
protocol CallControlsDelegate: AnyObject {
|
|
func didPressHangup(sender: UIButton)
|
|
func didPressAudioSource(sender: UIButton)
|
|
func didPressMute(sender: UIButton)
|
|
func didPressVideo(sender: UIButton)
|
|
func didPressFlipCamera(sender: UIButton)
|
|
func didPressCancel(sender: UIButton)
|
|
func didPressJoin(sender: UIButton)
|
|
}
|
|
|
|
class CallControls: UIView {
|
|
private lazy var hangUpButton: CallButton = {
|
|
let button = createButton(
|
|
iconName: "phone-down-solid-28",
|
|
action: #selector(CallControlsDelegate.didPressHangup)
|
|
)
|
|
button.unselectedBackgroundColor = .ows_accentRed
|
|
return button
|
|
}()
|
|
private(set) lazy var audioSourceButton = createButton(
|
|
iconName: "speaker-solid-28",
|
|
action: #selector(CallControlsDelegate.didPressAudioSource)
|
|
)
|
|
private lazy var muteButton = createButton(
|
|
iconName: "mic-off-solid-28",
|
|
action: #selector(CallControlsDelegate.didPressMute)
|
|
)
|
|
private lazy var videoButton = createButton(
|
|
iconName: "video-solid-28",
|
|
action: #selector(CallControlsDelegate.didPressVideo)
|
|
)
|
|
private lazy var flipCameraButton: CallButton = {
|
|
let button = createButton(
|
|
iconName: "switch-camera-28",
|
|
action: #selector(CallControlsDelegate.didPressFlipCamera)
|
|
)
|
|
button.selectedIconColor = button.iconColor
|
|
button.selectedBackgroundColor = button.unselectedBackgroundColor
|
|
return button
|
|
}()
|
|
|
|
private lazy var cancelButton: UIButton = {
|
|
let button = OWSButton()
|
|
button.setTitle(CommonStrings.cancelButton, for: .normal)
|
|
button.setTitleColor(.ows_white, for: .normal)
|
|
button.setBackgroundImage(UIImage(color: .ows_whiteAlpha40), for: .normal)
|
|
button.titleLabel?.font = UIFont.ows_dynamicTypeBodyClamped.ows_semibold
|
|
button.clipsToBounds = true
|
|
button.layer.cornerRadius = 8
|
|
button.block = { [weak self] in
|
|
self?.delegate.didPressCancel(sender: button)
|
|
}
|
|
button.contentEdgeInsets = UIEdgeInsets(top: 11, leading: 11, bottom: 11, trailing: 11)
|
|
return button
|
|
}()
|
|
|
|
private lazy var joinButtonActivityIndicator = UIActivityIndicatorView(style: .white)
|
|
|
|
private lazy var joinButton: UIButton = {
|
|
let button = OWSButton()
|
|
button.setTitleColor(.ows_white, for: .normal)
|
|
button.setBackgroundImage(UIImage(color: .ows_accentGreen), for: .normal)
|
|
button.titleLabel?.font = UIFont.ows_dynamicTypeBodyClamped.ows_semibold
|
|
button.clipsToBounds = true
|
|
button.layer.cornerRadius = 8
|
|
button.block = { [weak self] in
|
|
self?.delegate.didPressJoin(sender: button)
|
|
}
|
|
button.contentEdgeInsets = UIEdgeInsets(top: 11, leading: 11, bottom: 11, trailing: 11)
|
|
button.addSubview(joinButtonActivityIndicator)
|
|
button.setTitle(
|
|
NSLocalizedString(
|
|
"GROUP_CALL_IS_FULL",
|
|
comment: "Text explaining the group call is full"
|
|
),
|
|
for: .disabled
|
|
)
|
|
button.setTitleColor(.ows_whiteAlpha40, for: .disabled)
|
|
joinButtonActivityIndicator.autoCenterInSuperview()
|
|
return button
|
|
}()
|
|
|
|
private lazy var gradientView: UIView = {
|
|
let gradientLayer = CAGradientLayer()
|
|
gradientLayer.colors = [
|
|
UIColor.black.withAlphaComponent(0).cgColor,
|
|
UIColor.ows_blackAlpha60.cgColor
|
|
]
|
|
let view = OWSLayerView(frame: .zero) { view in
|
|
gradientLayer.frame = view.bounds
|
|
}
|
|
view.layer.addSublayer(gradientLayer)
|
|
return view
|
|
}()
|
|
|
|
private lazy var topStackView = createTopStackView()
|
|
private lazy var bottomStackView = createBottomStackView()
|
|
|
|
private weak var delegate: CallControlsDelegate!
|
|
private let call: SignalCall
|
|
|
|
init(call: SignalCall, delegate: CallControlsDelegate) {
|
|
self.call = call
|
|
self.delegate = delegate
|
|
super.init(frame: .zero)
|
|
|
|
call.addObserverAndSyncState(observer: self)
|
|
|
|
callService.audioService.delegate = self
|
|
|
|
addSubview(gradientView)
|
|
gradientView.autoPinEdgesToSuperviewEdges()
|
|
|
|
let controlsStack = UIStackView(arrangedSubviews: [topStackView, bottomStackView])
|
|
controlsStack.axis = .vertical
|
|
controlsStack.spacing = 40
|
|
|
|
addSubview(controlsStack)
|
|
controlsStack.autoPinWidthToSuperview()
|
|
controlsStack.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: 24)
|
|
controlsStack.autoPinEdge(toSuperviewEdge: .top, withInset: 22)
|
|
|
|
updateControls()
|
|
}
|
|
|
|
deinit {
|
|
call.removeObserver(self)
|
|
callService.audioService.delegate = nil
|
|
}
|
|
|
|
func createTopStackView() -> UIStackView {
|
|
let stackView = UIStackView()
|
|
stackView.axis = .horizontal
|
|
stackView.spacing = 16
|
|
|
|
let leadingSpacer = UIView.hStretchingSpacer()
|
|
let trailingSpacer = UIView.hStretchingSpacer()
|
|
|
|
stackView.addArrangedSubview(leadingSpacer)
|
|
stackView.addArrangedSubview(audioSourceButton)
|
|
stackView.addArrangedSubview(flipCameraButton)
|
|
stackView.addArrangedSubview(muteButton)
|
|
stackView.addArrangedSubview(videoButton)
|
|
stackView.addArrangedSubview(hangUpButton)
|
|
stackView.addArrangedSubview(trailingSpacer)
|
|
|
|
leadingSpacer.autoMatch(.width, to: .width, of: trailingSpacer)
|
|
|
|
return stackView
|
|
}
|
|
|
|
func createBottomStackView() -> UIStackView {
|
|
let stackView = UIStackView()
|
|
stackView.axis = .horizontal
|
|
stackView.spacing = 8
|
|
|
|
let leadingSpacer = UIView.hStretchingSpacer()
|
|
let trailingSpacer = UIView.hStretchingSpacer()
|
|
|
|
stackView.addArrangedSubview(leadingSpacer)
|
|
stackView.addArrangedSubview(cancelButton)
|
|
stackView.addArrangedSubview(joinButton)
|
|
stackView.addArrangedSubview(trailingSpacer)
|
|
|
|
// Prefer to be big.
|
|
NSLayoutConstraint.autoSetPriority(.defaultHigh) {
|
|
cancelButton.autoSetDimension(.width, toSize: 170)
|
|
}
|
|
|
|
cancelButton.autoMatch(.width, to: .width, of: joinButton)
|
|
leadingSpacer.autoMatch(.width, to: .width, of: trailingSpacer)
|
|
leadingSpacer.autoSetDimension(.width, toSize: 16, relation: .greaterThanOrEqual)
|
|
|
|
return stackView
|
|
}
|
|
|
|
private func updateControls() {
|
|
let hasExternalAudioInputs = callService.audioService.hasExternalInputs
|
|
let isLocalVideoMuted = call.groupCall.isOutgoingVideoMuted
|
|
|
|
flipCameraButton.isHidden = isLocalVideoMuted
|
|
videoButton.isSelected = !isLocalVideoMuted
|
|
muteButton.isSelected = call.groupCall.isOutgoingAudioMuted
|
|
hangUpButton.isHidden = call.groupCall.localDeviceState.joinState != .joined
|
|
|
|
// Use small controls if video is enabled and we have external
|
|
// audio inputs, because we have five buttons now.
|
|
[audioSourceButton, flipCameraButton, videoButton, muteButton, hangUpButton].forEach {
|
|
$0.isSmall = hasExternalAudioInputs && !isLocalVideoMuted
|
|
}
|
|
|
|
// Audio Source Handling
|
|
if hasExternalAudioInputs, let audioSource = callService.audioService.currentAudioSource {
|
|
audioSourceButton.showDropdownArrow = true
|
|
audioSourceButton.isHidden = false
|
|
|
|
if audioSource.isBuiltInEarPiece {
|
|
audioSourceButton.iconName = "phone-solid-28"
|
|
} else if audioSource.isBuiltInSpeaker {
|
|
audioSourceButton.iconName = "speaker-solid-28"
|
|
} else {
|
|
audioSourceButton.iconName = "speaker-bt-solid-28"
|
|
}
|
|
} else if UIDevice.current.isIPad {
|
|
// iPad *only* supports speaker mode, if there are no external
|
|
// devices connected, so we don't need to show the button unless
|
|
// we have alternate audio sources.
|
|
audioSourceButton.isHidden = true
|
|
} else {
|
|
// If there are no external audio sources, and video is enabled,
|
|
// speaker mode is always enabled so we don't need to show the button.
|
|
audioSourceButton.isHidden = !isLocalVideoMuted
|
|
|
|
// No bluetooth audio detected
|
|
audioSourceButton.iconName = "speaker-solid-28"
|
|
audioSourceButton.showDropdownArrow = false
|
|
}
|
|
|
|
bottomStackView.isHidden = call.groupCall.localDeviceState.joinState == .joined
|
|
|
|
let startCallText = NSLocalizedString("GROUP_CALL_START_BUTTON", comment: "Button to start a group call")
|
|
let joinCallText = NSLocalizedString("GROUP_CALL_JOIN_BUTTON", comment: "Button to join an ongoing group call")
|
|
|
|
if call.groupCall.isFull {
|
|
joinButton.isEnabled = false
|
|
} else if call.groupCall.localDeviceState.joinState == .joining {
|
|
joinButton.isEnabled = true
|
|
joinButton.isUserInteractionEnabled = false
|
|
joinButtonActivityIndicator.startAnimating()
|
|
|
|
joinButton.setTitle("", for: .normal)
|
|
} else {
|
|
joinButton.isEnabled = true
|
|
joinButton.isUserInteractionEnabled = true
|
|
joinButtonActivityIndicator.stopAnimating()
|
|
|
|
let deviceCount = call.groupCall.peekInfo?.deviceCount ?? 0
|
|
joinButton.setTitle(deviceCount == 0 ? startCallText : joinCallText, for: .normal)
|
|
}
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func createButton(iconName: String, action: Selector) -> CallButton {
|
|
let button = CallButton(iconName: iconName)
|
|
button.addTarget(delegate, action: action, for: .touchUpInside)
|
|
button.setContentHuggingHorizontalHigh()
|
|
button.setCompressionResistanceHorizontalLow()
|
|
button.alpha = 0.9
|
|
return button
|
|
}
|
|
}
|
|
|
|
extension CallControls: CallObserver {
|
|
func groupCallLocalDeviceStateChanged(_ call: SignalCall) {
|
|
owsAssertDebug(call.isGroupCall)
|
|
updateControls()
|
|
}
|
|
|
|
func groupCallPeekChanged(_ call: SignalCall) {
|
|
updateControls()
|
|
}
|
|
|
|
func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) {
|
|
updateControls()
|
|
}
|
|
|
|
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason) {
|
|
updateControls()
|
|
}
|
|
}
|
|
|
|
extension CallControls: CallAudioServiceDelegate {
|
|
func callAudioServiceDidChangeAudioSession(_ callAudioService: CallAudioService) {
|
|
updateControls()
|
|
}
|
|
|
|
func callAudioServiceDidChangeAudioSource(_ callAudioService: CallAudioService, audioSource: AudioSource?) {
|
|
updateControls()
|
|
}
|
|
}
|