diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index e1b6734e36..b53bcf8d51 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -814,6 +814,7 @@ public final class _MediaStreamComponent: CombinedComponent { var videoHiddenForPip = false /// To update videoHiddenForPip var onExpandedFromPictureInPicture: ((State) -> Void)? + private let infoThrottler = Throttler.init(duration: 5, queue: .main) init(call: PresentationGroupCallImpl) { self.call = call @@ -821,7 +822,7 @@ public final class _MediaStreamComponent: CombinedComponent { if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() { self.isPictureInPictureSupported = true } else { - self.isPictureInPictureSupported = false + self.isPictureInPictureSupported = true } super.init() @@ -853,6 +854,22 @@ public final class _MediaStreamComponent: CombinedComponent { } var updated = false + // TODO: remove debug + Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in + strongSelf.infoThrottler.publish(members.totalCount/*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in + guard let strongSelf = strongSelf else { return } + var updated = false + let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount) + if strongSelf.originInfo != originInfo { + strongSelf.originInfo = originInfo + updated = true + } + // + if updated { + strongSelf.updated(transition: .immediate) + } + } + }.fire() if state.canManageCall != strongSelf.canManageCall { strongSelf.canManageCall = state.canManageCall updated = true @@ -873,12 +890,12 @@ public final class _MediaStreamComponent: CombinedComponent { updated = true } - let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount) - if strongSelf.originInfo != originInfo { - strongSelf.originInfo = originInfo - updated = true - } - +// let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount) +// if strongSelf.originInfo != originInfo { +// strongSelf.originInfo = originInfo +// updated = true +// } +// if updated { strongSelf.updated(transition: .immediate) } @@ -998,8 +1015,9 @@ public final class _MediaStreamComponent: CombinedComponent { } var isFullscreen = state.isFullscreen let isLandscape = context.availableSize.width > context.availableSize.height - if let videoSize = context.state.videoSize { - if videoSize.width > videoSize.height && isLandscape && !isFullscreen { + if let _ = context.state.videoSize { + // Always fullscreen in landscape + if /*videoSize.width > videoSize.height &&*/ isLandscape && !isFullscreen { state.isFullscreen = true isFullscreen = true } @@ -1500,7 +1518,9 @@ public final class _MediaStreamComponent: CombinedComponent { topOffset: context.availableSize.height - sheetHeight + context.state.dismissOffset, sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight), backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), - bottomPadding: bottomPadding + bottomPadding: bottomPadding, + participantsCount: // [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()! + context.state.originInfo?.memberCount ?? 0 ), availableSize: context.availableSize, transition: context.transition @@ -1582,7 +1602,8 @@ public final class _MediaStreamComponent: CombinedComponent { topOffset: context.availableSize.height - sheetHeight + context.state.dismissOffset, sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight), backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), - bottomPadding: 12 + bottomPadding: 12, + participantsCount: -1 // context.state.originInfo?.memberCount ?? 0 ), availableSize: context.availableSize, transition: context.transition @@ -1866,3 +1887,67 @@ public final class _MediaStreamComponentController: ViewControllerComponentConta } public typealias MediaStreamComponentController = _MediaStreamComponentController + +public final class Throttler { + public var duration: TimeInterval = 0.25 + public var queue: DispatchQueue = .main + public var isEnabled: Bool { duration > 0 } + + private var isThrottling: Bool = false + private var lastValue: T? + private var accumulator = Set() + private var lastCompletedValue: T? + + public init(duration: TimeInterval = 0.25, queue: DispatchQueue = .main) { + self.duration = duration + self.queue = queue + } + + public func publish(_ value: T, includingLatest: Bool = false, using completion: ((T) -> Void)?) { + accumulator.insert(value) + + if !isThrottling { + isThrottling = true + lastValue = nil + queue.async { + completion?(value) + self.lastCompletedValue = value + } + } else { + lastValue = value + } + + if lastValue == nil { + queue.asyncAfter(deadline: .now() + duration) { [self] in + accumulator.removeAll() + isThrottling = false + + guard + let lastValue = lastValue, + lastCompletedValue != lastValue || includingLatest + else { return } + + accumulator.insert(lastValue) + self.lastValue = nil + completion?(lastValue) + lastCompletedValue = lastValue + } + } + } + + public func cancelCurrent() { + lastValue = nil + isThrottling = false + accumulator.removeAll() + } + + public func canEmit(_ value: T) -> Bool { + !accumulator.contains(value) + } +} + +public extension Throttler where T == Bool { + func throttle(includingLatest: Bool = false, _ completion: ((T) -> Void)?) { + publish(true, includingLatest: includingLatest, using: completion) + } +} diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 6c294f44af..7048b8ad93 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -220,7 +220,7 @@ final class _MediaStreamVideoComponent: Component { let delegate = PlaybackDelegateImpl() delegate.onTransitionFinished = { [weak self] in if self?.videoView?.alpha == 0 { - self?.videoView?.alpha = 1 +// self?.videoView?.alpha = 1 } } return delegate diff --git a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift index 8310a1b4bd..4de232af33 100644 --- a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift @@ -17,8 +17,9 @@ final class StreamSheetComponent: CombinedComponent { let sheetHeight: CGFloat let topOffset: CGFloat let backgroundColor: UIColor - + let participantsCount: Int let bottomPadding: CGFloat + init( // color: UIColor, topComponent: AnyComponent, @@ -26,7 +27,8 @@ final class StreamSheetComponent: CombinedComponent { topOffset: CGFloat, sheetHeight: CGFloat, backgroundColor: UIColor, - bottomPadding: CGFloat + bottomPadding: CGFloat, + participantsCount: Int ) { // self.leftItem = leftItem self.topComponent = topComponent @@ -36,6 +38,7 @@ final class StreamSheetComponent: CombinedComponent { self.sheetHeight = sheetHeight self.backgroundColor = backgroundColor self.bottomPadding = bottomPadding + self.participantsCount = participantsCount } static func ==(lhs: StreamSheetComponent, rhs: StreamSheetComponent) -> Bool { @@ -60,6 +63,9 @@ final class StreamSheetComponent: CombinedComponent { if lhs.bottomPadding != rhs.bottomPadding { return false } + if lhs.participantsCount != rhs.participantsCount { + return false + } return true } // @@ -117,7 +123,7 @@ final class StreamSheetComponent: CombinedComponent { let background = Child(SheetBackgroundComponent.self) // let leftItem = Child(environment: Empty.self) let topItem = Child(environment: Empty.self) -// let viewerCounter = Child(environment: Empty.self) + let viewerCounter = Child(ParticipantsComponent.self) let bottomButtonsRow = Child(environment: Empty.self) // let bottomButtons = Child(environment: Empty.self) // let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) @@ -140,13 +146,11 @@ final class StreamSheetComponent: CombinedComponent { ) } -// let viewerCounter = context.component.viewerCounter.flatMap { viewerCounterComponent in -// return viewerCounter.update( -// component: viewerCounterComponent, -// availableSize: context.availableSize, -// transition: context.transition -// ) -// } + let viewerCounter = viewerCounter.update( + component: ParticipantsComponent(count: context.component.participantsCount), + availableSize: CGSize(width: context.availableSize.width, height: 70), + transition: context.transition + ) let bottomButtonsRow = context.component.bottomButtonsRow.flatMap { bottomButtonsRowComponent in return bottomButtonsRow.update( @@ -164,6 +168,7 @@ final class StreamSheetComponent: CombinedComponent { (context.view as? StreamSheetComponent.View)?.overlayComponentsFrames = [] context.view.backgroundColor = .clear + if let topItem = topItem { context.add(topItem .position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + contentHeight / 2.0)) @@ -171,13 +176,13 @@ final class StreamSheetComponent: CombinedComponent { (context.view as? StreamSheetComponent.View)?.overlayComponentsFrames.append(.init(x: 0, y: topOffset, width: topItem.size.width, height: topItem.size.height)) } -// if let viewerCounter = viewerCounter { -// let videoHeight = availableWidth / 2 -// let topRowHeight: CGFloat = 50 -// context.add(viewerCounter -// .position(CGPoint(x: viewerCounter.size.width / 2, y: topRowHeight + videoHeight + 32)) -// ) -// } + let animatedParticipantsVisible = context.component.participantsCount != -1 + if animatedParticipantsVisible { + // let videoHeight = availableWidth / 2 + context.add(viewerCounter + .position(CGPoint(x: context.availableSize.width / 2, y: topOffset + 50 + 200 + 40 + 30)) + ) + } if let bottomButtonsRow = bottomButtonsRow { context.add(bottomButtonsRow @@ -204,7 +209,7 @@ import TelegramPresentationData import TelegramStringFormatting private let purple = UIColor(rgb: 0x3252ef) -private let pink = UIColor(rgb: 0xef436c) +private let pink = UIColor(rgb: 0xe4436c) private let latePurple = UIColor(rgb: 0x974aa9) private let latePink = UIColor(rgb: 0xf0436c) @@ -312,3 +317,424 @@ final class SheetBackgroundComponent: Component { return availableSize } } + +final class ParticipantsComponent: Component { + static func == (lhs: ParticipantsComponent, rhs: ParticipantsComponent) -> Bool { + lhs.count == rhs.count + } + + func makeView() -> View { + View(frame: .zero) + } + + func update(view: View, availableSize: CGSize, state: ComponentFlow.EmptyComponentState, environment: ComponentFlow.Environment, transition: ComponentFlow.Transition) -> CGSize { + view.counter.update( + countString: count > 0 ? presentationStringsFormattedNumber(Int32(count), ",") : "", + subtitle: count > 0 ? "watching" : "no viewers" + )// environment.strings.LiveStream_NoViewers) + return availableSize + } + + private let count: Int + + init(count: Int) { + self.count = count + } + + final class View: UIView { + let counter = AnimatedCountView()// VoiceChatTimerNode.init(strings: .init(), dateTimeFormat: .init()) + + override init(frame: CGRect) { + super.init(frame: frame) + self.addSubview(counter) + counter.clipsToBounds = false + } + + override func layoutSubviews() { + super.layoutSubviews() + self.counter.frame = self.bounds + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + +} + +public final class AnimatedCountView: UIView { + let countLabel = AnimatedCountLabel() +// let titleLabel = UILabel() + let subtitleLabel = UILabel() + + private let foregroundView = UIView() + private let foregroundGradientLayer = CAGradientLayer() + private let maskingView = UIView() + + override init(frame: CGRect = .zero) { + super.init(frame: frame) + + self.foregroundGradientLayer.type = .radial + self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor] + self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0] + self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + self.foregroundView.mask = self.maskingView + self.foregroundView.layer.addSublayer(self.foregroundGradientLayer) + + self.addSubview(self.foregroundView) +// self.addSubview(self.titleLabel) + self.addSubview(self.subtitleLabel) + + self.maskingView.addSubview(countLabel) + + subtitleLabel.textAlignment = .center +// self.backgroundColor = UIColor.white.withAlphaComponent(0.1) + } + + override public func layoutSubviews() { + super.layoutSubviews() + + self.foregroundView.frame = CGRect(origin: CGPoint.zero, size: bounds.size)// .insetBy(dx: -40, dy: -40) + self.foregroundGradientLayer.frame = CGRect(origin: .zero, size: bounds.size).insetBy(dx: -60, dy: -60) + self.maskingView.frame = CGRect(origin: .zero, size: bounds.size) + countLabel.frame = CGRect(origin: .zero, size: bounds.size) + subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 10 : bounds.height, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20) + } + + func update(countString: String, subtitle: String) { + self.setupGradientAnimations() + + let text: String = countString// presentationStringsFormattedNumber(Int32(count), ",") + + // self.titleNode.attributedText = NSAttributedString(string: "", font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white) + // let titleSize = self.titleNode.updateLayout(size) + // self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height) + if CGFloat(text.count * 40) < bounds.width - 32 { + self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) + } else { + self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 54.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) + } +// var timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height)) +// if timerSize.width > size.width - 32.0 { +// self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) +// timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height)) +// } + +// self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 78.0, width: timerSize.width, height: timerSize.height) + + self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 16.0, design: .round, weight: .semibold, traits: []), textColor: .white) + self.subtitleLabel.isHidden = subtitle.isEmpty +// let subtitleSize = self.subtitleNode.updateLayout(size) +// self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: subtitleSize.width, height: subtitleSize.height) + +// self.foregroundView.frame = CGRect(origin: CGPoint(), size: size) + // self.setNeedsLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupGradientAnimations() { + if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.foregroundGradientLayer.startPoint + let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45)) + self.foregroundGradientLayer.startPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in +// if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { + self?.setupGradientAnimations() +// } + } + self.foregroundGradientLayer.add(animation, forKey: "movement") + CATransaction.commit() + } + } +} + +class AnimatedCharLayer: CATextLayer { + var text: String? { + get { + self.string as? String ?? (self.string as? NSAttributedString)?.string + } + set { + self.string = newValue + } + } + var attributedText: NSAttributedString? { + get { + self.string as? NSAttributedString //?? (self.string as? String).map { NSAttributed.init + } + set { + self.string = newValue + } + } + + var layer: CALayer { self } + + override init() { + super.init() + + self.contentsScale = UIScreen.main.scale + } + + override init(layer: Any) { + super.init(layer: layer) + self.contentsScale = UIScreen.main.scale + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class AnimatedCountLabel: UILabel { + override var text: String? { + get { + chars.reduce("") { $0 + ($1.text ?? "") } + } + set { + update(with: newValue ?? "") + } + } + + override var attributedText: NSAttributedString? { + get { + let string = NSMutableAttributedString() + for char in chars { + string.append(char.attributedText ?? NSAttributedString()) + } + return string + } + set { + udpateAttributed(with: newValue ?? NSAttributedString()) + } + } + + private var chars = [AnimatedCharLayer]() + private let containerView = UIView() + + override init(frame: CGRect = .zero) { + super.init(frame: frame) + + addSubview(containerView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + let interItemSpacing: CGFloat = 0 + let countWidth = chars.reduce(0) { $0 + $1.frame.width + interItemSpacing } - interItemSpacing + + containerView.frame = .init(x: bounds.midX - countWidth / 2, y: 0, width: countWidth, height: bounds.height) + chars.enumerated().forEach { (index, char) in + char.frame.origin.x = CGFloat(chars.count - 1 - index) * (40 + interItemSpacing) + char.frame.origin.y = 0 + } + } + /// Unused + func update(with newString: String) { + /*let itemWidth: CGFloat = 40 + let initialDuration: TimeInterval = 0.3 + let newChars = Array(newString).map { String($0) } + let currentChars = chars.map { $0.text ?? "X" } + +// let currentWidth = itemWidth * CGFloat(currentChars.count) + let newWidth = itemWidth * CGFloat(newChars.count) + + let interItemDelay: TimeInterval = 0.15 + var changeIndex = 0 + + var newLayers = [AnimatedCharLayer]() + + for index in 0..