session-ios/Session/Calls/UserInterface/Group/GroupCallMemberSheet.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")
}
}