Path: blob/main/Signal/Calls/UserInterface/GroupCallVideoGrid.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalRingRTC
import SignalServiceKit
import UIKit
class GroupCallVideoGrid: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource, GroupCallVideoGridLayoutDelegate, GroupCallObserver {
weak var memberViewErrorPresenter: CallMemberErrorPresenter?
let layout: GroupCallVideoGridLayout
let call: SignalCall
let groupCall: GroupCall
let ringRtcCall: SignalRingRTC.GroupCall
private var contactManager: ContactManager { SSKEnvironment.shared.contactManagerRef }
private var db: DB { DependenciesBridge.shared.db }
init(call: SignalCall, groupCall: GroupCall) {
self.call = call
self.groupCall = groupCall
self.ringRtcCall = groupCall.ringRtcCall
self.layout = GroupCallVideoGridLayout()
super.init(frame: .zero, collectionViewLayout: layout)
groupCall.addObserver(self, syncStateImmediately: true)
layout.delegate = self
backgroundColor = .clear
register(GroupCallVideoGridCell.self, forCellWithReuseIdentifier: GroupCallVideoGridCell.reuseIdentifier)
dataSource = self
delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let cell = cell as? GroupCallVideoGridCell else { return }
cell.cleanupVideoViews()
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let cell = cell as? GroupCallVideoGridCell else { return }
guard let remoteDevice = gridRemoteDeviceStates[safe: indexPath.row] else {
return owsFailDebug("missing member address")
}
cell.configureRemoteVideo(device: remoteDevice)
}
func collectionView(
_ collectionView: UICollectionView,
contextMenuConfigurationForItemsAt indexPaths: [IndexPath],
point: CGPoint,
) -> UIContextMenuConfiguration? {
guard
indexPaths.count == 1,
let indexPath = indexPaths.first,
let remoteDevice = gridRemoteDeviceStates[safe: indexPath.row]
else {
return nil
}
return GroupCallVideoContextMenuConfiguration.build(
call: call,
groupCall: groupCall,
ringRtcCall: ringRtcCall,
remoteDevice: remoteDevice,
interactionProvider: { [weak self] in
return self?.interactions
.compactMap({ $0 as? UIContextMenuInteraction })
.first
},
)
}
// MARK: - UICollectionViewDataSource
var gridRemoteDeviceStates: [RemoteDeviceState] {
let remoteDeviceStates = ringRtcCall.remoteDeviceStates.sortedBySpeakerTime
return Array(remoteDeviceStates.prefix(maxItems)).sortedByAddedTime
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return gridRemoteDeviceStates.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GroupCallVideoGridCell.reuseIdentifier,
for: indexPath,
) as! GroupCallVideoGridCell
guard let remoteDevice = gridRemoteDeviceStates[safe: indexPath.row] else {
owsFailDebug("missing member address")
return cell
}
cell.setMemberViewErrorPresenter(memberViewErrorPresenter)
cell.configure(call: call, device: remoteDevice)
return cell
}
// MARK: - GroupCallObserver
func groupCallRemoteDeviceStatesChanged(_ call: GroupCall) {
AssertIsOnMainThread()
reloadData()
}
func groupCallPeekChanged(_ call: GroupCall) {
AssertIsOnMainThread()
reloadData()
}
func groupCallEnded(_ call: GroupCall, reason: CallEndReason) {
AssertIsOnMainThread()
reloadData()
}
func groupCallReceivedRaisedHands(_ call: GroupCall, raisedHands: [DemuxId]) {
AssertIsOnMainThread()
reloadData()
}
// MARK: - GroupCallVideoGridLayoutDelegate
var maxColumns: Int {
if CurrentAppContext().frame.width > 1080 {
return 4
} else if CurrentAppContext().frame.width > 768 {
return 3
} else {
return 2
}
}
var maxRows: Int {
if CurrentAppContext().frame.height > 1024 {
return 4
} else {
return 3
}
}
var maxItems: Int { maxColumns * maxRows }
}
// MARK: -
private class GroupCallVideoGridCell: UICollectionViewCell {
static let reuseIdentifier = "GroupCallVideoGridCell"
private let memberView: CallMemberView
override init(frame: CGRect) {
let type = CallMemberView.MemberType.remoteInGroup(.videoGrid)
memberView = CallMemberView(type: type)
super.init(frame: frame)
memberView.applyChangesToCallMemberViewAndVideoView { view in
contentView.addSubview(view)
view.autoPinEdgesToSuperviewEdges()
}
contentView.layer.cornerRadius = 10
contentView.clipsToBounds = true
}
func configure(call: SignalCall, device: RemoteDeviceState) {
memberView.configure(call: call, remoteGroupMemberDeviceState: device)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func cleanupVideoViews() {
memberView.cleanupVideoViews()
}
func configureRemoteVideo(device: RemoteDeviceState) {
memberView.configureRemoteVideo(device: device, context: .videoGrid)
}
func setMemberViewErrorPresenter(_ errorPresenter: CallMemberErrorPresenter?) {
memberView.errorPresenter = errorPresenter
}
}
// MARK: -
extension Sequence where Element: RemoteDeviceState {
/// The first person to join the call is the first item in the list.
/// Members that are presenting are always put at the top of the list.
var sortedByAddedTime: [RemoteDeviceState] {
return sorted { lhs, rhs in
if lhs.presenting ?? false != rhs.presenting ?? false {
return lhs.presenting ?? false
} else if lhs.mediaKeysReceived != rhs.mediaKeysReceived {
return lhs.mediaKeysReceived
} else if lhs.addedTime != rhs.addedTime {
return lhs.addedTime < rhs.addedTime
} else {
return lhs.demuxId < rhs.demuxId
}
}
}
/// The most recent speaker is the first item in the list.
/// Members that are presenting are always put at the top of the list.
var sortedBySpeakerTime: [RemoteDeviceState] {
return sorted { lhs, rhs in
if lhs.presenting ?? false != rhs.presenting ?? false {
return lhs.presenting ?? false
} else if lhs.mediaKeysReceived != rhs.mediaKeysReceived {
return lhs.mediaKeysReceived
} else if lhs.speakerTime != rhs.speakerTime {
return lhs.speakerTime > rhs.speakerTime
} else {
return lhs.demuxId < rhs.demuxId
}
}
}
}
// MARK: -
extension Dictionary where Value: RemoteDeviceState {
/// The first person to join the call is the first item in the list.
var sortedByAddedTime: [RemoteDeviceState] {
return values.sortedByAddedTime
}
/// The most recent speaker is the first item in the list.
var sortedBySpeakerTime: [RemoteDeviceState] {
return values.sortedBySpeakerTime
}
}