Video chat improvements

This commit is contained in:
Isaac 2024-10-01 22:10:11 +08:00
parent 776caeb8ae
commit 0e1998df1e
3 changed files with 116 additions and 28 deletions

View File

@ -11,6 +11,7 @@ import SwiftSignalKit
import MetalEngine import MetalEngine
import CallScreen import CallScreen
import AvatarNode import AvatarNode
import ContextUI
final class VideoChatParticipantThumbnailComponent: Component { final class VideoChatParticipantThumbnailComponent: Component {
let call: PresentationGroupCall let call: PresentationGroupCall
@ -21,6 +22,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
let isSpeaking: Bool let isSpeaking: Bool
let interfaceOrientation: UIInterfaceOrientation let interfaceOrientation: UIInterfaceOrientation
let action: (() -> Void)? let action: (() -> Void)?
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
init( init(
call: PresentationGroupCall, call: PresentationGroupCall,
@ -30,7 +32,8 @@ final class VideoChatParticipantThumbnailComponent: Component {
isSelected: Bool, isSelected: Bool,
isSpeaking: Bool, isSpeaking: Bool,
interfaceOrientation: UIInterfaceOrientation, interfaceOrientation: UIInterfaceOrientation,
action: (() -> Void)? action: (() -> Void)?,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
) { ) {
self.call = call self.call = call
self.theme = theme self.theme = theme
@ -40,6 +43,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.isSpeaking = isSpeaking self.isSpeaking = isSpeaking
self.interfaceOrientation = interfaceOrientation self.interfaceOrientation = interfaceOrientation
self.action = action self.action = action
self.contextAction = contextAction
} }
static func ==(lhs: VideoChatParticipantThumbnailComponent, rhs: VideoChatParticipantThumbnailComponent) -> Bool { static func ==(lhs: VideoChatParticipantThumbnailComponent, rhs: VideoChatParticipantThumbnailComponent) -> Bool {
@ -64,6 +68,12 @@ final class VideoChatParticipantThumbnailComponent: Component {
if lhs.interfaceOrientation != rhs.interfaceOrientation { if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false return false
} }
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true return true
} }
@ -79,7 +89,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
} }
} }
final class View: HighlightTrackingButton { final class View: ContextControllerSourceView {
private static let selectedBorderImage: UIImage? = { private static let selectedBorderImage: UIImage? = {
return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: UIColor.white, strokeWidth: 2.0)?.withRenderingMode(.alwaysTemplate) return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: UIColor.white, strokeWidth: 2.0)?.withRenderingMode(.alwaysTemplate)
}() }()
@ -88,6 +98,10 @@ final class VideoChatParticipantThumbnailComponent: Component {
private weak var componentState: EmptyComponentState? private weak var componentState: EmptyComponentState?
private var isUpdating: Bool = false private var isUpdating: Bool = false
private let extractedContainerView: ContextExtractedContentContainingView
private let backgroundLayer: SimpleLayer
private var avatarNode: AvatarNode? private var avatarNode: AvatarNode?
private let title = ComponentView<Empty>() private let title = ComponentView<Empty>()
private let muteStatus = ComponentView<Empty>() private let muteStatus = ComponentView<Empty>()
@ -101,13 +115,30 @@ final class VideoChatParticipantThumbnailComponent: Component {
private var videoSpec: VideoSpec? private var videoSpec: VideoSpec?
override init(frame: CGRect) { override init(frame: CGRect) {
self.extractedContainerView = ContextExtractedContentContainingView()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor(rgb: 0x1C1C1E).cgColor
super.init(frame: frame) super.init(frame: frame)
//TODO:release optimize self.addSubview(self.extractedContainerView)
self.clipsToBounds = true self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.layer.cornerRadius = 10.0
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.extractedContainerView.contentView.layer.addSublayer(self.backgroundLayer)
self.extractedContainerView.contentView.clipsToBounds = true
self.extractedContainerView.contentView.layer.cornerRadius = 10.0
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture)
}
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -118,11 +149,13 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.videoDisposable?.dispose() self.videoDisposable?.dispose()
} }
@objc private func pressed() { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
guard let component = self.component, let action = component.action else { if case .ended = recognizer.state {
return guard let component = self.component, let action = component.action else {
return
}
action()
} }
action()
} }
func update(component: VideoChatParticipantThumbnailComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize { func update(component: VideoChatParticipantThumbnailComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
@ -131,10 +164,6 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.isUpdating = false self.isUpdating = false
} }
if self.component == nil {
self.backgroundColor = UIColor(rgb: 0x1C1C1E)
}
let previousComponent = self.component let previousComponent = self.component
let wasSpeaking = previousComponent?.isSpeaking ?? false let wasSpeaking = previousComponent?.isSpeaking ?? false
@ -156,6 +185,14 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.component = component self.component = component
self.componentState = state self.componentState = state
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
let avatarNode: AvatarNode let avatarNode: AvatarNode
if let current = self.avatarNode { if let current = self.avatarNode {
avatarNode = current avatarNode = current
@ -163,7 +200,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
avatarNode.isUserInteractionEnabled = false avatarNode.isUserInteractionEnabled = false
self.avatarNode = avatarNode self.avatarNode = avatarNode
self.addSubview(avatarNode.view) self.extractedContainerView.contentView.addSubview(avatarNode.view)
} }
let avatarSize = CGSize(width: 50.0, height: 50.0) let avatarSize = CGSize(width: 50.0, height: 50.0)
@ -188,7 +225,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
let muteStatusFrame = CGRect(origin: CGPoint(x: availableSize.width + 5.0 - muteStatusSize.width, y: availableSize.height + 5.0 - muteStatusSize.height), size: muteStatusSize) let muteStatusFrame = CGRect(origin: CGPoint(x: availableSize.width + 5.0 - muteStatusSize.width, y: availableSize.height + 5.0 - muteStatusSize.height), size: muteStatusSize)
if let muteStatusView = self.muteStatus.view as? VideoChatMuteIconComponent.View { if let muteStatusView = self.muteStatus.view as? VideoChatMuteIconComponent.View {
if muteStatusView.superview == nil { if muteStatusView.superview == nil {
self.addSubview(muteStatusView) self.extractedContainerView.contentView.addSubview(muteStatusView)
} }
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
@ -208,7 +245,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
if titleView.superview == nil { if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint() titleView.layer.anchorPoint = CGPoint()
titleView.isUserInteractionEnabled = false titleView.isUserInteractionEnabled = false
self.addSubview(titleView) self.extractedContainerView.contentView.addSubview(titleView)
} }
transition.setPosition(view: titleView, position: titleFrame.origin) transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
@ -222,7 +259,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
videoBackgroundLayer = SimpleLayer() videoBackgroundLayer = SimpleLayer()
videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor
self.videoBackgroundLayer = videoBackgroundLayer self.videoBackgroundLayer = videoBackgroundLayer
self.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer) self.extractedContainerView.contentView.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer)
videoBackgroundLayer.isHidden = true videoBackgroundLayer.isHidden = true
} }
@ -232,8 +269,8 @@ final class VideoChatParticipantThumbnailComponent: Component {
} else { } else {
videoLayer = PrivateCallVideoLayer() videoLayer = PrivateCallVideoLayer()
self.videoLayer = videoLayer self.videoLayer = videoLayer
self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) self.extractedContainerView.contentView.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) self.extractedContainerView.contentView.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)
videoLayer.blurredLayer.opacity = 0.25 videoLayer.blurredLayer.opacity = 0.25
@ -346,7 +383,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
selectedBorderView = UIImageView() selectedBorderView = UIImageView()
self.selectedBorderView = selectedBorderView self.selectedBorderView = selectedBorderView
selectedBorderView.alpha = 0.0 selectedBorderView.alpha = 0.0
self.addSubview(selectedBorderView) self.extractedContainerView.contentView.addSubview(selectedBorderView)
selectedBorderView.image = View.selectedBorderImage selectedBorderView.image = View.selectedBorderImage
selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize) selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize)
@ -438,6 +475,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
let speakingParticipants: Set<EnginePeer.Id> let speakingParticipants: Set<EnginePeer.Id>
let interfaceOrientation: UIInterfaceOrientation let interfaceOrientation: UIInterfaceOrientation
let updateSelectedParticipant: (Participant.Key) -> Void let updateSelectedParticipant: (Participant.Key) -> Void
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
init( init(
call: PresentationGroupCall, call: PresentationGroupCall,
@ -446,7 +484,8 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
selectedParticipant: Participant.Key?, selectedParticipant: Participant.Key?,
speakingParticipants: Set<EnginePeer.Id>, speakingParticipants: Set<EnginePeer.Id>,
interfaceOrientation: UIInterfaceOrientation, interfaceOrientation: UIInterfaceOrientation,
updateSelectedParticipant: @escaping (Participant.Key) -> Void updateSelectedParticipant: @escaping (Participant.Key) -> Void,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
) { ) {
self.call = call self.call = call
self.theme = theme self.theme = theme
@ -455,6 +494,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
self.speakingParticipants = speakingParticipants self.speakingParticipants = speakingParticipants
self.interfaceOrientation = interfaceOrientation self.interfaceOrientation = interfaceOrientation
self.updateSelectedParticipant = updateSelectedParticipant self.updateSelectedParticipant = updateSelectedParticipant
self.contextAction = contextAction
} }
static func ==(lhs: VideoChatExpandedParticipantThumbnailsComponent, rhs: VideoChatExpandedParticipantThumbnailsComponent) -> Bool { static func ==(lhs: VideoChatExpandedParticipantThumbnailsComponent, rhs: VideoChatExpandedParticipantThumbnailsComponent) -> Bool {
@ -476,6 +516,9 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
if lhs.interfaceOrientation != rhs.interfaceOrientation { if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false return false
} }
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true return true
} }
@ -617,7 +660,8 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
return return
} }
component.updateSelectedParticipant(participantKey) component.updateSelectedParticipant(participantKey)
} },
contextAction: component.contextAction
)), )),
environment: {}, environment: {},
containerSize: itemFrame.size containerSize: itemFrame.size

