351 lines
13 KiB
Swift
351 lines
13 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalRingRTC
|
|
|
|
@objc
|
|
class GroupCallMemberSheet: InteractiveSheetViewController {
|
|
override var interactiveScrollViews: [UIScrollView] { [tableView] }
|
|
let tableView = UITableView(frame: .zero, style: .grouped)
|
|
let call: SignalCall
|
|
|
|
init(call: SignalCall) {
|
|
self.call = call
|
|
super.init()
|
|
call.addObserverAndSyncState(observer: self)
|
|
}
|
|
|
|
public required init() {
|
|
fatalError("init() has not been implemented")
|
|
}
|
|
|
|
deinit { call.removeObserver(self) }
|
|
|
|
// MARK: -
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
if UIAccessibility.isReduceTransparencyEnabled {
|
|
contentView.backgroundColor = .ows_blackAlpha80
|
|
} else {
|
|
let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
|
contentView.addSubview(blurEffectView)
|
|
blurEffectView.autoPinEdgesToSuperviewEdges()
|
|
contentView.backgroundColor = .ows_blackAlpha40
|
|
}
|
|
|
|
tableView.dataSource = self
|
|
tableView.delegate = self
|
|
tableView.backgroundColor = .clear
|
|
tableView.separatorStyle = .none
|
|
tableView.tableHeaderView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 0, height: CGFloat.leastNormalMagnitude)))
|
|
contentView.addSubview(tableView)
|
|
tableView.autoPinEdgesToSuperviewEdges()
|
|
|
|
tableView.register(GroupCallMemberCell.self, forCellReuseIdentifier: GroupCallMemberCell.reuseIdentifier)
|
|
tableView.register(GroupCallEmptyCell.self, forCellReuseIdentifier: GroupCallEmptyCell.reuseIdentifier)
|
|
|
|
updateMembers()
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
struct JoinedMember {
|
|
let address: SignalServiceAddress
|
|
let displayName: String
|
|
let comparableName: String
|
|
let isAudioMuted: Bool?
|
|
let isVideoMuted: Bool?
|
|
let isPresenting: Bool?
|
|
}
|
|
|
|
private var sortedMembers = [JoinedMember]()
|
|
func updateMembers() {
|
|
let unsortedMembers: [JoinedMember] = databaseStorage.read { transaction in
|
|
var members = [JoinedMember]()
|
|
|
|
if self.call.groupCall.localDeviceState.joinState == .joined {
|
|
members += self.call.groupCall.remoteDeviceStates.values.map { member in
|
|
let displayName: String
|
|
let comparableName: String
|
|
if member.address.isLocalAddress {
|
|
displayName = NSLocalizedString(
|
|
"GROUP_CALL_YOU_ON_ANOTHER_DEVICE",
|
|
comment: "Text describing the local user in the group call members sheet when connected from another device."
|
|
)
|
|
comparableName = displayName
|
|
} else {
|
|
displayName = self.contactsManager.displayName(for: member.address, transaction: transaction)
|
|
comparableName = self.contactsManager.comparableName(for: member.address, transaction: transaction)
|
|
}
|
|
|
|
return JoinedMember(
|
|
address: member.address,
|
|
displayName: displayName,
|
|
comparableName: comparableName,
|
|
isAudioMuted: member.audioMuted,
|
|
isVideoMuted: member.videoMuted,
|
|
isPresenting: member.presenting
|
|
)
|
|
}
|
|
|
|
guard let localAddress = self.tsAccountManager.localAddress else { return members }
|
|
|
|
let displayName = NSLocalizedString(
|
|
"GROUP_CALL_YOU",
|
|
comment: "Text describing the local user as a participant in a group call."
|
|
)
|
|
let comparableName = displayName
|
|
|
|
members.append(JoinedMember(
|
|
address: localAddress,
|
|
displayName: displayName,
|
|
comparableName: comparableName,
|
|
isAudioMuted: self.call.groupCall.isOutgoingAudioMuted,
|
|
isVideoMuted: self.call.groupCall.isOutgoingVideoMuted,
|
|
isPresenting: false
|
|
))
|
|
} else {
|
|
// If we're not yet in the call, `remoteDeviceStates` will not exist.
|
|
// We can get the list of joined members still, provided we are connected.
|
|
members += self.call.groupCall.peekInfo?.joinedMembers.map { uuid in
|
|
let address = SignalServiceAddress(uuid: uuid)
|
|
let displayName = self.contactsManager.displayName(for: address, transaction: transaction)
|
|
let comparableName = self.contactsManager.comparableName(for: address, transaction: transaction)
|
|
|
|
return JoinedMember(
|
|
address: address,
|
|
displayName: displayName,
|
|
comparableName: comparableName,
|
|
isAudioMuted: nil,
|
|
isVideoMuted: nil,
|
|
isPresenting: nil
|
|
)
|
|
} ?? []
|
|
}
|
|
|
|
return members
|
|
}
|
|
|
|
sortedMembers = unsortedMembers.sorted { $0.comparableName.caseInsensitiveCompare($1.comparableName) == .orderedAscending }
|
|
|
|
tableView.reloadData()
|
|
}
|
|
}
|
|
|
|
extension GroupCallMemberSheet: UITableViewDataSource, UITableViewDelegate {
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return sortedMembers.count > 0 ? sortedMembers.count : 1
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
guard !sortedMembers.isEmpty else {
|
|
return tableView.dequeueReusableCell(withIdentifier: GroupCallEmptyCell.reuseIdentifier, for: indexPath)
|
|
}
|
|
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: GroupCallMemberCell.reuseIdentifier, for: indexPath)
|
|
|
|
guard let memberCell = cell as? GroupCallMemberCell else {
|
|
owsFailDebug("unexpected cell type")
|
|
return cell
|
|
}
|
|
|
|
guard let member = sortedMembers[safe: indexPath.row] else {
|
|
owsFailDebug("missing member")
|
|
return cell
|
|
}
|
|
|
|
memberCell.configure(item: member)
|
|
|
|
return memberCell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
|
let label = UILabel()
|
|
label.font = UIFont.ows_dynamicTypeSubheadlineClamped.ows_semibold
|
|
label.textColor = Theme.darkThemePrimaryColor
|
|
|
|
if sortedMembers.count > 1 {
|
|
let formatString = NSLocalizedString(
|
|
"GROUP_CALL_MANY_IN_THIS_CALL_FORMAT",
|
|
comment: "String indicating how many people are current in the call"
|
|
)
|
|
label.text = String(format: formatString, sortedMembers.count)
|
|
} else if sortedMembers.count > 0 {
|
|
label.text = NSLocalizedString(
|
|
"GROUP_CALL_ONE_IN_THIS_CALL",
|
|
comment: "String indicating one person is currently in the call"
|
|
)
|
|
} else {
|
|
label.text = nil
|
|
}
|
|
|
|
let labelContainer = UIView()
|
|
labelContainer.layoutMargins = UIEdgeInsets(top: 13, left: 16, bottom: 13, right: 16)
|
|
labelContainer.addSubview(label)
|
|
label.autoPinEdgesToSuperviewMargins()
|
|
return labelContainer
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
|
return UITableView.automaticDimension
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
|
return .leastNormalMagnitude
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension GroupCallMemberSheet: CallObserver {
|
|
func groupCallLocalDeviceStateChanged(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(call.isGroupCall)
|
|
|
|
updateMembers()
|
|
}
|
|
|
|
func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(call.isGroupCall)
|
|
|
|
updateMembers()
|
|
}
|
|
|
|
func groupCallPeekChanged(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(call.isGroupCall)
|
|
|
|
updateMembers()
|
|
}
|
|
|
|
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(call.isGroupCall)
|
|
|
|
updateMembers()
|
|
}
|
|
}
|
|
|
|
private class GroupCallMemberCell: UITableViewCell {
|
|
static let reuseIdentifier = "GroupCallMemberCell"
|
|
|
|
let avatarView = ConversationAvatarView(diameterPoints: 36,
|
|
localUserDisplayMode: .asUser)
|
|
let nameLabel = UILabel()
|
|
let videoMutedIndicator = UIImageView()
|
|
let audioMutedIndicator = UIImageView()
|
|
let presentingIndicator = UIImageView()
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
|
|
backgroundColor = .clear
|
|
selectionStyle = .none
|
|
|
|
layoutMargins = UIEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
|
|
|
|
avatarView.autoSetDimensions(to: CGSize(square: 36))
|
|
|
|
nameLabel.font = .ows_dynamicTypeBody
|
|
|
|
audioMutedIndicator.contentMode = .scaleAspectFit
|
|
audioMutedIndicator.setTemplateImage(#imageLiteral(resourceName: "mic-off-solid-28"), tintColor: .ows_white)
|
|
audioMutedIndicator.autoSetDimensions(to: CGSize(square: 16))
|
|
audioMutedIndicator.setContentHuggingHorizontalHigh()
|
|
let audioMutedWrapper = UIView()
|
|
audioMutedWrapper.addSubview(audioMutedIndicator)
|
|
audioMutedIndicator.autoPinEdgesToSuperviewEdges()
|
|
|
|
videoMutedIndicator.contentMode = .scaleAspectFit
|
|
videoMutedIndicator.setTemplateImage(#imageLiteral(resourceName: "video-off-solid-28"), tintColor: .ows_white)
|
|
videoMutedIndicator.autoSetDimensions(to: CGSize(square: 16))
|
|
videoMutedIndicator.setContentHuggingHorizontalHigh()
|
|
|
|
presentingIndicator.contentMode = .scaleAspectFit
|
|
presentingIndicator.setTemplateImage(#imageLiteral(resourceName: "share-screen-solid-28"), tintColor: .ows_white)
|
|
presentingIndicator.autoSetDimensions(to: CGSize(square: 16))
|
|
presentingIndicator.setContentHuggingHorizontalHigh()
|
|
|
|
// We share a wrapper for video muted and presenting states
|
|
// as they render in the same column.
|
|
let videoMutedAndPresentingWrapper = UIView()
|
|
videoMutedAndPresentingWrapper.addSubview(videoMutedIndicator)
|
|
videoMutedIndicator.autoPinEdgesToSuperviewEdges()
|
|
|
|
videoMutedAndPresentingWrapper.addSubview(presentingIndicator)
|
|
presentingIndicator.autoPinEdgesToSuperviewEdges()
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [
|
|
avatarView,
|
|
UIView.spacer(withWidth: 8),
|
|
nameLabel,
|
|
UIView.spacer(withWidth: 16),
|
|
videoMutedAndPresentingWrapper,
|
|
UIView.spacer(withWidth: 16),
|
|
audioMutedWrapper
|
|
])
|
|
stackView.axis = .horizontal
|
|
stackView.alignment = .center
|
|
contentView.addSubview(stackView)
|
|
stackView.autoPinEdgesToSuperviewMargins()
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configure(item: GroupCallMemberSheet.JoinedMember) {
|
|
nameLabel.textColor = Theme.darkThemePrimaryColor
|
|
|
|
videoMutedIndicator.isHidden = item.isVideoMuted != true || item.isPresenting == true
|
|
audioMutedIndicator.isHidden = item.isAudioMuted != true
|
|
presentingIndicator.isHidden = item.isPresenting != true
|
|
|
|
nameLabel.text = item.displayName
|
|
|
|
avatarView.configureWithSneakyTransaction(address: item.address)
|
|
}
|
|
}
|
|
|
|
private class GroupCallEmptyCell: UITableViewCell {
|
|
static let reuseIdentifier = "GroupCallEmptyCell"
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
|
|
backgroundColor = .clear
|
|
selectionStyle = .none
|
|
|
|
layoutMargins = UIEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
|
|
|
|
let imageView = UIImageView(image: #imageLiteral(resourceName: "sad-cat"))
|
|
imageView.contentMode = .scaleAspectFit
|
|
contentView.addSubview(imageView)
|
|
imageView.autoSetDimensions(to: CGSize(square: 160))
|
|
imageView.autoHCenterInSuperview()
|
|
imageView.autoPinTopToSuperviewMargin(withInset: 32)
|
|
|
|
let label = UILabel()
|
|
label.font = .ows_dynamicTypeSubheadlineClamped
|
|
label.textColor = Theme.darkThemePrimaryColor
|
|
label.text = NSLocalizedString("GROUP_CALL_NOBODY_IS_IN_YET",
|
|
comment: "Text explaining to the user that nobody has joined this call yet.")
|
|
label.numberOfLines = 0
|
|
label.lineBreakMode = .byWordWrapping
|
|
label.textAlignment = .center
|
|
contentView.addSubview(label)
|
|
label.autoPinWidthToSuperviewMargins()
|
|
label.autoPinBottomToSuperviewMargin()
|
|
label.autoPinEdge(.top, to: .bottom, of: imageView, withOffset: 16)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|