[WIP] Video chat screen V2

This commit is contained in:
Isaac 2024-08-23 19:37:33 +08:00
parent 67ed88e951
commit 33e78cd5ed
19 changed files with 1927 additions and 398 deletions

View File

@ -273,17 +273,19 @@ open class ViewControllerComponentContainer: ViewController {
theme = theme.withModalBlocksBackground()
resolvedTheme = resolvedTheme.withModalBlocksBackground()
}
let presentationData = presentationData.withUpdated(theme: theme)
strongSelf.node.presentationData = presentationData.withUpdated(theme: theme)
strongSelf.node.presentationData = presentationData
strongSelf.node.resolvedTheme = resolvedTheme
switch statusBarStyle {
case .none:
strongSelf.statusBar.statusBarStyle = .Hide
case .ignore:
strongSelf.statusBar.statusBarStyle = .Ignore
case .default:
strongSelf.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
strongSelf.statusBar.statusBarStyle = resolvedTheme.rootController.statusBarStyle.style
}
let navigationBarPresentationData: NavigationBarPresentationData?
@ -305,13 +307,14 @@ open class ViewControllerComponentContainer: ViewController {
}
}).strict()
let resolvedTheme = resolveTheme(baseTheme: presentationData.theme, theme: self.theme)
switch statusBarStyle {
case .none:
self.statusBar.statusBarStyle = .Hide
case .ignore:
self.statusBar.statusBarStyle = .Ignore
case .default:
self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
self.statusBar.statusBarStyle = resolvedTheme.rootController.statusBarStyle.style
}
}

View File

@ -236,7 +236,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
sideInset: 0.0,
title: peer.compactDisplayTitle,
peer: peer,
subtitle: subtitle,
subtitle: PeerListItemComponent.Subtitle(text: subtitle, color: .neutral),
subtitleAccessory: .none,
presence: nil,
selectionState: hasSelection ? .editing(isSelected: state.selectedSlots.contains(boost.slot), isTinted: false) : .none,

View File

