mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Video chat screen V2
This commit is contained in:
parent
67ed88e951
commit
33e78cd5ed
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
159
submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift
Normal file
159
submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user