View File

@ -205,7 +205,7 @@ final class VideoChatParticipantVideoComponent: Component {
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.extractedContainerView) self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.pinchContainerNode.view) self.extractedContainerView.contentView.addSubview(self.pinchContainerNode.view)
self.pinchContainerNode.contentNode.view.addSubview(self.backgroundGradientView) self.pinchContainerNode.contentNode.view.addSubview(self.backgroundGradientView)
@ -276,6 +276,8 @@ final class VideoChatParticipantVideoComponent: Component {
transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center) transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
transition.setFrame(view: self.pinchContainerNode.contentNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: self.pinchContainerNode.contentNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))

View File

@ -648,6 +648,8 @@ final class VideoChatParticipantsComponent: Component {
private var isPinchToZoomActive: Bool = false private var isPinchToZoomActive: Bool = false
private var stopRequestingNonCentralVideo: Bool = false
private var stopRequestingNonCentralVideoTimer: Foundation.Timer?
private var currentLoadMoreToken: String? private var currentLoadMoreToken: String?
private var mainScrollViewEventCycleState: EventCycleState? private var mainScrollViewEventCycleState: EventCycleState?
@ -722,6 +724,10 @@ final class VideoChatParticipantsComponent: Component {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
self.stopRequestingNonCentralVideoTimer?.invalidate()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let component = self.component else { guard let component = self.component else {
return nil return nil
@ -1381,6 +1387,12 @@ final class VideoChatParticipantsComponent: Component {
return return
} }
component.updateMainParticipant(VideoParticipantKey(id: key.id, isPresentation: key.isPresentation), nil) component.updateMainParticipant(VideoParticipantKey(id: key.id, isPresentation: key.isPresentation), nil)
},
contextAction: { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, sourceView, gesture)
} }
)), )),
environment: {}, environment: {},
@ -1599,9 +1611,33 @@ final class VideoChatParticipantsComponent: Component {
self.isUpdating = false self.isUpdating = false
} }
let previousComponent = self.component
self.component = component self.component = component
self.state = state self.state = state
if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden {
if self.stopRequestingNonCentralVideoTimer == nil || previousComponent?.expandedVideoState != expandedVideoState {
self.stopRequestingNonCentralVideoTimer?.invalidate()
self.stopRequestingNonCentralVideoTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.stopRequestingNonCentralVideo = true
self.stopRequestingNonCentralVideoTimer = nil
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
})
}
} else {
self.stopRequestingNonCentralVideo = false
if let stopRequestingNonCentralVideoTimer = self.stopRequestingNonCentralVideoTimer {
self.stopRequestingNonCentralVideoTimer = nil
stopRequestingNonCentralVideoTimer.invalidate()
}
}
let measureListItemSize = self.measureListItemView.update( let measureListItemSize = self.measureListItemView.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(PeerListItemComponent( component: AnyComponent(PeerListItemComponent(
@ -1749,13 +1785,19 @@ final class VideoChatParticipantsComponent: Component {
} }
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) { if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) {
if !requestedVideo.contains(videoChannel) { if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxVideoQuality != .full {
requestedVideo.append(videoChannel) } else {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
} }
} }
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) { if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) {
if !requestedVideo.contains(videoChannel) { if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxPresentationQuality != .full {
requestedVideo.append(videoChannel) } else {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
} }
} }
} }