@ -112,6 +112,8 @@ swift_library(
"//submodules/MetalEngine",
"//submodules/TelegramUI/Components/Calls/VoiceChatActionButton",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
],
visibility = [
"//visibility:public",

View File

@ -408,7 +408,7 @@ private extension CurrentImpl {
}
}
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError>? {
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
switch self {
case let .call(callContext):
return callContext.video(endpointId: endpointId)
@ -663,6 +663,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
private var ssrcMapping: [UInt32: SsrcMapping] = [:]
private var requestedVideoChannels: [OngoingGroupCallContext.VideoChannel] = []
private var pendingVideoSubscribers = Bag<(String, MetaDisposable, (OngoingGroupCallContext.VideoFrameData) -> Void)>()
private var summaryInfoState = Promise<SummaryInfoState?>(nil)
private var summaryParticipantsState = Promise<SummaryParticipantsState?>(nil)
@ -1695,6 +1698,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.genericCallContext = genericCallContext
self.stateVersionValue += 1
genericCallContext.setRequestedVideoChannels(self.requestedVideoChannels)
self.connectPendingVideoSubscribers()
}
self.joinDisposable.set((genericCallContext.joinPayload
@ -3055,7 +3061,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
public func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo]) {
self.genericCallContext?.setRequestedVideoChannels(items.compactMap { item -> OngoingGroupCallContext.VideoChannel in
self.requestedVideoChannels = items.compactMap { item -> OngoingGroupCallContext.VideoChannel in
let mappedMinQuality: OngoingGroupCallContext.VideoChannel.Quality
let mappedMaxQuality: OngoingGroupCallContext.VideoChannel.Quality
switch item.minQuality {
@ -3083,7 +3089,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
minQuality: mappedMinQuality,
maxQuality: mappedMaxQuality
)
})
}
if let genericCallContext = self.genericCallContext {
genericCallContext.setRequestedVideoChannels(self.requestedVideoChannels)
}
}
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
@ -3538,7 +3547,49 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError>? {
return self.genericCallContext?.video(endpointId: endpointId)
return Signal { [weak self] subscriber in
guard let self else {
return EmptyDisposable
}
if let genericCallContext = self.genericCallContext {
return genericCallContext.video(endpointId: endpointId).start(next: { value in
subscriber.putNext(value)
})
} else {
let disposable = MetaDisposable()
let index = self.pendingVideoSubscribers.add((endpointId, disposable, { value in
subscriber.putNext(value)
}))
return ActionDisposable { [weak self] in
disposable.dispose()
Queue.mainQueue().async {
guard let self else {
return
}
self.pendingVideoSubscribers.remove(index)
}
}
}
}
|> runOn(.mainQueue())
}
private func connectPendingVideoSubscribers() {
guard let genericCallContext = self.genericCallContext else {
return
}
let items = self.pendingVideoSubscribers.copyItems()
self.pendingVideoSubscribers.removeAll()
for (endpointId, disposable, f) in items {
disposable.set(genericCallContext.video(endpointId: endpointId).start(next: { value in
f(value)
}))
}
}
public func loadMoreMembers(token: String) {

View File

@ -0,0 +1,193 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import AppBundle
final class VideoChatActionButtonComponent: Component {
enum Content: Equatable {
fileprivate enum IconType {
case video
case leave
}
case video(isActive: Bool)
case leave
fileprivate var iconType: IconType {
switch self {
case .video:
return .video
case .leave:
return .leave
}
}
}
enum MicrophoneState {
case connecting
case muted
case unmuted
}
let content: Content
let microphoneState: MicrophoneState
init(
content: Content,
microphoneState: MicrophoneState
) {
self.content = content
self.microphoneState = microphoneState
}
static func ==(lhs: VideoChatActionButtonComponent, rhs: VideoChatActionButtonComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.microphoneState != rhs.microphoneState {
return false
}
return true
}
final class View: HighlightTrackingButton {
private let icon = ComponentView<Empty>()
private let background = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var component: VideoChatActionButtonComponent?
private var isUpdating: Bool = false
private var contentImage: UIImage?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: VideoChatActionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let previousComponent = self.component
self.component = component
let titleText: String
let backgroundColor: UIColor
let iconDiameter: CGFloat
switch component.content {
case let .video(isActive):
titleText = "video"
switch component.microphoneState {
case .connecting:
backgroundColor = UIColor(white: 1.0, alpha: 0.1)
case .muted:
backgroundColor = isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF)
case .unmuted:
backgroundColor = isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
}
iconDiameter = 60.0
case .leave:
titleText = "leave"
backgroundColor = UIColor(rgb: 0x47191E)
iconDiameter = 22.0
}
if self.contentImage == nil || previousComponent?.content.iconType != component.content.iconType {
switch component.content.iconType {
case .video:
self.contentImage = UIImage(bundleImageName: "Call/CallCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate)
case .leave:
self.contentImage = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setLineWidth(4.0 - UIScreenPixel)
context.setLineCap(.round)
context.setStrokeColor(UIColor.white.cgColor)
context.move(to: CGPoint(x: 2.0 + UIScreenPixel, y: 2.0 + UIScreenPixel))
context.addLine(to: CGPoint(x: 26.0 - UIScreenPixel, y: 26.0 - UIScreenPixel))
context.strokePath()
context.move(to: CGPoint(x: 26.0 - UIScreenPixel, y: 2.0 + UIScreenPixel))
context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel))
context.strokePath()
})
}
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.regular(13.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 90.0, height: 100.0)
)
let size = CGSize(width: availableSize.width, height: availableSize.height)
let _ = self.background.update(
transition: transition,
component: AnyComponent(RoundedRectangle(
color: backgroundColor,
cornerRadius: nil
)),
environment: {},
containerSize: size
)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size))
}
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 8.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.center)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(Image(
image: self.contentImage,
tintColor: .white,
size: CGSize(width: iconDiameter, height: iconDiameter)
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,103 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import BundleIconComponent
final class VideoChatListInviteComponent: Component {
let title: String
let theme: PresentationTheme
init(
title: String,
theme: PresentationTheme
) {
self.title = title
self.theme = theme
}
static func ==(lhs: VideoChatListInviteComponent, rhs: VideoChatListInviteComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var component: VideoChatListInviteComponent?
private var isUpdating: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: VideoChatListInviteComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 62.0 - 8.0, height: 100.0)
)
let size = CGSize(width: availableSize.width, height: 46.0)
let titleFrame = CGRect(origin: CGPoint(x: 62.0, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: "Chat/Context Menu/AddUser",
tintColor: component.theme.list.itemAccentColor
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((62.0 - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,135 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import LottieComponent
final class VideoChatMicButtonComponent: Component {
enum Content {
case connecting
case muted
case unmuted
}
let content: Content
init(
content: Content
) {
self.content = content
}
static func ==(lhs: VideoChatMicButtonComponent, rhs: VideoChatMicButtonComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
return true
}
final class View: HighlightTrackingButton {
private let background = ComponentView<Empty>()
private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var component: VideoChatMicButtonComponent?
private var isUpdating: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: VideoChatMicButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
let titleText: String
let backgroundColor: UIColor
switch component.content {
case .connecting:
titleText = "Connecting..."
backgroundColor = UIColor(white: 1.0, alpha: 0.1)
case .muted:
titleText = "Unmute"
backgroundColor = UIColor(rgb: 0x0086FF)
case .unmuted:
titleText = "Mute"
backgroundColor = UIColor(rgb: 0x34C659)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 120.0, height: 100.0)
)
let size = CGSize(width: availableSize.width, height: availableSize.height)
let _ = self.background.update(
transition: transition,
component: AnyComponent(RoundedRectangle(
color: backgroundColor,
cornerRadius: nil
)),
environment: {},
containerSize: size
)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size))
}
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.center)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: "VoiceUnmute"
),
color: .white
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,266 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import BundleIconComponent
import MetalEngine
import CallScreen
import TelegramCore
import AccountContext
import SwiftSignalKit
final class VideoChatParticipantVideoComponent: Component {
let call: PresentationGroupCall
let participant: GroupCallParticipantsContext.Participant
let isPresentation: Bool
init(
call: PresentationGroupCall,
participant: GroupCallParticipantsContext.Participant,
isPresentation: Bool
) {
self.call = call
self.participant = participant
self.isPresentation = isPresentation
}
static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool {
if lhs.participant != rhs.participant {
return false
}
if lhs.isPresentation != rhs.isPresentation {
return false
}
return true
}
private struct VideoSpec: Equatable {
var resolution: CGSize
var rotationAngle: Float
init(resolution: CGSize, rotationAngle: Float) {
self.resolution = resolution
self.rotationAngle = rotationAngle
}
}
final class View: UIView {
private var component: VideoChatParticipantVideoComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private let title = ComponentView<Empty>()
private var videoSource: AdaptedCallVideoSource?
private var videoDisposable: Disposable?
private var videoBackgroundLayer: SimpleLayer?
private var videoLayer: PrivateCallVideoLayer?
private var videoSpec: VideoSpec?
override init(frame: CGRect) {
super.init(frame: frame)
self.clipsToBounds = true
self.layer.cornerRadius = 10.0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.videoDisposable?.dispose()
}
func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
self.state = state
let nameColor = component.participant.peer.nameColor ?? .blue
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
self.backgroundColor = nameColors.main
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.regular(14.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: 8.0, y: availableSize.height - 8.0 - titleSize.height), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription {
let videoBackgroundLayer: SimpleLayer
if let current = self.videoBackgroundLayer {
videoBackgroundLayer = current
} else {
videoBackgroundLayer = SimpleLayer()
videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor
self.videoBackgroundLayer = videoBackgroundLayer
self.layer.insertSublayer(videoBackgroundLayer, at: 0)
videoBackgroundLayer.isHidden = true
}
let videoLayer: PrivateCallVideoLayer
if let current = self.videoLayer {
videoLayer = current
} else {
videoLayer = PrivateCallVideoLayer()
self.videoLayer = videoLayer
self.layer.insertSublayer(videoLayer, above: videoBackgroundLayer)
if let input = (component.call as! PresentationGroupCallImpl).video(endpointId: videoDescription.endpointId) {
let videoSource = AdaptedCallVideoSource(videoStreamSignal: input)
self.videoSource = videoSource
self.videoDisposable?.dispose()
self.videoDisposable = videoSource.addOnUpdated { [weak self] in
guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else {
return
}
let videoOutput = videoSource.currentOutput
videoLayer.video = videoOutput
if let videoOutput {
let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle)
if self.videoSpec != videoSpec {
self.videoSpec = videoSpec
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
} else {
if self.videoSpec != nil {
self.videoSpec = nil
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
}
/*var notifyOrientationUpdated = false
var notifyIsMirroredUpdated = false
if !self.didReportFirstFrame {
notifyOrientationUpdated = true
notifyIsMirroredUpdated = true
}
if let currentOutput = videoOutput {
let currentAspect: CGFloat
if currentOutput.resolution.height > 0.0 {
currentAspect = currentOutput.resolution.width / currentOutput.resolution.height
} else {
currentAspect = 1.0
}
if self.currentAspect != currentAspect {
self.currentAspect = currentAspect
notifyOrientationUpdated = true
}
let currentOrientation: PresentationCallVideoView.Orientation
if currentOutput.followsDeviceOrientation {
currentOrientation = .rotation0
} else {
if abs(currentOutput.rotationAngle - 0.0) < .ulpOfOne {
currentOrientation = .rotation0
} else if abs(currentOutput.rotationAngle - Float.pi * 0.5) < .ulpOfOne {
currentOrientation = .rotation90
} else if abs(currentOutput.rotationAngle - Float.pi) < .ulpOfOne {
currentOrientation = .rotation180
} else if abs(currentOutput.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne {
currentOrientation = .rotation270
} else {
currentOrientation = .rotation0
}
}
if self.currentOrientation != currentOrientation {
self.currentOrientation = currentOrientation
notifyOrientationUpdated = true
}
let currentIsMirrored = !currentOutput.mirrorDirection.isEmpty
if self.currentIsMirrored != currentIsMirrored {
self.currentIsMirrored = currentIsMirrored
notifyIsMirroredUpdated = true
}
}
if !self.didReportFirstFrame {
self.didReportFirstFrame = true
self.onFirstFrameReceived?(Float(self.currentAspect))
}
if notifyOrientationUpdated {
self.onOrientationUpdated?(self.currentOrientation, self.currentAspect)
}
if notifyIsMirroredUpdated {
self.onIsMirroredUpdated?(self.currentIsMirrored)
}*/
}
}
}
transition.setFrame(layer: videoBackgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
if let videoSpec = self.videoSpec {
videoBackgroundLayer.isHidden = false
let rotatedResolution = videoSpec.resolution
let videoSize = rotatedResolution.aspectFitted(availableSize)
let videoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - videoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - videoSize.height) * 0.5)), size: videoSize)
let videoResolution = rotatedResolution.aspectFitted(CGSize(width: availableSize.width * 3.0, height: availableSize.height * 3.0))
let rotatedVideoResolution = videoResolution
transition.setPosition(layer: videoLayer, position: videoFrame.center)
transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: videoFrame.size))
videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
}
} else {
if let videoBackgroundLayer = self.videoBackgroundLayer {
self.videoBackgroundLayer = nil
videoBackgroundLayer.removeFromSuperlayer()
}
if let videoLayer = self.videoLayer {
self.videoLayer = nil
videoLayer.removeFromSuperlayer()
}
self.videoDisposable?.dispose()
self.videoDisposable = nil
self.videoSource = nil
self.videoSpec = nil
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -8,256 +8,43 @@ import AccountContext
import PlainButtonComponent
import SwiftSignalKit
import MultilineTextComponent
import MetalEngine
import CallScreen
private final class ParticipantVideoComponent: Component {
let call: PresentationGroupCall
let participant: GroupCallParticipantsContext.Participant
init(
call: PresentationGroupCall,
participant: GroupCallParticipantsContext.Participant
) {
self.call = call
self.participant = participant
}
static func ==(lhs: ParticipantVideoComponent, rhs: ParticipantVideoComponent) -> Bool {
if lhs.participant != rhs.participant {
return false
}
return true
}
private struct VideoSpec: Equatable {
var resolution: CGSize
var rotationAngle: Float
init(resolution: CGSize, rotationAngle: Float) {
self.resolution = resolution
self.rotationAngle = rotationAngle
}
}
final class View: UIView {
private var component: ParticipantVideoComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private let title = ComponentView<Empty>()
private var videoSource: AdaptedCallVideoSource?
private var videoDisposable: Disposable?
private var videoLayer: PrivateCallVideoLayer?
private var videoSpec: VideoSpec?
override init(frame: CGRect) {
super.init(frame: frame)
self.clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.videoDisposable?.dispose()
}
func update(component: ParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
self.state = state
let nameColor = component.participant.peer.nameColor ?? .blue
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
self.backgroundColor = nameColors.main
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.regular(14.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: 8.0, y: availableSize.height - 8.0 - titleSize.height), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
if let videoDescription = component.participant.videoDescription {
let _ = videoDescription
let videoLayer: PrivateCallVideoLayer
if let current = self.videoLayer {
videoLayer = current
} else {
videoLayer = PrivateCallVideoLayer()
self.videoLayer = videoLayer
self.layer.insertSublayer(videoLayer, at: 0)
if let input = (component.call as! PresentationGroupCallImpl).video(endpointId: videoDescription.endpointId) {
let videoSource = AdaptedCallVideoSource(videoStreamSignal: input)
self.videoSource = videoSource
self.videoDisposable?.dispose()
self.videoDisposable = videoSource.addOnUpdated { [weak self] in
guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else {
return
}
let videoOutput = videoSource.currentOutput
videoLayer.video = videoOutput
if let videoOutput {
let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle)
if self.videoSpec != videoSpec {
self.videoSpec = videoSpec
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
} else {
if self.videoSpec != nil {
self.videoSpec = nil
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
}
/*var notifyOrientationUpdated = false
var notifyIsMirroredUpdated = false
if !self.didReportFirstFrame {
notifyOrientationUpdated = true
notifyIsMirroredUpdated = true
}
if let currentOutput = videoOutput {
let currentAspect: CGFloat
if currentOutput.resolution.height > 0.0 {
currentAspect = currentOutput.resolution.width / currentOutput.resolution.height
} else {
currentAspect = 1.0
}
if self.currentAspect != currentAspect {
self.currentAspect = currentAspect
notifyOrientationUpdated = true
}
let currentOrientation: PresentationCallVideoView.Orientation
if currentOutput.followsDeviceOrientation {
currentOrientation = .rotation0
} else {
if abs(currentOutput.rotationAngle - 0.0) < .ulpOfOne {
currentOrientation = .rotation0
} else if abs(currentOutput.rotationAngle - Float.pi * 0.5) < .ulpOfOne {
currentOrientation = .rotation90
} else if abs(currentOutput.rotationAngle - Float.pi) < .ulpOfOne {
currentOrientation = .rotation180
} else if abs(currentOutput.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne {
currentOrientation = .rotation270
} else {
currentOrientation = .rotation0
}
}
if self.currentOrientation != currentOrientation {
self.currentOrientation = currentOrientation
notifyOrientationUpdated = true
}
let currentIsMirrored = !currentOutput.mirrorDirection.isEmpty
if self.currentIsMirrored != currentIsMirrored {
self.currentIsMirrored = currentIsMirrored
notifyIsMirroredUpdated = true
}
}
if !self.didReportFirstFrame {
self.didReportFirstFrame = true
self.onFirstFrameReceived?(Float(self.currentAspect))
}
if notifyOrientationUpdated {
self.onOrientationUpdated?(self.currentOrientation, self.currentAspect)
}
if notifyIsMirroredUpdated {
self.onIsMirroredUpdated?(self.currentIsMirrored)
}*/
}
}
}
transition.setFrame(layer: videoLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
if let videoSpec = self.videoSpec {
let rotatedResolution = videoSpec.resolution
let videoSize = rotatedResolution.aspectFilled(availableSize)
let videoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - videoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - videoSize.height) * 0.5)), size: videoSize)
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: availableSize.width, height: availableSize.height)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
let rotatedVideoResolution = videoResolution
transition.setPosition(layer: videoLayer, position: videoFrame.center)
transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: videoFrame.size))
videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
}
} else {
if let videoLayer = self.videoLayer {
self.videoLayer = nil
videoLayer.removeFromSuperlayer()
}
self.videoDisposable?.dispose()
self.videoDisposable = nil
self.videoSource = nil
self.videoSpec = nil
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
import TelegramPresentationData
import PeerListItemComponent
final class VideoChatParticipantsComponent: Component {
let call: PresentationGroupCall
let members: PresentationGroupCallMembers?
let theme: PresentationTheme
let strings: PresentationStrings
let sideInset: CGFloat
init(
call: PresentationGroupCall,
members: PresentationGroupCallMembers?
members: PresentationGroupCallMembers?,
theme: PresentationTheme,
strings: PresentationStrings,
sideInset: CGFloat
) {
self.call = call
self.members = members
self.theme = theme
self.strings = strings
self.sideInset = sideInset
}
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
if lhs.members != rhs.members {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
return true
}
@ -268,52 +55,201 @@ final class VideoChatParticipantsComponent: Component {
}
private final class ItemLayout {
let containerSize: CGSize
let itemCount: Int
let itemSize: CGSize
let itemSpacing: CGFloat
let lastItemSize: CGFloat
let itemsPerRow: Int
init(containerSize: CGSize, itemCount: Int) {
self.containerSize = containerSize
self.itemCount = itemCount
struct Grid {
let containerSize: CGSize
let sideInset: CGFloat
let itemCount: Int
let itemSize: CGSize
let itemSpacing: CGFloat
let lastItemSize: CGFloat
let itemsPerRow: Int
let width: CGFloat = containerSize.width
self.itemSpacing = 1.0
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int) {
self.containerSize = containerSize
self.sideInset = sideInset
self.itemCount = itemCount
let width: CGFloat = containerSize.width - sideInset * 2.0
self.itemSpacing = 4.0
let itemsPerRow: CGFloat = CGFloat(3)
self.itemsPerRow = Int(itemsPerRow)
let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
self.itemSize = CGSize(width: itemSize, height: itemSize)
let itemsPerRow: Int
if itemCount == 1 {
itemsPerRow = 1
} else {
itemsPerRow = 2
}
self.itemsPerRow = Int(itemsPerRow)
let itemWidth = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / CGFloat(itemsPerRow))
let itemHeight = min(180.0, itemWidth)
self.itemSize = CGSize(width: itemWidth, height: itemHeight)
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
}
func frame(at index: Int) -> CGRect {
let row = index / self.itemsPerRow
let column = index % self.itemsPerRow
let frame = CGRect(origin: CGPoint(x: self.sideInset + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height))
return frame
}
func contentHeight() -> CGFloat {
return self.frame(at: self.itemCount - 1).maxY
}
func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
if self.itemCount == 0 {
return (0, -1)
}
let offsetRect = rect.offsetBy(dx: 0.0, dy: 0.0)
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing)))
let minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
return (minVisibleIndex, maxVisibleIndex)
}
}
func frame(at index: Int) -> CGRect {
let row = index / self.itemsPerRow
let column = index % self.itemsPerRow
struct List {
let containerSize: CGSize
let sideInset: CGFloat
let itemCount: Int
let itemHeight: CGFloat
let trailingItemHeight: CGFloat
let frame = CGRect(origin: CGPoint(x: CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height))
return frame
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeight: CGFloat) {
self.containerSize = containerSize
self.sideInset = sideInset
self.itemCount = itemCount
self.itemHeight = itemHeight
self.trailingItemHeight = trailingItemHeight
}
func frame(at index: Int) -> CGRect {
let frame = CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.itemHeight))
return frame
}
func trailingItemFrame() -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeight))
}
func contentHeight() -> CGFloat {
var result: CGFloat = 0.0
if self.itemCount != 0 {
result = self.frame(at: self.itemCount - 1).maxY
}
result += self.trailingItemHeight
return result
}
func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
if self.itemCount == 0 {
return (0, -1)
}
let offsetRect = rect.offsetBy(dx: 0.0, dy: 0.0)
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
let minVisibleIndex = minVisibleRow
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) - 1)
return (minVisibleIndex, maxVisibleIndex)
}
}
let containerSize: CGSize
let sideInset: CGFloat
let grid: Grid
let list: List
let spacing: CGFloat
let listOffsetY: CGFloat
init(containerSize: CGSize, sideInset: CGFloat, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
self.containerSize = containerSize
self.sideInset = sideInset
self.grid = Grid(containerSize: containerSize, sideInset: sideInset, itemCount: gridItemCount)
self.list = List(containerSize: containerSize, sideInset: sideInset, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.spacing = 4.0
var listOffsetY: CGFloat = 0.0
if self.grid.itemCount != 0 {
listOffsetY += self.grid.contentHeight()
listOffsetY += self.spacing
}
self.listOffsetY = listOffsetY
}
func contentHeight() -> CGFloat {
return self.frame(at: self.itemCount - 1).maxY
var result: CGFloat = 0.0
if self.grid.itemCount != 0 {
result += self.grid.contentHeight()
result += self.spacing
}
result += self.list.contentHeight()
return result
}
func visibleItemRange(for rect: CGRect, count: Int) -> (minIndex: Int, maxIndex: Int) {
let offsetRect = rect.offsetBy(dx: 0.0, dy: 0.0)
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing)))
let minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
return (minVisibleIndex, maxVisibleIndex)
func visibleGridItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
return self.grid.visibleItemRange(for: rect)
}
func gridItemFrame(at index: Int) -> CGRect {
return self.grid.frame(at: index)
}
func visibleListItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
return self.list.visibleItemRange(for: rect.offsetBy(dx: 0.0, dy: -self.listOffsetY))
}
func listItemFrame(at index: Int) -> CGRect {
return self.list.frame(at: index).offsetBy(dx: 0.0, dy: self.listOffsetY)
}
func listTrailingItemFrame() -> CGRect {
return self.list.trailingItemFrame().offsetBy(dx: 0.0, dy: self.listOffsetY)
}
}
private final class VideoParticipant: Equatable {
struct Key: Hashable {
var id: EnginePeer.Id
var isPresentation: Bool
init(id: EnginePeer.Id, isPresentation: Bool) {
self.id = id
self.isPresentation = isPresentation
}
}
let participant: GroupCallParticipantsContext.Participant
let isPresentation: Bool
var key: Key {
return Key(id: self.participant.peer.id, isPresentation: self.isPresentation)
}
init(participant: GroupCallParticipantsContext.Participant, isPresentation: Bool) {
self.participant = participant
self.isPresentation = isPresentation
}
static func ==(lhs: VideoParticipant, rhs: VideoParticipant) -> Bool {
if lhs.participant != rhs.participant {
return false
}
if lhs.isPresentation != rhs.isPresentation {
return false
}
return true
}
}
@ -325,7 +261,17 @@ final class VideoChatParticipantsComponent: Component {
private var ignoreScrolling: Bool = false
private var itemViews: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var gridParticipants: [VideoParticipant] = []
private var listParticipants: [GroupCallParticipantsContext.Participant] = []
private let measureListItemView = ComponentView<Empty>()
private let inviteListItemView = ComponentView<Empty>()
private var gridItemViews: [VideoParticipant.Key: ComponentView<Empty>] = [:]
private var listItemViews: [EnginePeer.Id: ComponentView<Empty>] = [:]
private let listItemsBackround = ComponentView<Empty>()
private var itemLayout: ItemLayout?
override init(frame: CGRect) {
@ -343,7 +289,7 @@ final class VideoChatParticipantsComponent: Component {
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = true
self.scrollView.alwaysBounceVertical = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
@ -366,57 +312,146 @@ final class VideoChatParticipantsComponent: Component {
return
}
var validItemIds: [EnginePeer.Id] = []
if let members = component.members {
let visibleItemRange = itemLayout.visibleItemRange(for: self.scrollView.bounds, count: itemLayout.itemCount)
if visibleItemRange.maxIndex >= visibleItemRange.minIndex {
for i in visibleItemRange.minIndex ... visibleItemRange.maxIndex {
let participant = members.participants[i]
validItemIds.append(participant.peer.id)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.itemViews[participant.peer.id] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ComponentView()
self.itemViews[participant.peer.id] = itemView
}
let itemFrame = itemLayout.frame(at: i)
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(ParticipantVideoComponent(
call: component.call,
participant: participant
)),
environment: {},
containerSize: itemFrame.size
)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.scrollView.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
var validGridItemIds: [VideoParticipant.Key] = []
let visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
for i in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
let videoParticipant = self.gridParticipants[i]
validGridItemIds.append(videoParticipant.key)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.gridItemViews[videoParticipant.key] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ComponentView()
self.gridItemViews[videoParticipant.key] = itemView
}
let itemFrame = itemLayout.gridItemFrame(at: i)
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(VideoChatParticipantVideoComponent(
call: component.call,
participant: videoParticipant.participant,
isPresentation: videoParticipant.isPresentation
)),
environment: {},
containerSize: itemFrame.size
)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.scrollView.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
}
var removedItemIds: [EnginePeer.Id] = []
for (itemId, itemView) in self.itemViews {
if !validItemIds.contains(itemId) {
removedItemIds.append(itemId)
var removedGridItemIds: [VideoParticipant.Key] = []
for (itemId, itemView) in self.gridItemViews {
if !validGridItemIds.contains(itemId) {
removedGridItemIds.append(itemId)
if let itemComponentView = itemView.view {
itemComponentView.removeFromSuperview()
}
}
}
for itemId in removedItemIds {
self.itemViews.removeValue(forKey: itemId)
for itemId in removedGridItemIds {
self.gridItemViews.removeValue(forKey: itemId)
}
var validListItemIds: [EnginePeer.Id] = []
let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds)
if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex {
for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex {
let participant = self.listParticipants[i]
validListItemIds.append(participant.peer.id)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.listItemViews[participant.peer.id] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ComponentView()
self.listItemViews[participant.peer.id] = itemView
}
let itemFrame = itemLayout.listItemFrame(at: i)
let subtitle: PeerListItemComponent.Subtitle
if participant.peer.id == component.call.accountContext.account.peerId {
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
} else {
subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral)
}
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(PeerListItemComponent(
context: component.call.accountContext,
theme: component.theme,
strings: component.strings,
style: .generic,
sideInset: 0.0,
title: EnginePeer(participant.peer).displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: EnginePeer(participant.peer),
subtitle: subtitle,
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: true,
action: { [weak self] peer, _, _ in
guard let self else {
return
}
let _ = self
let _ = peer
}
)),
environment: {},
containerSize: itemFrame.size
)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.scrollView.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
}
var removedListItemIds: [EnginePeer.Id] = []
for (itemId, itemView) in self.listItemViews {
if !validListItemIds.contains(itemId) {
removedListItemIds.append(itemId)
if let itemComponentView = itemView.view {
itemComponentView.removeFromSuperview()
}
}
}
for itemId in removedListItemIds {
self.listItemViews.removeValue(forKey: itemId)
}
do {
var itemTransition = transition
let itemView = self.inviteListItemView
let itemFrame = itemLayout.listTrailingItemFrame()
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
itemTransition = itemTransition.withAnimation(.none)
self.scrollView.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
}
@ -428,15 +463,109 @@ final class VideoChatParticipantsComponent: Component {
self.component = component
let itemLayout = ItemLayout(containerSize: availableSize, itemCount: component.members?.totalCount ?? 0)
let measureListItemSize = self.measureListItemView.update(
transition: .immediate,
component: AnyComponent(PeerListItemComponent(
context: component.call.accountContext,
theme: component.theme,
strings: component.strings,
style: .generic,
sideInset: 0.0,
title: "AAA",
peer: nil,
subtitle: PeerListItemComponent.Subtitle(text: "bbb", color: .neutral),
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: true,
action: { _, _, _ in
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let inviteListItemSize = self.inviteListItemView.update(
transition: transition,
component: AnyComponent(VideoChatListInviteComponent(
title: "Invite Members",
theme: component.theme
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
var gridParticipants: [VideoParticipant] = []
var listParticipants: [GroupCallParticipantsContext.Participant] = []
if let members = component.members {
for participant in members.participants {
var hasVideo = false
if participant.videoDescription != nil {
hasVideo = true
let videoParticipant = VideoParticipant(participant: participant, isPresentation: false)
if participant.peer.id == component.call.accountContext.account.peerId {
gridParticipants.insert(videoParticipant, at: 0)
} else {
gridParticipants.append(videoParticipant)
}
}
if participant.presentationDescription != nil {
hasVideo = true
let videoParticipant = VideoParticipant(participant: participant, isPresentation: true)
if participant.peer.id == component.call.accountContext.account.peerId {
gridParticipants.insert(videoParticipant, at: 0)
} else {
gridParticipants.append(videoParticipant)
}
}
if !hasVideo {
if participant.peer.id == component.call.accountContext.account.peerId {
listParticipants.insert(participant, at: 0)
} else {
listParticipants.append(participant)
}
}
}
}
self.gridParticipants = gridParticipants
self.listParticipants = listParticipants
let itemLayout = ItemLayout(
containerSize: availableSize,
sideInset: component.sideInset,
gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count,
listItemHeight: measureListItemSize.height,
listTrailingItemHeight: inviteListItemSize.height
)
self.itemLayout = itemLayout
let listItemsBackroundSize = self.listItemsBackround.update(
transition: transition,
component: AnyComponent(RoundedRectangle(
color: UIColor(white: 1.0, alpha: 0.1),
cornerRadius: 10.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - itemLayout.sideInset * 2.0, height: itemLayout.list.contentHeight())
)
let listItemsBackroundFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: itemLayout.listOffsetY), size: listItemsBackroundSize)
if let listItemsBackroundView = self.listItemsBackround.view {
if listItemsBackroundView.superview == nil {
self.scrollView.addSubview(listItemsBackroundView)
}
transition.setFrame(view: listItemsBackroundView, frame: listItemsBackroundFrame)
}
var requestedVideo: [PresentationGroupCallRequestedVideo] = []
if let members = component.members {
for participant in members.participants {
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
requestedVideo.append(videoChannel)
}
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
requestedVideo.append(videoChannel)
}
}
}
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)

View File

@ -9,36 +9,80 @@ import TelegramCore
import AccountContext
import PlainButtonComponent
import SwiftSignalKit
import LottieComponent
import BundleIconComponent
import ContextUI
import TelegramPresentationData
private final class VideoChatScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let initialData: VideoChatScreenV2Impl.InitialData
let call: PresentationGroupCall
init(
initialData: VideoChatScreenV2Impl.InitialData,
call: PresentationGroupCall
) {
self.initialData = initialData
self.call = call
}
static func ==(lhs: VideoChatScreenComponent, rhs: VideoChatScreenComponent) -> Bool {
return true
}
private struct PanGestureState {
var offsetFraction: CGFloat
init(offsetFraction: CGFloat) {
self.offsetFraction = offsetFraction
}
}
final class View: UIView {
private let containerView: UIView
private var component: VideoChatScreenComponent?
private var environment: ViewControllerComponentContainer.Environment?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private let closeButton = ComponentView<Empty>()
private var panGestureState: PanGestureState?
private var notifyDismissedInteractivelyOnPanGestureApply: Bool = false
private var completionOnPanGestureApply: (() -> Void)?
private let title = ComponentView<Empty>()
private let navigationLeftButton = ComponentView<Empty>()
private let navigationRightButton = ComponentView<Empty>()
private let videoButton = ComponentView<Empty>()
private let leaveButton = ComponentView<Empty>()
private let microphoneButton = ComponentView<Empty>()
private let participants = ComponentView<Empty>()
private var peer: EnginePeer?
private var callState: PresentationGroupCallState?
private var stateDisposable: Disposable?
private var members: PresentationGroupCallMembers?
private var membersDisposable: Disposable?
override init(frame: CGRect) {
self.containerView = UIView()
self.containerView.clipsToBounds = true
super.init(frame: frame)
self.backgroundColor = nil
self.isOpaque = false
self.addSubview(self.containerView)
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
self.panGestureState = PanGestureState(offsetFraction: 1.0)
}
required init?(coder: NSCoder) {
@ -46,9 +90,111 @@ private final class VideoChatScreenComponent: Component {
}
deinit {
self.stateDisposable?.dispose()
self.membersDisposable?.dispose()
}
func animateIn() {
self.panGestureState = PanGestureState(offsetFraction: 1.0)
self.state?.updated(transition: .immediate)
self.panGestureState = nil
self.state?.updated(transition: .spring(duration: 0.5))
}
func animateOut(completion: @escaping () -> Void) {
self.panGestureState = PanGestureState(offsetFraction: 1.0)
self.completionOnPanGestureApply = completion
self.state?.updated(transition: .spring(duration: 0.5))
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply {
let translation = recognizer.translation(in: self)
self.panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height)
self.state?.updated(transition: .immediate)
}
case .cancelled, .ended:
if !self.bounds.height.isZero {
let translation = recognizer.translation(in: self)
let panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height)
let velocity = recognizer.velocity(in: self)
self.panGestureState = nil
if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 {
self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0)
self.notifyDismissedInteractivelyOnPanGestureApply = true
}
self.state?.updated(transition: .spring(duration: 0.4))
}
default:
break
}
}
private func openMoreMenu() {
guard let sourceView = self.navigationLeftButton.view else {
return
}
guard let component = self.component, let environment = self.environment, let controller = environment.controller() else {
return
}
var items: [ContextMenuItem] = []
let text: String
let isScheduled = component.call.schedulePending
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
text = isScheduled ? environment.strings.VoiceChat_CancelLiveStream : environment.strings.VoiceChat_EndLiveStream
} else {
text = isScheduled ? environment.strings.VoiceChat_CancelVoiceChat : environment.strings.VoiceChat_EndVoiceChat
}
items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
/*guard let strongSelf = self else {
return
}
let action: () -> Void = {
guard let strongSelf = self else {
return
}
let _ = (strongSelf.call.leave(terminateIfPossible: true)
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: {
self?.controller?.dismiss()
})
}
let title: String
let text: String
if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info {
title = isScheduled ? strongSelf.presentationData.strings.LiveStream_CancelConfirmationTitle : strongSelf.presentationData.strings.LiveStream_EndConfirmationTitle
text = isScheduled ? strongSelf.presentationData.strings.LiveStream_CancelConfirmationText : strongSelf.presentationData.strings.LiveStream_EndConfirmationText
} else {
title = isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationTitle : strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle
text = isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationText : strongSelf.presentationData.strings.VoiceChat_EndConfirmationText
}
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationEnd : strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: {
action()
})])
strongSelf.controller?.present(alertController, in: .window(.root))*/
})))
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let contextController = ContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
controller.presentInGlobalOverlay(contextController)
}
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@ -59,6 +205,10 @@ private final class VideoChatScreenComponent: Component {
let themeUpdated = self.environment?.theme !== environment.theme
if self.component == nil {
self.peer = component.initialData.peer
self.members = component.initialData.members
self.callState = component.initialData.callState
self.membersDisposable = (component.call.members
|> deliverOnMainQueue).startStrict(next: { [weak self] members in
guard let self else {
@ -72,6 +222,20 @@ private final class VideoChatScreenComponent: Component {
}
}
})
self.stateDisposable = (component.call.state
|> deliverOnMainQueue).startStrict(next: { [weak self] callState in
guard let self else {
return
}
if self.callState != callState {
self.callState = callState
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
}
})
}
self.component = component
@ -79,60 +243,273 @@ private final class VideoChatScreenComponent: Component {
self.state = state
if themeUpdated {
self.backgroundColor = .black
self.containerView.backgroundColor = .black
}
let closeButtonSize = self.closeButton.update(
transition: transition,
var containerOffset: CGFloat = 0.0
if let panGestureState = self.panGestureState {
containerOffset = panGestureState.offsetFraction * availableSize.height
self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius
}
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerOffset), size: availableSize), completion: { [weak self] completed in
guard let self, completed else {
return
}
if self.panGestureState == nil {
self.containerView.layer.cornerRadius = 0.0
}
if self.notifyDismissedInteractivelyOnPanGestureApply {
self.notifyDismissedInteractivelyOnPanGestureApply = false
if let controller = self.environment?.controller() as? VideoChatScreenV2Impl {
controller.superDismiss()
}
}
if let completionOnPanGestureApply = self.completionOnPanGestureApply {
self.completionOnPanGestureApply = nil
DispatchQueue.main.async {
completionOnPanGestureApply()
}
}
})
let sideInset: CGFloat = environment.safeInsets.left + 14.0
let topInset: CGFloat = environment.statusBarHeight + 2.0
let navigationBarHeight: CGFloat = 61.0
let navigationHeight = topInset + navigationBarHeight
let navigationButtonAreaWidth: CGFloat = 40.0
let navigationButtonDiameter: CGFloat = 28.0
let navigationLeftButtonSize = self.navigationLeftButton.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(Text(
text: "Leave", font: Font.regular(16.0), color: environment.theme.list.itemDestructiveColor)),
content: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: "anim_profilemore"
),
color: .white
)),
background: AnyComponent(Circle(
fillColor: UIColor(white: 1.0, alpha: 0.1),
size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter)
)),
effectAlignment: .center,
minSize: CGSize(width: 44.0, height: 44.0),
contentInsets: UIEdgeInsets(),
action: { [weak self] in
guard let self, let component = self.component else {
guard let self else {
return
}
let _ = component.call.leave(terminateIfPossible: false).startStandalone()
if let controller = self.environment?.controller() {
controller.dismiss()
}
},
animateAlpha: true,
animateScale: true,
animateContents: false
self.openMoreMenu()
}
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
containerSize: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - closeButtonSize.width, y: availableSize.height - environment.safeInsets.bottom - 16.0 - closeButtonSize.height), size: closeButtonSize)
if let closeButtonView = self.closeButton.view {
if closeButtonView.superview == nil {
self.addSubview(closeButtonView)
let navigationRightButtonSize = self.navigationRightButton.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(Image(
image: closeButtonImage(dark: false)
)),
background: AnyComponent(Circle(
fillColor: UIColor(white: 1.0, alpha: 0.1),
size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter)
)),
effectAlignment: .center,
action: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter)
)
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: sideInset + floor((navigationButtonAreaWidth - navigationLeftButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize)
if let navigationLeftButtonView = self.navigationLeftButton.view {
if navigationLeftButtonView.superview == nil {
self.containerView.addSubview(navigationLeftButtonView)
}
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
}
let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize)
if let navigationRightButtonView = self.navigationRightButton.view {
if navigationRightButtonView.superview == nil {
self.containerView.addSubview(navigationRightButtonView)
}
transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame)
}
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(VideoChatTitleComponent(
title: self.peer?.debugDisplayTitle ?? " ",
status: .idle(count: self.members?.totalCount ?? 1),
strings: environment.strings
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: topInset + floor((navigationBarHeight - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.containerView.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
let actionButtonDiameter: CGFloat = 56.0
let microphoneButtonDiameter: CGFloat = 116.0
let maxActionMicrophoneButtonSpacing: CGFloat = 38.0
let buttonsSideInset: CGFloat = 42.0
let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5))
let microphoneButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - microphoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - microphoneButtonDiameter), size: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter))
let leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
let rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
let participantsSize = self.participants.update(
transition: transition,
component: AnyComponent(VideoChatParticipantsComponent(
call: component.call,
members: self.members
members: self.members,
theme: environment.theme,
strings: environment.strings,
sideInset: sideInset
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: closeButtonFrame.minY - environment.statusBarHeight)
containerSize: CGSize(width: availableSize.width, height: microphoneButtonFrame.minY - navigationHeight)
)
let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.statusBarHeight), size: participantsSize)
let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: participantsSize)
if let participantsView = self.participants.view {
if participantsView.superview == nil {
self.addSubview(participantsView)
self.containerView.addSubview(participantsView)
}
transition.setFrame(view: participantsView, frame: participantsFrame)
}
let micButtonContent: VideoChatMicButtonComponent.Content
let actionButtonMicrophoneState: VideoChatActionButtonComponent.MicrophoneState
if let callState = self.callState {
switch callState.networkState {
case .connecting:
micButtonContent = .connecting
actionButtonMicrophoneState = .connecting
case .connected:
if let _ = callState.muteState {
micButtonContent = .muted
actionButtonMicrophoneState = .muted
} else {
micButtonContent = .unmuted
actionButtonMicrophoneState = .unmuted
}
}
} else {
micButtonContent = .connecting
actionButtonMicrophoneState = .connecting
}
let _ = self.microphoneButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatMicButtonComponent(
content: micButtonContent
)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
guard let callState = self.callState else {
return
}
if let muteState = callState.muteState {
if muteState.canUnmute {
component.call.setIsMuted(action: .unmuted)
}
} else {
component.call.setIsMuted(action: .muted(isPushToTalkActive: false))
}
},
animateAlpha: false,
animateScale: false
)),
environment: {},
containerSize: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter)
)
if let microphoneButtonView = self.microphoneButton.view {
if microphoneButtonView.superview == nil {
self.containerView.addSubview(microphoneButtonView)
}
transition.setPosition(view: microphoneButtonView, position: microphoneButtonFrame.center)
transition.setBounds(view: microphoneButtonView, bounds: CGRect(origin: CGPoint(), size: microphoneButtonFrame.size))
}
let _ = self.videoButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatActionButtonComponent(
content: .video(isActive: false),
microphoneState: actionButtonMicrophoneState
)),
effectAlignment: .center,
action: {
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)
)
if let videoButtonView = self.videoButton.view {
if videoButtonView.superview == nil {
self.containerView.addSubview(videoButtonView)
}
transition.setPosition(view: videoButtonView, position: leftActionButtonFrame.center)
transition.setBounds(view: videoButtonView, bounds: CGRect(origin: CGPoint(), size: leftActionButtonFrame.size))
}
let _ = self.leaveButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatActionButtonComponent(
content: .leave,
microphoneState: actionButtonMicrophoneState
)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
let _ = component.call.leave(terminateIfPossible: false).startStandalone()
if let controller = self.environment?.controller() as? VideoChatScreenV2Impl {
controller.dismiss(closing: true, manual: false)
}
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)
)
if let leaveButtonView = self.leaveButton.view {
if leaveButtonView.superview == nil {
self.containerView.addSubview(leaveButtonView)
}
transition.setPosition(view: leaveButtonView, position: rightActionButtonFrame.center)
transition.setBounds(view: leaveButtonView, bounds: CGRect(origin: CGPoint(), size: rightActionButtonFrame.size))
}
return availableSize
}
}
@ -146,7 +523,23 @@ private final class VideoChatScreenComponent: Component {
}
}
public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatController {
final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatController {
final class InitialData {
let peer: EnginePeer?
let members: PresentationGroupCallMembers?
let callState: PresentationGroupCallState
init(
peer: EnginePeer?,
members: PresentationGroupCallMembers?,
callState: PresentationGroupCallState
) {
self.peer = peer
self.members = members
self.callState = callState
}
}
public let call: PresentationGroupCall
public var currentOverlayController: VoiceChatOverlayController?
public var parentNavigationController: NavigationController?
@ -154,25 +547,38 @@ public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, Voic
public var onViewDidAppear: (() -> Void)?
public var onViewDidDisappear: (() -> Void)?
private var isDismissed: Bool = false
private var isDismissed: Bool = true
private var didAppearOnce: Bool = false
private var isAnimatingDismiss: Bool = false
private var idleTimerExtensionDisposable: Disposable?
public init(
initialData: InitialData,
call: PresentationGroupCall
) {
self.call = call
let theme = customizeDefaultDarkPresentationTheme(
theme: defaultDarkPresentationTheme,
editing: false,
title: nil,
accentColor: UIColor(rgb: 0x3E88F7),
backgroundColors: [],
bubbleColors: [],
animateBubbleColors: false
)
super.init(
context: call.accountContext,
component: VideoChatScreenComponent(
initialData: initialData,
call: call
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
statusBarStyle: .default,
presentationMode: .default,
theme: .dark
theme: .custom(theme)
)
}
@ -187,13 +593,17 @@ public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, Voic
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.isDismissed = false
if self.isDismissed {
self.isDismissed = false
if let componentView = self.node.hostView.componentView as? VideoChatScreenComponent.View {
componentView.animateIn()
}
}
if !self.didAppearOnce {
self.didAppearOnce = true
//self.controllerNode.animateIn()
self.idleTimerExtensionDisposable?.dispose()
self.idleTimerExtensionDisposable = self.call.accountContext.sharedContext.applicationBindings.pushIdleTimerExtension()
}
@ -208,7 +618,9 @@ public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, Voic
self.idleTimerExtensionDisposable = nil
self.didAppearOnce = false
self.isDismissed = true
if !self.isDismissed {
self.isDismissed = true
}
self.onViewDidDisappear?()
}
@ -216,4 +628,42 @@ public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, Voic
public func dismiss(closing: Bool, manual: Bool) {
self.dismiss()
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isAnimatingDismiss {
if let componentView = self.node.hostView.componentView as? VideoChatScreenComponent.View {
self.isAnimatingDismiss = true
componentView.animateOut(completion: { [weak self] in
guard let self else {
return
}
self.isAnimatingDismiss = false
self.superDismiss()
})
} else {
self.superDismiss()
}
}
}
func superDismiss() {
super.dismiss()
}
static func initialData(call: PresentationGroupCall) -> Signal<InitialData, NoError> {
return combineLatest(
call.accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)
),
call.members |> take(1),
call.state |> take(1)
)
|> map { peer, members, callState -> InitialData in
return InitialData(
peer: peer,
members: members,
callState: callState
)
}
}
}

