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
@ -274,7 +274,9 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
resolvedTheme = resolvedTheme.withModalBlocksBackground()
|
resolvedTheme = resolvedTheme.withModalBlocksBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.node.presentationData = presentationData.withUpdated(theme: theme)
|
let presentationData = presentationData.withUpdated(theme: theme)
|
||||||
|
|
||||||
|
strongSelf.node.presentationData = presentationData
|
||||||
strongSelf.node.resolvedTheme = resolvedTheme
|
strongSelf.node.resolvedTheme = resolvedTheme
|
||||||
|
|
||||||
switch statusBarStyle {
|
switch statusBarStyle {
|
||||||
@ -283,7 +285,7 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
case .ignore:
|
case .ignore:
|
||||||
strongSelf.statusBar.statusBarStyle = .Ignore
|
strongSelf.statusBar.statusBarStyle = .Ignore
|
||||||
case .default:
|
case .default:
|
||||||
strongSelf.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
|
strongSelf.statusBar.statusBarStyle = resolvedTheme.rootController.statusBarStyle.style
|
||||||
}
|
}
|
||||||
|
|
||||||
let navigationBarPresentationData: NavigationBarPresentationData?
|
let navigationBarPresentationData: NavigationBarPresentationData?
|
||||||
@ -305,13 +307,14 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
}
|
}
|
||||||
}).strict()
|
}).strict()
|
||||||
|
|
||||||
|
let resolvedTheme = resolveTheme(baseTheme: presentationData.theme, theme: self.theme)
|
||||||
switch statusBarStyle {
|
switch statusBarStyle {
|
||||||
case .none:
|
case .none:
|
||||||
self.statusBar.statusBarStyle = .Hide
|
self.statusBar.statusBarStyle = .Hide
|
||||||
case .ignore:
|
case .ignore:
|
||||||
self.statusBar.statusBarStyle = .Ignore
|
self.statusBar.statusBarStyle = .Ignore
|
||||||
case .default:
|
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,
|
sideInset: 0.0,
|
||||||
title: peer.compactDisplayTitle,
|
title: peer.compactDisplayTitle,
|
||||||
peer: peer,
|
peer: peer,
|
||||||
subtitle: subtitle,
|
subtitle: PeerListItemComponent.Subtitle(text: subtitle, color: .neutral),
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: hasSelection ? .editing(isSelected: state.selectedSlots.contains(boost.slot), isTinted: false) : .none,
|
selectionState: hasSelection ? .editing(isSelected: state.selectedSlots.contains(boost.slot), isTinted: false) : .none,
|
||||||
|
@ -112,6 +112,8 @@ swift_library(
|
|||||||
"//submodules/MetalEngine",
|
"//submodules/MetalEngine",
|
||||||
"//submodules/TelegramUI/Components/Calls/VoiceChatActionButton",
|
"//submodules/TelegramUI/Components/Calls/VoiceChatActionButton",
|
||||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||||
|
"//submodules/TelegramUI/Components/LottieComponent",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//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 {
|
switch self {
|
||||||
case let .call(callContext):
|
case let .call(callContext):
|
||||||
return callContext.video(endpointId: endpointId)
|
return callContext.video(endpointId: endpointId)
|
||||||
@ -663,6 +663,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
private var ssrcMapping: [UInt32: SsrcMapping] = [:]
|
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 summaryInfoState = Promise<SummaryInfoState?>(nil)
|
||||||
private var summaryParticipantsState = Promise<SummaryParticipantsState?>(nil)
|
private var summaryParticipantsState = Promise<SummaryParticipantsState?>(nil)
|
||||||
|
|
||||||
@ -1695,6 +1698,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
self.genericCallContext = genericCallContext
|
self.genericCallContext = genericCallContext
|
||||||
self.stateVersionValue += 1
|
self.stateVersionValue += 1
|
||||||
|
|
||||||
|
genericCallContext.setRequestedVideoChannels(self.requestedVideoChannels)
|
||||||
|
self.connectPendingVideoSubscribers()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.joinDisposable.set((genericCallContext.joinPayload
|
self.joinDisposable.set((genericCallContext.joinPayload
|
||||||
@ -3055,7 +3061,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo]) {
|
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 mappedMinQuality: OngoingGroupCallContext.VideoChannel.Quality
|
||||||
let mappedMaxQuality: OngoingGroupCallContext.VideoChannel.Quality
|
let mappedMaxQuality: OngoingGroupCallContext.VideoChannel.Quality
|
||||||
switch item.minQuality {
|
switch item.minQuality {
|
||||||
@ -3083,7 +3089,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
minQuality: mappedMinQuality,
|
minQuality: mappedMinQuality,
|
||||||
maxQuality: mappedMaxQuality
|
maxQuality: mappedMaxQuality
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
if let genericCallContext = self.genericCallContext {
|
||||||
|
genericCallContext.setRequestedVideoChannels(self.requestedVideoChannels)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
|
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
|
||||||
@ -3538,7 +3547,49 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError>? {
|
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) {
|
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 PlainButtonComponent
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
import MetalEngine
|
import TelegramPresentationData
|
||||||
import CallScreen
|
import PeerListItemComponent
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class VideoChatParticipantsComponent: Component {
|
final class VideoChatParticipantsComponent: Component {
|
||||||
let call: PresentationGroupCall
|
let call: PresentationGroupCall
|
||||||
let members: PresentationGroupCallMembers?
|
let members: PresentationGroupCallMembers?
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let sideInset: CGFloat
|
||||||
|
|
||||||
init(
|
init(
|
||||||
call: PresentationGroupCall,
|
call: PresentationGroupCall,
|
||||||
members: PresentationGroupCallMembers?
|
members: PresentationGroupCallMembers?,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
sideInset: CGFloat
|
||||||
) {
|
) {
|
||||||
self.call = call
|
self.call = call
|
||||||
self.members = members
|
self.members = members
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.sideInset = sideInset
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
|
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
|
||||||
if lhs.members != rhs.members {
|
if lhs.members != rhs.members {
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,26 +55,35 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class ItemLayout {
|
private final class ItemLayout {
|
||||||
|
struct Grid {
|
||||||
let containerSize: CGSize
|
let containerSize: CGSize
|
||||||
|
let sideInset: CGFloat
|
||||||
let itemCount: Int
|
let itemCount: Int
|
||||||
let itemSize: CGSize
|
let itemSize: CGSize
|
||||||
let itemSpacing: CGFloat
|
let itemSpacing: CGFloat
|
||||||
let lastItemSize: CGFloat
|
let lastItemSize: CGFloat
|
||||||
let itemsPerRow: Int
|
let itemsPerRow: Int
|
||||||
|
|
||||||
init(containerSize: CGSize, itemCount: Int) {
|
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int) {
|
||||||
self.containerSize = containerSize
|
self.containerSize = containerSize
|
||||||
|
self.sideInset = sideInset
|
||||||
self.itemCount = itemCount
|
self.itemCount = itemCount
|
||||||
|
|
||||||
let width: CGFloat = containerSize.width
|
let width: CGFloat = containerSize.width - sideInset * 2.0
|
||||||
|
|
||||||
self.itemSpacing = 1.0
|
self.itemSpacing = 4.0
|
||||||
|
|
||||||
let itemsPerRow: CGFloat = CGFloat(3)
|
let itemsPerRow: Int
|
||||||
|
if itemCount == 1 {
|
||||||
|
itemsPerRow = 1
|
||||||
|
} else {
|
||||||
|
itemsPerRow = 2
|
||||||
|
}
|
||||||
self.itemsPerRow = Int(itemsPerRow)
|
self.itemsPerRow = Int(itemsPerRow)
|
||||||
|
|
||||||
let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
|
let itemWidth = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / CGFloat(itemsPerRow))
|
||||||
self.itemSize = CGSize(width: itemSize, height: itemSize)
|
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)
|
||||||
}
|
}
|
||||||
@ -296,7 +92,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
let row = index / self.itemsPerRow
|
let row = index / self.itemsPerRow
|
||||||
let column = index % self.itemsPerRow
|
let column = index % self.itemsPerRow
|
||||||
|
|
||||||
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))
|
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
|
return frame
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,19 +100,159 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
return self.frame(at: self.itemCount - 1).maxY
|
return self.frame(at: self.itemCount - 1).maxY
|
||||||
}
|
}
|
||||||
|
|
||||||
func visibleItemRange(for rect: CGRect, count: Int) -> (minIndex: Int, maxIndex: Int) {
|
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)
|
let offsetRect = rect.offsetBy(dx: 0.0, dy: 0.0)
|
||||||
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
|
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
|
||||||
minVisibleRow = max(0, minVisibleRow)
|
minVisibleRow = max(0, minVisibleRow)
|
||||||
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing)))
|
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing)))
|
||||||
|
|
||||||
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
||||||
let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
|
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
|
||||||
|
|
||||||
return (minVisibleIndex, maxVisibleIndex)
|
return (minVisibleIndex, maxVisibleIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct List {
|
||||||
|
let containerSize: CGSize
|
||||||
|
let sideInset: CGFloat
|
||||||
|
let itemCount: Int
|
||||||
|
let itemHeight: CGFloat
|
||||||
|
let trailingItemHeight: CGFloat
|
||||||
|
|
||||||
|
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 {
|
||||||
|
var result: CGFloat = 0.0
|
||||||
|
if self.grid.itemCount != 0 {
|
||||||
|
result += self.grid.contentHeight()
|
||||||
|
result += self.spacing
|
||||||
|
}
|
||||||
|
result += self.list.contentHeight()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class View: UIView, UIScrollViewDelegate {
|
final class View: UIView, UIScrollViewDelegate {
|
||||||
private let scrollView: ScrollView
|
private let scrollView: ScrollView
|
||||||
|
|
||||||
@ -325,7 +261,17 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
|
|
||||||
private var ignoreScrolling: Bool = false
|
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?
|
private var itemLayout: ItemLayout?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
@ -343,7 +289,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
self.scrollView.showsVerticalScrollIndicator = false
|
self.scrollView.showsVerticalScrollIndicator = false
|
||||||
self.scrollView.showsHorizontalScrollIndicator = false
|
self.scrollView.showsHorizontalScrollIndicator = false
|
||||||
self.scrollView.alwaysBounceHorizontal = false
|
self.scrollView.alwaysBounceHorizontal = false
|
||||||
self.scrollView.alwaysBounceVertical = true
|
self.scrollView.alwaysBounceVertical = false
|
||||||
self.scrollView.scrollsToTop = false
|
self.scrollView.scrollsToTop = false
|
||||||
self.scrollView.delegate = self
|
self.scrollView.delegate = self
|
||||||
self.scrollView.clipsToBounds = true
|
self.scrollView.clipsToBounds = true
|
||||||
@ -366,31 +312,31 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var validItemIds: [EnginePeer.Id] = []
|
var validGridItemIds: [VideoParticipant.Key] = []
|
||||||
if let members = component.members {
|
let visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
|
||||||
let visibleItemRange = itemLayout.visibleItemRange(for: self.scrollView.bounds, count: itemLayout.itemCount)
|
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
|
||||||
if visibleItemRange.maxIndex >= visibleItemRange.minIndex {
|
for i in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
|
||||||
for i in visibleItemRange.minIndex ... visibleItemRange.maxIndex {
|
let videoParticipant = self.gridParticipants[i]
|
||||||
let participant = members.participants[i]
|
validGridItemIds.append(videoParticipant.key)
|
||||||
validItemIds.append(participant.peer.id)
|
|
||||||
|
|
||||||
var itemTransition = transition
|
var itemTransition = transition
|
||||||
let itemView: ComponentView<Empty>
|
let itemView: ComponentView<Empty>
|
||||||
if let current = self.itemViews[participant.peer.id] {
|
if let current = self.gridItemViews[videoParticipant.key] {
|
||||||
itemView = current
|
itemView = current
|
||||||
} else {
|
} else {
|
||||||
itemTransition = itemTransition.withAnimation(.none)
|
itemTransition = itemTransition.withAnimation(.none)
|
||||||
itemView = ComponentView()
|
itemView = ComponentView()
|
||||||
self.itemViews[participant.peer.id] = itemView
|
self.gridItemViews[videoParticipant.key] = itemView
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemFrame = itemLayout.frame(at: i)
|
let itemFrame = itemLayout.gridItemFrame(at: i)
|
||||||
|
|
||||||
let _ = itemView.update(
|
let _ = itemView.update(
|
||||||
transition: itemTransition,
|
transition: itemTransition,
|
||||||
component: AnyComponent(ParticipantVideoComponent(
|
component: AnyComponent(VideoChatParticipantVideoComponent(
|
||||||
call: component.call,
|
call: component.call,
|
||||||
participant: participant
|
participant: videoParticipant.participant,
|
||||||
|
isPresentation: videoParticipant.isPresentation
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: itemFrame.size
|
containerSize: itemFrame.size
|
||||||
@ -403,20 +349,109 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var removedItemIds: [EnginePeer.Id] = []
|
var removedGridItemIds: [VideoParticipant.Key] = []
|
||||||
for (itemId, itemView) in self.itemViews {
|
for (itemId, itemView) in self.gridItemViews {
|
||||||
if !validItemIds.contains(itemId) {
|
if !validGridItemIds.contains(itemId) {
|
||||||
removedItemIds.append(itemId)
|
removedGridItemIds.append(itemId)
|
||||||
|
|
||||||
if let itemComponentView = itemView.view {
|
if let itemComponentView = itemView.view {
|
||||||
itemComponentView.removeFromSuperview()
|
itemComponentView.removeFromSuperview()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for itemId in removedItemIds {
|
for itemId in removedGridItemIds {
|
||||||
self.itemViews.removeValue(forKey: itemId)
|
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
|
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
|
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] = []
|
var requestedVideo: [PresentationGroupCallRequestedVideo] = []
|
||||||
if let members = component.members {
|
if let members = component.members {
|
||||||
for participant in members.participants {
|
for participant in members.participants {
|
||||||
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
|
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
|
||||||
requestedVideo.append(videoChannel)
|
requestedVideo.append(videoChannel)
|
||||||
}
|
}
|
||||||
|
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
|
||||||
|
requestedVideo.append(videoChannel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)
|
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)
|
||||||
|
@ -9,15 +9,22 @@ import TelegramCore
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import PlainButtonComponent
|
import PlainButtonComponent
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import LottieComponent
|
||||||
|
import BundleIconComponent
|
||||||
|
import ContextUI
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
private final class VideoChatScreenComponent: Component {
|
private final class VideoChatScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
|
let initialData: VideoChatScreenV2Impl.InitialData
|
||||||
let call: PresentationGroupCall
|
let call: PresentationGroupCall
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
initialData: VideoChatScreenV2Impl.InitialData,
|
||||||
call: PresentationGroupCall
|
call: PresentationGroupCall
|
||||||
) {
|
) {
|
||||||
|
self.initialData = initialData
|
||||||
self.call = call
|
self.call = call
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,20 +32,57 @@ private final class VideoChatScreenComponent: Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct PanGestureState {
|
||||||
|
var offsetFraction: CGFloat
|
||||||
|
|
||||||
|
init(offsetFraction: CGFloat) {
|
||||||
|
self.offsetFraction = offsetFraction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class View: UIView {
|
final class View: UIView {
|
||||||
|
private let containerView: UIView
|
||||||
|
|
||||||
private var component: VideoChatScreenComponent?
|
private var component: VideoChatScreenComponent?
|
||||||
private var environment: ViewControllerComponentContainer.Environment?
|
private var environment: ViewControllerComponentContainer.Environment?
|
||||||
private weak var state: EmptyComponentState?
|
private weak var state: EmptyComponentState?
|
||||||
private var isUpdating: Bool = false
|
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 let participants = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private var peer: EnginePeer?
|
||||||
|
private var callState: PresentationGroupCallState?
|
||||||
|
private var stateDisposable: Disposable?
|
||||||
|
|
||||||
private var members: PresentationGroupCallMembers?
|
private var members: PresentationGroupCallMembers?
|
||||||
private var membersDisposable: Disposable?
|
private var membersDisposable: Disposable?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
|
self.containerView = UIView()
|
||||||
|
self.containerView.clipsToBounds = true
|
||||||
|
|
||||||
super.init(frame: frame)
|
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) {
|
required init?(coder: NSCoder) {
|
||||||
@ -46,9 +90,111 @@ private final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
self.stateDisposable?.dispose()
|
||||||
self.membersDisposable?.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 {
|
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
defer {
|
defer {
|
||||||
@ -59,6 +205,10 @@ private final class VideoChatScreenComponent: Component {
|
|||||||
let themeUpdated = self.environment?.theme !== environment.theme
|
let themeUpdated = self.environment?.theme !== environment.theme
|
||||||
|
|
||||||
if self.component == nil {
|
if self.component == nil {
|
||||||
|
self.peer = component.initialData.peer
|
||||||
|
self.members = component.initialData.members
|
||||||
|
self.callState = component.initialData.callState
|
||||||
|
|
||||||
self.membersDisposable = (component.call.members
|
self.membersDisposable = (component.call.members
|
||||||
|> deliverOnMainQueue).startStrict(next: { [weak self] members in
|
|> deliverOnMainQueue).startStrict(next: { [weak self] members in
|
||||||
guard let self else {
|
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
|
self.component = component
|
||||||
@ -79,60 +243,273 @@ private final class VideoChatScreenComponent: Component {
|
|||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
if themeUpdated {
|
if themeUpdated {
|
||||||
self.backgroundColor = .black
|
self.containerView.backgroundColor = .black
|
||||||
}
|
}
|
||||||
|
|
||||||
let closeButtonSize = self.closeButton.update(
|
var containerOffset: CGFloat = 0.0
|
||||||
transition: transition,
|
if let panGestureState = self.panGestureState {
|
||||||
component: AnyComponent(PlainButtonComponent(
|
containerOffset = panGestureState.offsetFraction * availableSize.height
|
||||||
content: AnyComponent(Text(
|
self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius
|
||||||
text: "Leave", font: Font.regular(16.0), color: environment.theme.list.itemDestructiveColor)),
|
}
|
||||||
effectAlignment: .center,
|
|
||||||
minSize: CGSize(width: 44.0, height: 44.0),
|
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerOffset), size: availableSize), completion: { [weak self] completed in
|
||||||
contentInsets: UIEdgeInsets(),
|
guard let self, completed else {
|
||||||
action: { [weak self] in
|
|
||||||
guard let self, let component = self.component else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if self.panGestureState == nil {
|
||||||
let _ = component.call.leave(terminateIfPossible: false).startStandalone()
|
self.containerView.layer.cornerRadius = 0.0
|
||||||
|
}
|
||||||
if let controller = self.environment?.controller() {
|
if self.notifyDismissedInteractivelyOnPanGestureApply {
|
||||||
controller.dismiss()
|
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(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,
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.openMoreMenu()
|
||||||
}
|
}
|
||||||
},
|
|
||||||
animateAlpha: true,
|
|
||||||
animateScale: true,
|
|
||||||
animateContents: false
|
|
||||||
)),
|
)),
|
||||||
environment: {},
|
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 {
|
let navigationRightButtonSize = self.navigationRightButton.update(
|
||||||
if closeButtonView.superview == nil {
|
transition: .immediate,
|
||||||
self.addSubview(closeButtonView)
|
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
|
||||||
}
|
}
|
||||||
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
|
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: 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(
|
let participantsSize = self.participants.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(VideoChatParticipantsComponent(
|
component: AnyComponent(VideoChatParticipantsComponent(
|
||||||
call: component.call,
|
call: component.call,
|
||||||
members: self.members
|
members: self.members,
|
||||||
|
theme: environment.theme,
|
||||||
|
strings: environment.strings,
|
||||||
|
sideInset: sideInset
|
||||||
)),
|
)),
|
||||||
environment: {},
|
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 let participantsView = self.participants.view {
|
||||||
if participantsView.superview == nil {
|
if participantsView.superview == nil {
|
||||||
self.addSubview(participantsView)
|
self.containerView.addSubview(participantsView)
|
||||||
}
|
}
|
||||||
transition.setFrame(view: participantsView, frame: participantsFrame)
|
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
|
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 let call: PresentationGroupCall
|
||||||
public var currentOverlayController: VoiceChatOverlayController?
|
public var currentOverlayController: VoiceChatOverlayController?
|
||||||
public var parentNavigationController: NavigationController?
|
public var parentNavigationController: NavigationController?
|
||||||
@ -154,25 +547,38 @@ public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, Voic
|
|||||||
public var onViewDidAppear: (() -> Void)?
|
public var onViewDidAppear: (() -> Void)?
|
||||||
public var onViewDidDisappear: (() -> Void)?
|
public var onViewDidDisappear: (() -> Void)?
|
||||||
|
|
||||||
private var isDismissed: Bool = false
|
private var isDismissed: Bool = true
|
||||||
private var didAppearOnce: Bool = false
|
private var didAppearOnce: Bool = false
|
||||||
|
private var isAnimatingDismiss: Bool = false
|
||||||
|
|
||||||
private var idleTimerExtensionDisposable: Disposable?
|
private var idleTimerExtensionDisposable: Disposable?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
initialData: InitialData,
|
||||||
call: PresentationGroupCall
|
call: PresentationGroupCall
|
||||||
) {
|
) {
|
||||||
self.call = call
|
self.call = call
|
||||||
|
|
||||||
|
let theme = customizeDefaultDarkPresentationTheme(
|
||||||
|
theme: defaultDarkPresentationTheme,
|
||||||
|
editing: false,
|
||||||
|
title: nil,
|
||||||
|
accentColor: UIColor(rgb: 0x3E88F7),
|
||||||
|
backgroundColors: [],
|
||||||
|
bubbleColors: [],
|
||||||
|
animateBubbleColors: false
|
||||||
|
)
|
||||||
|
|
||||||
super.init(
|
super.init(
|
||||||
context: call.accountContext,
|
context: call.accountContext,
|
||||||
component: VideoChatScreenComponent(
|
component: VideoChatScreenComponent(
|
||||||
|
initialData: initialData,
|
||||||
call: call
|
call: call
|
||||||
),
|
),
|
||||||
navigationBarAppearance: .none,
|
navigationBarAppearance: .none,
|
||||||
statusBarStyle: .ignore,
|
statusBarStyle: .default,
|
||||||
presentationMode: .default,
|
presentationMode: .default,
|
||||||
theme: .dark
|
theme: .custom(theme)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,13 +593,17 @@ public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, Voic
|
|||||||
override public func viewDidAppear(_ animated: Bool) {
|
override public func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if self.isDismissed {
|
||||||
self.isDismissed = false
|
self.isDismissed = false
|
||||||
|
|
||||||
|
if let componentView = self.node.hostView.componentView as? VideoChatScreenComponent.View {
|
||||||
|
componentView.animateIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !self.didAppearOnce {
|
if !self.didAppearOnce {
|
||||||
self.didAppearOnce = true
|
self.didAppearOnce = true
|
||||||
|
|
||||||
//self.controllerNode.animateIn()
|
|
||||||
|
|
||||||
self.idleTimerExtensionDisposable?.dispose()
|
self.idleTimerExtensionDisposable?.dispose()
|
||||||
self.idleTimerExtensionDisposable = self.call.accountContext.sharedContext.applicationBindings.pushIdleTimerExtension()
|
self.idleTimerExtensionDisposable = self.call.accountContext.sharedContext.applicationBindings.pushIdleTimerExtension()
|
||||||
}
|
}
|
||||||
@ -208,7 +618,9 @@ public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, Voic
|
|||||||
self.idleTimerExtensionDisposable = nil
|
self.idleTimerExtensionDisposable = nil
|
||||||
|
|
||||||
self.didAppearOnce = false
|
self.didAppearOnce = false
|
||||||
|
if !self.isDismissed {
|
||||||
self.isDismissed = true
|
self.isDismissed = true
|
||||||
|
}
|
||||||
|
|
||||||
self.onViewDidDisappear?()
|
self.onViewDidDisappear?()
|
||||||
}
|
}
|
||||||
@ -216,4 +628,42 @@ public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, Voic
|
|||||||
public func dismiss(closing: Bool, manual: Bool) {
|
public func dismiss(closing: Bool, manual: Bool) {
|
||||||
self.dismiss()
|
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?) {
|
private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
|
||||||
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
|
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
|
||||||
if let controller = self.controller {
|
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)
|
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 controller: ViewController
|
||||||
private let sourceNode: ContextReferenceContentNode
|
private let sourceView: UIView
|
||||||
|
|
||||||
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
|
init(controller: ViewController, sourceView: UIView) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.sourceNode = sourceNode
|
self.sourceView = sourceView
|
||||||
}
|
}
|
||||||
|
|
||||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
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 {
|
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 {
|
} else {
|
||||||
return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call)
|
return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call)
|
||||||
}
|
}
|
||||||
|
@ -225,7 +225,7 @@ final class ContextResultPanelComponent: Component {
|
|||||||
sideInset: itemLayout.sideInset,
|
sideInset: itemLayout.sideInset,
|
||||||
title: peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
title: peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
||||||
peer: peer,
|
peer: peer,
|
||||||
subtitle: peer.addressName.flatMap { "@\($0)" },
|
subtitle: peer.addressName.flatMap { PeerListItemComponent.Subtitle(text: "@\($0)", color: .neutral) },
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
@ -294,7 +294,7 @@ final class ContextResultPanelComponent: Component {
|
|||||||
sideInset: sideInset,
|
sideInset: sideInset,
|
||||||
title: "AAAAAAAAAAAA",
|
title: "AAAAAAAAAAAA",
|
||||||
peer: nil,
|
peer: nil,
|
||||||
subtitle: "BBBBBBB",
|
subtitle: PeerListItemComponent.Subtitle(text: "BBBBBBB", color: .neutral),
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
|
@ -1357,7 +1357,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
|||||||
sideInset: 0.0,
|
sideInset: 0.0,
|
||||||
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
peer: peer.peer,
|
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,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
|
@ -507,7 +507,7 @@ final class BusinessRecipientListScreenComponent: Component {
|
|||||||
sideInset: 0.0,
|
sideInset: 0.0,
|
||||||
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
peer: peer.peer,
|
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,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
|
@ -1129,7 +1129,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
sideInset: itemLayout.sideInset,
|
sideInset: itemLayout.sideInset,
|
||||||
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
peer: peer,
|
peer: peer,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle.flatMap { PeerListItemComponent.Subtitle(text: $0, color: .neutral) },
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
rightAccessory: accessory,
|
rightAccessory: accessory,
|
||||||
@ -1465,7 +1465,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
sideInset: itemLayout.sideInset,
|
sideInset: itemLayout.sideInset,
|
||||||
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
peer: peer,
|
peer: peer,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle.flatMap { PeerListItemComponent.Subtitle(text: $0, color: .neutral) },
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: stateValue.presences[peer.id],
|
presence: stateValue.presences[peer.id],
|
||||||
selectionState: .editing(isSelected: isSelected, isTinted: false),
|
selectionState: .editing(isSelected: isSelected, isTinted: false),
|
||||||
@ -2346,7 +2346,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
sideInset: sideInset,
|
sideInset: sideInset,
|
||||||
title: "Name",
|
title: "Name",
|
||||||
peer: nil,
|
peer: nil,
|
||||||
subtitle: isContactsSearch ? "" : "sub",
|
subtitle: PeerListItemComponent.Subtitle(text: isContactsSearch ? "" : "sub", color: .neutral),
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .editing(isSelected: false, isTinted: false),
|
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 context: AccountContext
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
@ -173,7 +188,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
let avatar: Avatar?
|
let avatar: Avatar?
|
||||||
let peer: EnginePeer?
|
let peer: EnginePeer?
|
||||||
let storyStats: PeerStoryStats?
|
let storyStats: PeerStoryStats?
|
||||||
let subtitle: String?
|
let subtitle: Subtitle?
|
||||||
let subtitleAccessory: SubtitleAccessory
|
let subtitleAccessory: SubtitleAccessory
|
||||||
let presence: EnginePeer.Presence?
|
let presence: EnginePeer.Presence?
|
||||||
let rightAccessory: RightAccessory
|
let rightAccessory: RightAccessory
|
||||||
@ -199,7 +214,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
avatar: Avatar? = nil,
|
avatar: Avatar? = nil,
|
||||||
peer: EnginePeer?,
|
peer: EnginePeer?,
|
||||||
storyStats: PeerStoryStats? = nil,
|
storyStats: PeerStoryStats? = nil,
|
||||||
subtitle: String?,
|
subtitle: Subtitle?,
|
||||||
subtitleAccessory: SubtitleAccessory,
|
subtitleAccessory: SubtitleAccessory,
|
||||||
presence: EnginePeer.Presence?,
|
presence: EnginePeer.Presence?,
|
||||||
rightAccessory: RightAccessory = .none,
|
rightAccessory: RightAccessory = .none,
|
||||||
@ -574,15 +589,16 @@ public final class PeerListItemComponent: Component {
|
|||||||
|
|
||||||
self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil && component.openStories != nil
|
self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil && component.openStories != nil
|
||||||
|
|
||||||
let labelData: (String, Bool)
|
let labelData: (String, Subtitle.Color)
|
||||||
if let presence = component.presence {
|
if let presence = component.presence {
|
||||||
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||||
let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat
|
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 {
|
} else if let subtitle = component.subtitle {
|
||||||
labelData = (subtitle, false)
|
labelData = (subtitle.text, subtitle.color)
|
||||||
} else {
|
} else {
|
||||||
labelData = ("", false)
|
labelData = ("", .neutral)
|
||||||
}
|
}
|
||||||
|
|
||||||
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
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 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(
|
let labelSize = self.label.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(MultilineTextComponent(
|
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: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: labelAvailableWidth, height: 100.0)
|
containerSize: CGSize(width: labelAvailableWidth, height: 100.0)
|
||||||
|
@ -548,7 +548,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
||||||
peer: item.peer,
|
peer: item.peer,
|
||||||
storyStats: item.storyStats,
|
storyStats: item.storyStats,
|
||||||
subtitle: dateText,
|
subtitle: PeerListItemComponent.Subtitle(text: dateText, color: .neutral),
|
||||||
subtitleAccessory: subtitleAccessory,
|
subtitleAccessory: subtitleAccessory,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
reaction: item.reaction.flatMap { reaction -> PeerListItemComponent.Reaction in
|
reaction: item.reaction.flatMap { reaction -> PeerListItemComponent.Reaction in
|
||||||
@ -929,7 +929,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
sideInset: 0.0,
|
sideInset: 0.0,
|
||||||
title: "AAAAAAAAAAAA",
|
title: "AAAAAAAAAAAA",
|
||||||
peer: nil,
|
peer: nil,
|
||||||
subtitle: "BBBBBBB",
|
subtitle: PeerListItemComponent.Subtitle(text: "BBBBBBB", color: .neutral),
|
||||||
subtitleAccessory: .checks,
|
subtitleAccessory: .checks,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
|
@ -906,15 +906,21 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
strongSelf.groupCallController = groupCallController
|
strongSelf.groupCallController = groupCallController
|
||||||
navigationController.pushViewController(groupCallController)
|
navigationController.pushViewController(groupCallController)
|
||||||
} else {
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
strongSelf.hasGroupCallOnScreenPromise.set(true)
|
strongSelf.hasGroupCallOnScreenPromise.set(true)
|
||||||
let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
|
let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call, initialData: initialData)
|
||||||
groupCallController.onViewDidAppear = { [weak self] in
|
groupCallController.onViewDidAppear = { [weak strongSelf] in
|
||||||
if let strongSelf = self {
|
if let strongSelf {
|
||||||
strongSelf.hasGroupCallOnScreenPromise.set(true)
|
strongSelf.hasGroupCallOnScreenPromise.set(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
groupCallController.onViewDidDisappear = { [weak self] in
|
groupCallController.onViewDidDisappear = { [weak strongSelf] in
|
||||||
if let strongSelf = self {
|
if let strongSelf {
|
||||||
strongSelf.hasGroupCallOnScreenPromise.set(false)
|
strongSelf.hasGroupCallOnScreenPromise.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -922,6 +928,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
groupCallController.parentNavigationController = navigationController
|
groupCallController.parentNavigationController = navigationController
|
||||||
strongSelf.groupCallController = groupCallController
|
strongSelf.groupCallController = groupCallController
|
||||||
navigationController.pushViewController(groupCallController)
|
navigationController.pushViewController(groupCallController)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.hasOngoingCall.set(true)
|
strongSelf.hasOngoingCall.set(true)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user