TM-SGNL-iOS/Signal/Calls/UserInterface/CallMemberChromeOverlayView.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

363 lines
14 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalRingRTC
import SignalServiceKit
import SignalUI
class CallMemberChromeOverlayView: UIView, CallMemberComposableView {
private var call: SignalCall?
private var type: CallMemberView.MemberType
private var callService: CallService { AppEnvironment.shared.callService }
init(type: CallMemberView.MemberType) {
self.type = type
switch type {
case .local, .remoteInIndividual:
self.raisedHandView = nil
case .remoteInGroup(let callMemberVisualContext):
switch callMemberVisualContext {
case .videoGrid, .speaker:
self.raisedHandView = RaisedHandView(useCompactSize: false)
case .videoOverflow:
self.raisedHandView = RaisedHandView(useCompactSize: true)
}
}
super.init(frame: .zero)
self.addLayoutGuide(layoutGuide)
layoutGuideConstraints.append(layoutGuide.topAnchor.constraint(equalTo: self.topAnchor, constant: inset))
layoutGuideConstraints.append(layoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -inset))
layoutGuideConstraints.append(layoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: inset))
layoutGuideConstraints.append(layoutGuide.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -inset))
NSLayoutConstraint.activate(layoutGuideConstraints)
muteIndicatorCircleView.isHidden = true
muteIndicatorCircleView.backgroundColor = .ows_blackAlpha70
let muteIndicatorImage = UIImageView()
muteIndicatorImage.setTemplateImageName("mic-slash-fill-28", tintColor: .ows_white)
muteIndicatorCircleView.addSubview(muteIndicatorImage)
addSubview(muteIndicatorCircleView)
muteIndicatorCircleView.autoSetDimension(.height, toSize: Constants.muteImageCircleDimension)
muteIndicatorCircleView.autoMatch(.width, to: .height, of: muteIndicatorCircleView)
muteIndicatorImage.autoSetDimension(.height, toSize: Constants.muteImageDimension)
muteIndicatorImage.autoMatch(.width, to: .height, of: muteIndicatorImage)
muteIndicatorImage.autoCenterInSuperview()
NSLayoutConstraint.activate([
muteIndicatorCircleView.leadingAnchor.constraint(equalTo: self.layoutGuide.leadingAnchor),
muteIndicatorCircleView.bottomAnchor.constraint(equalTo: self.layoutGuide.bottomAnchor)
])
if let raisedHandView {
raisedHandView.isHidden = true
self.addSubview(raisedHandView)
raisedHandView.autoPinEdge(toSuperviewMargin: .top)
raisedHandView.autoPinEdge(toSuperviewMargin: .leading)
raisedHandView.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual)
}
switch type {
case .local:
addSubview(flipCameraButton)
NSLayoutConstraint.activate([
flipCameraButton.trailingAnchor.constraint(equalTo: self.layoutGuide.trailingAnchor),
flipCameraButton.bottomAnchor.constraint(equalTo: self.layoutGuide.bottomAnchor)
])
updateFlipCameraButton()
case .remoteInGroup, .remoteInIndividual:
break
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self.flipCameraButton {
return view
}
return nil
}
func rotateForPhoneOrientation(_ rotationAngle: CGFloat) {
/// TODO: Add support for rotating other elements too.
self.muteIndicatorCircleView.transform = CGAffineTransform(rotationAngle: rotationAngle)
updateFlipCameraButton()
updateLayoutGuideConstraints()
}
func configure(
call: SignalCall,
isFullScreen: Bool = false,
remoteGroupMemberDeviceState: RemoteDeviceState?
) {
self.call = call
updateFlipCameraButton()
updateMuteIndicatorHiddenState(
call: call,
isFullScreen: isFullScreen,
remoteGroupMemberDeviceState: remoteGroupMemberDeviceState
)
updateRaisedHand(
call: call,
remoteGroupMemberDeviceState: remoteGroupMemberDeviceState
)
}
func updateDimensions() {
updateFlipCameraButton()
updateLayoutGuideConstraints()
}
func clearConfiguration() {
muteIndicatorCircleView.isHidden = true
}
// MARK: - General Layout
private let layoutGuide = UILayoutGuide()
private var layoutGuideConstraints = [NSLayoutConstraint]()
enum Constants {
// For example, for expanded pip on iPhone in 1:1 call.
fileprivate static let expandedPipMinWidth = CallMemberView.Constants.enlargedPipWidth
// For example, for non-expanded pip on iPad in group call of 3 members total.
fileprivate static let mediumPipMinWidth: CGFloat = 72
fileprivate static let expandedPipInset: CGFloat = 8
fileprivate static let normalPipInset: CGFloat = 4
fileprivate static let flipCameraButtonDimensionWhenPipExpanded: CGFloat = 48
fileprivate static let flipCameraButtonDimensionWhenPipNormal: CGFloat = 28
fileprivate static let flipCameraImageDimensionWhenPipExpanded: CGFloat = 24
fileprivate static let flipCameraImageDimensionWhenPipNormal: CGFloat = 16
fileprivate static let muteImageDimension: CGFloat = 16
fileprivate static let muteImageCircleDimension: CGFloat = 28
}
private var inset: CGFloat {
return width >= Constants.expandedPipMinWidth ? Constants.expandedPipInset : Constants.normalPipInset
}
private func updateLayoutGuideConstraints() {
self.layoutGuideConstraints.forEach {
$0.constant = $0.constant/abs($0.constant) * inset
}
self.layoutMargins = .init(margin: self.inset)
}
// MARK: - Raised hand
private let raisedHandView: RaisedHandView?
private func updateRaisedHand(
call: SignalCall,
remoteGroupMemberDeviceState: RemoteDeviceState?
) {
guard let raisedHandView else { return }
guard
let deviceState = remoteGroupMemberDeviceState,
case .groupThread(let groupThreadCall) = call.mode,
deviceState.demuxId != groupThreadCall.ringRtcCall.localDeviceState.demuxId,
groupThreadCall.raisedHands.contains(deviceState.demuxId),
!AppEnvironment.shared.windowManagerRef.isCallInPip
else {
raisedHandView.isHidden = true
return
}
raisedHandView.name = SSKEnvironment.shared.databaseStorageRef.read { tx in
let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)
if deviceState.aci == localIdentifiers?.aci {
return CommonStrings.you
} else {
return SSKEnvironment.shared.contactManagerRef.displayName(for: deviceState.address, tx: tx).resolvedValue(useShortNameIfAvailable: true)
}
}
raisedHandView.isHidden = false
}
private class RaisedHandView: UIStackView {
private var useCompactSize: Bool
private var circleSize: CGFloat {
useCompactSize ? 28 : 40
}
private var iconSize: CGFloat {
useCompactSize ? 16 : 24
}
var name: String? {
didSet {
nameLabel.text = name
}
}
private let nameLabel = UILabel()
init(useCompactSize: Bool) {
self.useCompactSize = useCompactSize
super.init(frame: .zero)
self.axis = .horizontal
self.spacing = 8
let iconBackground = UIView()
self.addArrangedSubview(iconBackground)
iconBackground.autoSetDimensions(to: .square(self.circleSize))
iconBackground.backgroundColor = .ows_white
iconBackground.layer.cornerRadius = self.circleSize / 2
let iconView = UIImageView(image: Theme.iconImage(.raiseHand))
iconBackground.addSubview(iconView)
iconView.autoSetDimensions(to: .square(self.iconSize))
iconView.autoCenterInSuperview()
iconView.tintColor = .ows_black
if !useCompactSize {
self.addArrangedSubview(nameLabel)
nameLabel.textColor = .ows_white
nameLabel.font = .dynamicTypeBody2
}
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Mute Button
private let muteIndicatorImage = UIImageView()
private let muteIndicatorCircleView = CircleView()
private func updateMuteIndicatorHiddenState(
call: SignalCall,
isFullScreen: Bool,
remoteGroupMemberDeviceState: RemoteDeviceState?
) {
switch type {
case .local:
muteIndicatorCircleView.isHidden = !call.isOutgoingAudioMuted || isFullScreen
case .remoteInGroup(let context):
muteIndicatorCircleView.isHidden = context == .speaker || remoteGroupMemberDeviceState?.audioMuted != true || isFullScreen
case .remoteInIndividual:
muteIndicatorCircleView.isHidden = true
}
}
// MARK: - Flip camera button
private var flipCameraButtonWidthConstraint: NSLayoutConstraint?
private var flipCameraImageWidthConstraint: NSLayoutConstraint?
private lazy var flipCameraCircleView: CircleBlurView = {
let circleView = CircleBlurView(effect: UIBlurEffect(style: .systemThinMaterialDark))
circleView.isUserInteractionEnabled = false
return circleView
}()
private lazy var flipCameraImageView = {
let imageView = UIImageView(image: UIImage(named: "switch-camera-28"))
imageView.isUserInteractionEnabled = false
imageView.tintColor = .ows_white
flipCameraImageWidthConstraint = imageView.autoSetDimension(
.height,
toSize: Constants.flipCameraImageDimensionWhenPipNormal
)
imageView.autoMatch(.width, to: .height, of: imageView)
return imageView
}()
private lazy var flipCameraButton: UIButton = {
flipCameraCircleView.contentView.addSubview(flipCameraImageView)
flipCameraImageView.autoCenterInSuperview()
let button = UIButton()
button.addSubview(flipCameraCircleView)
flipCameraCircleView.autoPinEdgesToSuperviewEdges()
flipCameraButtonWidthConstraint = button.autoSetDimension(
.height,
toSize: Constants.flipCameraButtonDimensionWhenPipNormal
)
button.autoMatch(.width, to: .height, of: button)
button.accessibilityLabel = flipCameraButtonAccessibilityLabel
button.addTarget(self, action: #selector(didPressFlipCamera), for: .touchUpInside)
flipCameraCircleView.backgroundColor = .ows_blackAlpha70
button.isHidden = true
return button
}()
@objc
private func didPressFlipCamera() {
guard let call else { return }
if let isUsingFrontCamera = call.videoCaptureController.isUsingFrontCamera {
callService.updateCameraSource(call: call, isUsingFrontCamera: !isUsingFrontCamera)
}
}
private var flipCameraButtonAccessibilityLabel: String {
return OWSLocalizedString(
"CALL_VIEW_SWITCH_CAMERA_DIRECTION",
comment: "Accessibility label to toggle front- vs. rear-facing camera"
)
}
private func updateFlipCameraButton() {
switch type {
case .local:
break
case .remoteInGroup, .remoteInIndividual:
// Flip camera is only for the local user.
return
}
guard let call else { return }
if width > CallMemberView.Constants.enlargedPipWidthIpadLandscape {
// This is the widest width the pip can be. If we're wider, it
// means that the local user is fullscreen (or animating from
// fullscreen down to the pip) and therefore should
// not show the flip camera button.
self.flipCameraButton.isHidden = true
} else if width >= Constants.expandedPipMinWidth {
// Pip is expanded or at its regular size for iPad in certain cases.
self.flipCameraButton.isEnabled = true
flipCameraCircleView.backgroundColor = .ows_whiteAlpha40
self.flipCameraButtonWidthConstraint?.constant = Constants.flipCameraButtonDimensionWhenPipExpanded
self.flipCameraImageWidthConstraint?.constant = Constants.flipCameraImageDimensionWhenPipExpanded
animateFlipCameraButtonAlphaIfNecessary(
call: call,
newIsHidden: call.isOutgoingVideoMuted
)
} else if width >= Constants.mediumPipMinWidth {
flipCameraButton.isEnabled = false
flipCameraCircleView.backgroundColor = .ows_blackAlpha70
self.flipCameraButtonWidthConstraint?.constant = Constants.flipCameraButtonDimensionWhenPipNormal
self.flipCameraImageWidthConstraint?.constant = Constants.flipCameraImageDimensionWhenPipNormal
animateFlipCameraButtonAlphaIfNecessary(
call: call,
newIsHidden: call.isOutgoingVideoMuted
)
} else {
animateFlipCameraButtonAlphaIfNecessary(
call: call,
newIsHidden: true
)
}
}
private func animateFlipCameraButtonAlphaIfNecessary(call: SignalCall, newIsHidden: Bool) {
guard newIsHidden != self.flipCameraButton.isHidden else { return }
self.flipCameraButton.alpha = newIsHidden ? 1 : 0
UIView.animate(withDuration: 0.3, animations: {
self.flipCameraButton.alpha = newIsHidden ? 0 : 1
}, completion: { [weak self] _ in
self?.flipCameraButton.isHidden = newIsHidden
}
)
}
// MARK: - Required
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}