View File

@ -0,0 +1,159 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
final class VideoChatTitleComponent: Component {
enum Status: Equatable {
enum Key {
case idle
case speaking
}
case idle(count: Int)
case speaking(titles: [String])
var key: Key {
switch self {
case .idle:
return .idle
case .speaking:
return .speaking
}
}
}
let title: String
let status: Status
let strings: PresentationStrings
init(
title: String,
status: Status,
strings: PresentationStrings
) {
self.title = title
self.status = status
self.strings = strings
}
static func ==(lhs: VideoChatTitleComponent, rhs: VideoChatTitleComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.status != rhs.status {
return false
}
if lhs.strings !== rhs.strings {
return false
}
return true
}
final class View: UIView {
private let title = ComponentView<Empty>()
private var status: ComponentView<Empty>?
private var component: VideoChatTitleComponent?
private var isUpdating: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: VideoChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let previousComponent = self.component
self.component = component
let spacing: CGFloat = 1.0
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
if previousComponent?.status.key != component.status.key {
if let status = self.status {
self.status = nil
if let statusView = status.view {
transition.setAlpha(view: statusView, alpha: 0.0, completion: { [weak statusView] _ in
statusView?.removeFromSuperview()
})
transition.setPosition(view: statusView, position: statusView.center.offsetBy(dx: 0.0, dy: -10.0))
}
}
}
let status: ComponentView<Empty>
if let current = self.status {
status = current
} else {
status = ComponentView()
self.status = status
}
let statusComponent: AnyComponent<Empty>
switch component.status {
case let .idle(count):
statusComponent = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.VoiceChat_Panel_Members(Int32(count)), font: Font.regular(13.0), textColor: UIColor(white: 1.0, alpha: 0.5)))
))
case let .speaking(titles):
statusComponent = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titles.joined(separator: ", "), font: Font.regular(13.0), textColor: UIColor(rgb: 0x34c759)))
))
}
let statusSize = status.update(
transition: .immediate,
component: statusComponent,
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let size = CGSize(width: availableSize.width, height: titleSize.height + spacing + statusSize.height)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: 0.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.center)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize)
if let statusView = status.view {
if statusView.superview == nil {
self.addSubview(statusView)
}
transition.setPosition(view: statusView, position: statusFrame.center)
statusView.bounds = CGRect(origin: CGPoint(), size: statusFrame.size)
}
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -2500,7 +2500,7 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
if let controller = self.controller {
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: self.optionsButton.referenceNode.view)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
controller.presentInGlobalOverlay(contextController)
}
}
@ -7083,23 +7083,31 @@ private final class VoiceChatContextExtractedContentSource: ContextExtractedCont
}
}
private final class VoiceChatContextReferenceContentSource: ContextReferenceContentSource {
final class VoiceChatContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
private let sourceView: UIView
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
init(controller: ViewController, sourceView: UIView) {
self.controller = controller
self.sourceNode = sourceNode
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> VoiceChatController {
public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal<Any, NoError> {
if sharedContext.immediateExperimentalUISettings.callV2 {
return VideoChatScreenV2Impl(call: call)
return VideoChatScreenV2Impl.initialData(call: call) |> map { $0 as Any }
} else {
return .single(Void())
}
}
public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any) -> VoiceChatController {
if sharedContext.immediateExperimentalUISettings.callV2 {
return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call)
} else {
return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call)
}

View File

@ -225,7 +225,7 @@ final class ContextResultPanelComponent: Component {
sideInset: itemLayout.sideInset,
title: peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: peer,
subtitle: peer.addressName.flatMap { "@\($0)" },
subtitle: peer.addressName.flatMap { PeerListItemComponent.Subtitle(text: "@\($0)", color: .neutral) },
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
@ -294,7 +294,7 @@ final class ContextResultPanelComponent: Component {
sideInset: sideInset,
title: "AAAAAAAAAAAA",
peer: nil,
subtitle: "BBBBBBB",
subtitle: PeerListItemComponent.Subtitle(text: "BBBBBBB", color: .neutral),
subtitleAccessory: .none,
presence: nil,
selectionState: .none,

View File

@ -1357,7 +1357,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
sideInset: 0.0,
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: peer.peer,
subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser,
subtitle: PeerListItemComponent.Subtitle(text: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser, color: .neutral),
subtitleAccessory: .none,
presence: nil,
selectionState: .none,

View File

@ -507,7 +507,7 @@ final class BusinessRecipientListScreenComponent: Component {
sideInset: 0.0,
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: peer.peer,
subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser,
subtitle: PeerListItemComponent.Subtitle(text: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser, color: .neutral),
subtitleAccessory: .none,
presence: nil,
selectionState: .none,

View File

@ -1129,7 +1129,7 @@ final class ShareWithPeersScreenComponent: Component {
sideInset: itemLayout.sideInset,
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: peer,
subtitle: subtitle,
subtitle: subtitle.flatMap { PeerListItemComponent.Subtitle(text: $0, color: .neutral) },
subtitleAccessory: .none,
presence: nil,
rightAccessory: accessory,
@ -1465,7 +1465,7 @@ final class ShareWithPeersScreenComponent: Component {
sideInset: itemLayout.sideInset,
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: peer,
subtitle: subtitle,
subtitle: subtitle.flatMap { PeerListItemComponent.Subtitle(text: $0, color: .neutral) },
subtitleAccessory: .none,
presence: stateValue.presences[peer.id],
selectionState: .editing(isSelected: isSelected, isTinted: false),
@ -2346,7 +2346,7 @@ final class ShareWithPeersScreenComponent: Component {
sideInset: sideInset,
title: "Name",
peer: nil,
subtitle: isContactsSearch ? "" : "sub",
subtitle: PeerListItemComponent.Subtitle(text: isContactsSearch ? "" : "sub", color: .neutral),
subtitleAccessory: .none,
presence: nil,
selectionState: .editing(isSelected: false, isTinted: false),

View File

@ -164,6 +164,21 @@ public final class PeerListItemComponent: Component {
}
}
public struct Subtitle: Equatable {
public enum Color: Equatable {
case neutral
case accent
}
public var text: String
public var color: Color
public init(text: String, color: Color) {
self.text = text
self.color = color
}
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
@ -173,7 +188,7 @@ public final class PeerListItemComponent: Component {
let avatar: Avatar?
let peer: EnginePeer?
let storyStats: PeerStoryStats?
let subtitle: String?
let subtitle: Subtitle?
let subtitleAccessory: SubtitleAccessory
let presence: EnginePeer.Presence?
let rightAccessory: RightAccessory
@ -199,7 +214,7 @@ public final class PeerListItemComponent: Component {
avatar: Avatar? = nil,
peer: EnginePeer?,
storyStats: PeerStoryStats? = nil,
subtitle: String?,
subtitle: Subtitle?,
subtitleAccessory: SubtitleAccessory,
presence: EnginePeer.Presence?,
rightAccessory: RightAccessory = .none,
@ -574,15 +589,16 @@ public final class PeerListItemComponent: Component {
self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil && component.openStories != nil
let labelData: (String, Bool)
let labelData: (String, Subtitle.Color)
if let presence = component.presence {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat
labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp))
let labelDataValue = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp))
labelData = (labelDataValue.0, labelDataValue.1 ? .accent : .neutral)
} else if let subtitle = component.subtitle {
labelData = (subtitle, false)
labelData = (subtitle.text, subtitle.color)
} else {
labelData = ("", false)
labelData = ("", .neutral)
}
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
@ -779,10 +795,17 @@ public final class PeerListItemComponent: Component {
)
let labelAvailableWidth = component.style == .compact ? availableTextWidth - titleSize.width : availableSize.width - leftInset - rightInset
let labelColor: UIColor
switch labelData.1 {
case .neutral:
labelColor = component.theme.list.itemSecondaryTextColor
case .accent:
labelColor = component.theme.list.itemAccentColor
}
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: labelData.0, font: subtitleFont, textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
text: .plain(NSAttributedString(string: labelData.0, font: subtitleFont, textColor: labelColor))
)),
environment: {},
containerSize: CGSize(width: labelAvailableWidth, height: 100.0)

View File

@ -548,7 +548,7 @@ final class StoryItemSetViewListComponent: Component {
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: item.peer,
storyStats: item.storyStats,
subtitle: dateText,
subtitle: PeerListItemComponent.Subtitle(text: dateText, color: .neutral),
subtitleAccessory: subtitleAccessory,
presence: nil,
reaction: item.reaction.flatMap { reaction -> PeerListItemComponent.Reaction in
@ -929,7 +929,7 @@ final class StoryItemSetViewListComponent: Component {
sideInset: 0.0,
title: "AAAAAAAAAAAA",
peer: nil,
subtitle: "BBBBBBB",
subtitle: PeerListItemComponent.Subtitle(text: "BBBBBBB", color: .neutral),
subtitleAccessory: .checks,
presence: nil,
selectionState: .none,

View File

@ -906,22 +906,29 @@ public final class SharedAccountContextImpl: SharedAccountContext {
strongSelf.groupCallController = groupCallController
navigationController.pushViewController(groupCallController)
} else {
strongSelf.hasGroupCallOnScreenPromise.set(true)
let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
groupCallController.onViewDidAppear = { [weak self] in
if let strongSelf = self {
strongSelf.hasGroupCallOnScreenPromise.set(true)
let _ = (makeVoiceChatControllerInitialData(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
|> deliverOnMainQueue).start(next: { [weak strongSelf, weak navigationController] initialData in
guard let strongSelf, let navigationController else {
return
}
}
groupCallController.onViewDidDisappear = { [weak self] in
if let strongSelf = self {
strongSelf.hasGroupCallOnScreenPromise.set(false)
strongSelf.hasGroupCallOnScreenPromise.set(true)
let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call, initialData: initialData)
groupCallController.onViewDidAppear = { [weak strongSelf] in
if let strongSelf {
strongSelf.hasGroupCallOnScreenPromise.set(true)
}
}
}
groupCallController.navigationPresentation = .flatModal
groupCallController.parentNavigationController = navigationController
strongSelf.groupCallController = groupCallController
navigationController.pushViewController(groupCallController)
groupCallController.onViewDidDisappear = { [weak strongSelf] in
if let strongSelf {
strongSelf.hasGroupCallOnScreenPromise.set(false)
}
}
groupCallController.navigationPresentation = .flatModal
groupCallController.parentNavigationController = navigationController
strongSelf.groupCallController = groupCallController
navigationController.pushViewController(groupCallController)
})
}
strongSelf.hasOngoingCall.set(true)