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

227 lines
9.2 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalRingRTC
import SignalServiceKit
/// Only used for group calls currently; adjust if individual calls come to need it.
class CallMemberWaitingAndErrorView: UIView, CallMemberComposableView {
weak var errorPresenter: CallMemberErrorPresenter?
private let blurredAvatarBackgroundView = BlurredAvatarBackgroundView()
private let errorView = GroupCallErrorView()
private let spinner = UIActivityIndicatorView(style: .large)
private var deferredReconfigTimer: Timer?
private let type: CallMemberView.MemberType
var isCallMinimized: Bool = false {
didSet {
self.errorView.callMinimizedStateDidChange(isCallMinimized: isCallMinimized)
}
}
init(type: CallMemberView.MemberType) {
self.type = type
super.init(frame: .zero)
self.addSubview(blurredAvatarBackgroundView)
blurredAvatarBackgroundView.autoPinEdgesToSuperviewEdges()
blurredAvatarBackgroundView.isHidden = true
self.addSubview(errorView)
errorView.autoPinEdgesToSuperviewEdges()
errorView.isHidden = true
self.addSubview(spinner)
spinner.autoCenterInSuperview()
spinner.isHidden = true
}
func configure(
call: SignalCall,
isFullScreen: Bool = false,
remoteGroupMemberDeviceState: RemoteDeviceState?
) {
switch type {
case .local, .remoteInIndividual:
owsFailDebug("CallMemberWaitingAndErrorView should not be in the view hierarchy!")
case .remoteInGroup:
deferredReconfigTimer?.invalidate()
guard let remoteGroupMemberDeviceState else { return }
let ringRtcCall: SignalRingRTC.GroupCall
switch call.mode {
case .individual:
owsFail("Can't configure remoteInGroup for individual call.")
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
ringRtcCall = call.ringRtcCall
}
let isRemoteDeviceBlocked = SSKEnvironment.shared.databaseStorageRef.read { tx in
return SSKEnvironment.shared.blockingManagerRef.isAddressBlocked(remoteGroupMemberDeviceState.address, transaction: tx)
}
let errorDeferralInterval: TimeInterval = 5.0
let addedDate = Date(millisecondsSince1970: remoteGroupMemberDeviceState.addedTime)
let connectionDuration = -addedDate.timeIntervalSinceNow
if !remoteGroupMemberDeviceState.mediaKeysReceived, !isRemoteDeviceBlocked, connectionDuration < errorDeferralInterval {
// No media keys, but that's expected since we just joined the call.
// Schedule a timer to re-check and show a spinner in the meantime
spinner.isHidden = false
blurredAvatarBackgroundView.isHidden = false
if !spinner.isAnimating { spinner.startAnimating() }
let configuredDemuxId = remoteGroupMemberDeviceState.demuxId
let scheduledInterval = errorDeferralInterval - connectionDuration
deferredReconfigTimer = Timer.scheduledTimer(
withTimeInterval: scheduledInterval,
repeats: false,
block: { [weak self] _ in
guard let self = self else { return }
guard let updatedState = ringRtcCall.remoteDeviceStates.values
.first(where: { $0.demuxId == configuredDemuxId }) else { return }
self.configure(call: call, remoteGroupMemberDeviceState: updatedState)
}
)
} else if !remoteGroupMemberDeviceState.mediaKeysReceived {
// No media keys. Display error view
errorView.isHidden = false
if isRemoteDeviceBlocked {
configureErrorView(errorState: .blocked(remoteGroupMemberDeviceState.address))
} else {
configureErrorView(errorState: .noMediaKeys(remoteGroupMemberDeviceState.address))
}
blurredAvatarBackgroundView.isHidden = false
} else {
spinner.isHidden = true
errorView.isHidden = true
blurredAvatarBackgroundView.isHidden = true
}
}
self.blurredAvatarBackgroundView.update(
type: self.type,
remoteGroupMemberDeviceState: remoteGroupMemberDeviceState
)
}
private func displayName(address: SignalServiceAddress) -> String {
if address.isLocalAddress {
return OWSLocalizedString(
"GROUP_CALL_YOU_ON_ANOTHER_DEVICE",
comment: "Text describing the local user in the group call members sheet when connected from another device.")
} else {
return SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() }
}
}
private func configureErrorView(errorState: CallMemberErrorState) {
let label: String
let image: UIImage?
switch errorState {
case .blocked(let addr):
let blockFormat = OWSLocalizedString(
"GROUP_CALL_BLOCKED_USER_FORMAT",
comment: "String displayed in group call grid cell when a user is blocked. Embeds {user's name}"
)
let displayName = displayName(address: addr)
label = String(format: blockFormat, arguments: [displayName])
image = UIImage(named: "block")
case .noMediaKeys(let addr):
let missingKeyFormat = OWSLocalizedString(
"GROUP_CALL_MISSING_MEDIA_KEYS_FORMAT",
comment: "String displayed in cell when media from a user can't be displayed in group call grid. Embeds {user's name}"
)
let displayName = displayName(address: addr)
label = String(format: missingKeyFormat, arguments: [displayName])
image = UIImage(named: "error-circle-fill")
}
let (errorSheetTitle, errorSheetMessage) = errorSheetContents(errorState: errorState)
errorView.iconImage = image
errorView.labelText = label
errorView.userTapAction = { [weak self] _ in
guard let self = self else { return }
self.errorPresenter?.presentErrorSheet(
title: errorSheetTitle,
message: errorSheetMessage
)
}
}
private func errorSheetContents(errorState: CallMemberErrorState) -> (String, String) {
let title: String
let message: String
switch errorState {
case let .blocked(address):
message = OWSLocalizedString(
"GROUP_CALL_BLOCKED_ALERT_MESSAGE",
comment: "Message body for alert explaining that a group call participant is blocked")
let titleFormat = OWSLocalizedString(
"GROUP_CALL_BLOCKED_ALERT_TITLE_FORMAT",
comment: "Title for alert explaining that a group call participant is blocked. Embeds {{ user's name }}")
let displayName = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() }
title = String(format: titleFormat, displayName)
case let .noMediaKeys(address):
message = OWSLocalizedString(
"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 = OWSLocalizedString(
"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 = SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue() }
title = String(format: titleFormat, displayName)
}
return (title, message)
}
func rotateForPhoneOrientation(_ rotationAngle: CGFloat) {
/// TODO: Add support for rotating.
}
func updateDimensions() {
/// TODO: Add support for updating dimensions.
}
func clearConfiguration() {
deferredReconfigTimer?.invalidate()
errorView.isHidden = true
spinner.isHidden = true
blurredAvatarBackgroundView.isHidden = true
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self.errorView.button {
return view
}
return nil
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
enum CallMemberErrorState {
case blocked(SignalServiceAddress)
case noMediaKeys(SignalServiceAddress)
}
protocol CallMemberErrorPresenter: AnyObject {
func presentErrorSheet(title: String, message: String)
}