From cd09a54fe4734dd4097dfbd3d036e8a8d19321d8 Mon Sep 17 00:00:00 2001 From: Ilya Yelagov Date: Thu, 8 Dec 2022 08:43:31 +0400 Subject: [PATCH] Refactoring --- .../Components/AnimatedCountView.swift | 465 -- .../Components/MediaStreamComponent.swift | 2495 +++--- .../MediaStreamVideoComponent.swift | 174 +- .../Components/StreamSheetComponent.swift | 9 +- .../Sources/MediaStreamingController.swift | 7118 ----------------- .../Sources/SharedAccountContext.swift | 3 +- 6 files changed, 1226 insertions(+), 9038 deletions(-) delete mode 100644 submodules/TelegramCallsUI/Sources/Components/AnimatedCountView.swift delete mode 100644 submodules/TelegramCallsUI/Sources/MediaStreamingController.swift diff --git a/submodules/TelegramCallsUI/Sources/Components/AnimatedCountView.swift b/submodules/TelegramCallsUI/Sources/Components/AnimatedCountView.swift deleted file mode 100644 index 70b3a9cbbb..0000000000 --- a/submodules/TelegramCallsUI/Sources/Components/AnimatedCountView.swift +++ /dev/null @@ -1,465 +0,0 @@ -/*import Foundation -import UIKit - -import Display - -private let purple = UIColor(rgb: 0x3252ef) -private let pink = UIColor(rgb: 0xe4436c) - -private let latePurple = UIColor(rgb: 0x974aa9) -private let latePink = UIColor(rgb: 0xf0436c) - -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) - countLabel.clipsToBounds = false - 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 - 6, 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) - containerView.clipsToBounds = false - addSubview(containerView) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - var itemWidth: CGFloat { 36 } - var commaWidth: CGFloat { 8 } - var interItemSpacing: CGFloat { 0 } - - private func offsetForChar(at index: Int, within characters: [NSAttributedString]? = nil) -> CGFloat { - if let characters { - return characters[0.. Bool { + if lhs.call !== rhs.call { + return false + } + + return true + } + + public final class State: ComponentState { + private let call: PresentationGroupCallImpl + + private(set) var hasVideo: Bool = false + private var stateDisposable: Disposable? + private var infoDisposable: Disposable? + private var connectionDisposable: Disposable? + private var networkStateDisposable: Disposable? + + private(set) var originInfo: OriginInfo? + + private(set) var displayUI: Bool = true + var dismissOffset: CGFloat = 0.0 + var initialOffset: CGFloat = 0.0 + var storedIsFullscreen: Bool? + var isFullscreen: Bool = false + var videoSize: CGSize? + + private(set) var canManageCall: Bool = false + // TODO: also handle pictureInPicturePossible + let isPictureInPictureSupported: Bool + + private(set) var callTitle: String? + private(set) var recordingStartTimestamp: Int32? + + private(set) var peerTitle: String = "" + private(set) var chatPeer: Peer? + + private(set) var isVisibleInHierarchy: Bool = false + private var isVisibleInHierarchyDisposable: Disposable? + + private var scheduledDismissUITimer: SwiftSignalKit.Timer? + var videoStalled: Bool = true + + var videoIsPlayable: Bool { + !videoStalled && hasVideo + } + + let deactivatePictureInPictureIfVisible = StoredActionSlot(Void.self) + + private let infoThrottler = Throttler.init(duration: 5, queue: .main) + + init(call: PresentationGroupCallImpl) { + self.call = call + + if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() { + self.isPictureInPictureSupported = true + } else { + self.isPictureInPictureSupported = AVPictureInPictureController.isPictureInPictureSupported() + } + + super.init() + + self.stateDisposable = (call.state + |> map { state -> Bool in + switch state.networkState { + case .connected: + return true + default: + return false + } + } + |> filter { $0 } + |> take(1)).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.hasVideo = true + strongSelf.updated(transition: .immediate) + }) + + // TODO: retest to uncomment or delete. Relying only on video frames + /*self.networkStateDisposable = (call.account.networkState |> deliverOnMainQueue).start(next: { [weak self] state in + guard let strongSelf = self else { return } + switch state { + case .waitingForNetwork, .connecting: + print("[NEW] videoStalled") + strongSelf.videoStalled = true + default: + strongSelf.videoStalled = !strongSelf.hasVideo + } + strongSelf.updated(transition: .immediate) +// if let strongSelf = self, case .standard(previewing: false) = strongSelf.presentationInterfaceState.mode { +// strongSelf.chatTitleView?.networkState = state +// } + }) + + self.connectionDisposable = call.state.start(next: { [weak self] state in + let prev = self?.videoStalled + switch state.networkState { + case .connected: + self?.videoStalled = false + default: + print("[ALERT] video stalled") + self?.videoStalled = true + } + if prev != self?.videoStalled { + self?.updated(transition: .immediate) + } + })*/ + + let callPeer = call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) + + self.infoDisposable = (combineLatest(queue: .mainQueue(), call.state, call.members, callPeer) + |> deliverOnMainQueue).start(next: { [weak self] state, members, callPeer in + guard let strongSelf = self, let members = members, let callPeer = callPeer else { + return + } + + var updated = false +// TODO: remove debug timer +// Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in + strongSelf.infoThrottler.publish(members.totalCount/*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in + print(members.totalCount) + guard let strongSelf = strongSelf else { return } + var updated = false + let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: latestCount) + 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 + } + if strongSelf.peerTitle != callPeer.debugDisplayTitle { + strongSelf.peerTitle = callPeer.debugDisplayTitle + updated = true + } + strongSelf.chatPeer = callPeer._asPeer() + + if strongSelf.callTitle != state.title { + strongSelf.callTitle = state.title + updated = true + } + + if strongSelf.recordingStartTimestamp != state.recordingStartTimestamp { + strongSelf.recordingStartTimestamp = state.recordingStartTimestamp + 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) + } + }) + + self.isVisibleInHierarchyDisposable = (call.accountContext.sharedContext.applicationBindings.applicationInForeground + |> deliverOnMainQueue).start(next: { [weak self] inForeground in + guard let strongSelf = self else { + return + } + if strongSelf.isVisibleInHierarchy != inForeground { + strongSelf.isVisibleInHierarchy = inForeground + strongSelf.updated(transition: .immediate) + + if inForeground { + Queue.mainQueue().after(0.5, { + guard let strongSelf = self, strongSelf.isVisibleInHierarchy else { + return + } + + strongSelf.deactivatePictureInPictureIfVisible.invoke(Void()) + }) + } + } + }) + } + + deinit { + self.stateDisposable?.dispose() + self.infoDisposable?.dispose() + self.isVisibleInHierarchyDisposable?.dispose() + self.connectionDisposable?.dispose() + self.networkStateDisposable?.dispose() + } + + func toggleDisplayUI() { + self.displayUI = !self.displayUI + self.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .easeInOut))) + } + + func cancelScheduledDismissUI() { + self.scheduledDismissUITimer?.invalidate() + self.scheduledDismissUITimer = nil + } + + func scheduleDismissUI() { + if self.scheduledDismissUITimer == nil { + self.scheduledDismissUITimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.scheduledDismissUITimer = nil + if strongSelf.displayUI { + strongSelf.toggleDisplayUI() + } + }, queue: .mainQueue()) + self.scheduledDismissUITimer?.start() + } + } + + func updateDismissOffset(value: CGFloat, interactive: Bool) { + self.dismissOffset = value + if interactive { + self.updated(transition: .immediate) + } else { + self.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + } + } + } + + public func makeState() -> State { + return State(call: self.call) + } + + public static var body: Body { + let background = Child(Rectangle.self) + let dismissTapComponent = Child(Rectangle.self) + let video = Child(MediaStreamVideoComponent.self) +// let navigationBar = Child(NavigationBarComponent.self) +// let toolbar = Child(ToolbarComponent.self) + + let sheet = Child(StreamSheetComponent.self) + let fullscreenOverlay = Child(StreamSheetComponent.self) + + let activatePictureInPicture = StoredActionSlot(Action.self) + let deactivatePictureInPicture = StoredActionSlot(Void.self) + let moreButtonTag = GenericComponentViewTag() + let moreAnimationTag = GenericComponentViewTag() + + return { context in + let canEnforceOrientation = UIDevice.current.model != "iPad" + var forceFullScreenInLandscape: Bool { canEnforceOrientation && true } + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + if environment.isVisible { + } else { + context.state.dismissOffset = 0.0 + } + + let background = background.update( + component: Rectangle(color: .black.withAlphaComponent(0.0)), + availableSize: context.availableSize, + transition: context.transition + ) + + let call = context.component.call + let state = context.state + let controller = environment.controller + + context.state.deactivatePictureInPictureIfVisible.connect { + guard let controller = controller() else { + return + } + if controller.view.window == nil { + return + } + state.updated(transition: .easeInOut(duration: 3)) + deactivatePictureInPicture.invoke(Void()) + } + let isFullscreen: Bool // = state.isFullscreen + let isLandscape = context.availableSize.width > context.availableSize.height + +// if let videoSize = context.state.videoSize { + // Always fullscreen in landscape + // TODO: support landscape sheet (wrap in scrollview, video size same as portrait) + if forceFullScreenInLandscape && isLandscape && !state.isFullscreen { + state.isFullscreen = true + isFullscreen = true + } else if !isLandscape && state.isFullscreen && canEnforceOrientation { + state.isFullscreen = false + isFullscreen = false + } else { + isFullscreen = state.isFullscreen + } + // } + let videoInset: CGFloat + if !isFullscreen { + videoInset = 16 + } else { + videoInset = 0 + } + + let videoHeight: CGFloat = forceFullScreenInLandscape + ? (context.availableSize.width - videoInset * 2) / 16 * 9 + : context.state.videoSize?.height ?? (min(context.availableSize.width, context.availableSize.height) - videoInset * 2) / 16 * 9 + let bottomPadding = 40 + environment.safeInsets.bottom + let sheetHeight: CGFloat = isFullscreen + ? context.availableSize.height + : (44 + videoHeight + 40 + 69 + 16 + 32 + 70 + bottomPadding) + let isFullyDragged = context.availableSize.height - sheetHeight + state.dismissOffset - context.view.safeAreaInsets.top < 30 + + var dragOffset = context.state.dismissOffset + if isFullyDragged { + dragOffset = max(context.state.dismissOffset, sheetHeight - context.availableSize.height + context.view.safeAreaInsets.top)// sheetHeight - UIScreen.main.bounds.height + } + + let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - sheetHeight + dragOffset) + let dismissTapComponent = dismissTapComponent.update( + component: Rectangle(color: .red.withAlphaComponent(0)), + availableSize: CGSize(width: context.availableSize.width, height: dismissTapAreaHeight), + transition: context.transition + ) + + + let video = video.update( + component: MediaStreamVideoComponent( + call: context.component.call, + hasVideo: context.state.hasVideo, + isVisible: environment.isVisible && context.state.isVisibleInHierarchy, + isAdmin: context.state.canManageCall, + peerTitle: context.state.peerTitle, + isFullscreen: isFullscreen, + videoLoading: context.state.videoStalled, + callPeer: context.state.chatPeer, + activatePictureInPicture: activatePictureInPicture, + deactivatePictureInPicture: deactivatePictureInPicture, + bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in + guard let call = call else { + completed() + return + } + + call.accountContext.sharedContext.mainWindow?.inCallNavigate?() + completed() + }, + pictureInPictureClosed: { [weak call] in + let _ = call?.leave(terminateIfPossible: false) + }, + onVideoSizeRetrieved: { [weak state] size in + state?.videoSize = size + }, + onVideoPlaybackLiveChange: { [weak state] isLive in + guard let state else { return } + let wasLive = !state.videoStalled + if isLive != wasLive { + state.videoStalled = !isLive + state.updated() + } + } + ), + availableSize: context.availableSize, + transition: context.transition + ) + + var navigationRightItems: [AnyComponentWithIdentity] = [] + + if context.state.isPictureInPictureSupported, context.state.videoIsPlayable { + navigationRightItems.append(AnyComponentWithIdentity(id: "pip", component: AnyComponent(Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( + fillColor: .white.withAlphaComponent(0.08), + size: CGSize(width: 32.0, height: 32.0) + ))), + AnyComponentWithIdentity(id: "a", component: AnyComponent(BundleIconComponent( + name: "Call/pip", + tintColor: .white + ))) + ] + )), + action: { + activatePictureInPicture.invoke(Action { + guard let controller = controller() as? MediaStreamComponentController else { + return + } + controller.dismiss(closing: false, manual: true) + }) + } + ).minSize(CGSize(width: 44.0, height: 44.0))))) + } + var topLeftButton: AnyComponent? + if context.state.canManageCall { + let whiteColor = UIColor(white: 1.0, alpha: 1.0) + /*navigationRightItems.append(*/ topLeftButton = //AnyComponentWithIdentity(id: "more", component: + AnyComponent(Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( + fillColor: .white.withAlphaComponent(0.08), + size: CGSize(width: 32.0, height: 32.0) + ))), + AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_profilemore", + mode: .still(position: .begin) + ), + colors: [ + "Point 2.Group 1.Fill 1": whiteColor, + "Point 3.Group 1.Fill 1": whiteColor, + "Point 1.Group 1.Fill 1": whiteColor + ], + size: CGSize(width: 32.0, height: 32.0) + ).tagged(moreAnimationTag))), + ])), + action: { [weak call, weak state] in + guard let call = call, let state = state else { + return + } + guard let controller = controller() as? MediaStreamComponentController else { + return + } + guard let anchorView = controller.node.hostView.findTaggedView(tag: moreButtonTag) else { + return + } + + if let animationView = controller.node.hostView.findTaggedView(tag: moreAnimationTag) as? LottieAnimationComponent.View { + animationView.playOnce() + } + + let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_EditTitle, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak call, weak controller, weak state] _, a in + guard let call = call, let controller = controller, let state = state, let chatPeer = state.chatPeer else { + return + } + + let initialTitle = state.callTitle ?? "" + + let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } + + let title: String = presentationData.strings.LiveStream_EditTitle + let text: String = presentationData.strings.LiveStream_EditTitleText + + let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in + guard let call = call else { + return + } + + let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } + + if let title = title, title != initialTitle { + call.updateTitle(title) + + let text: String = title.isEmpty ? presentationData.strings.LiveStream_EditTitleRemoveSuccess : presentationData.strings.LiveStream_EditTitleSuccess(title).string + + let _ = text + //strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false }) + } + }) + controller.present(editController, in: .window(.root)) + + a(.default) + }))) + + if let recordingStartTimestamp = state.recordingStartTimestamp { + items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak call, weak controller] _, f in + f(.dismissWithoutContent) + + guard let call = call, let controller = controller else { + return + } + + let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } + + let alertController = textAlertController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: nil, text: presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_StopRecordingStop, action: { [weak call, weak controller] in + guard let call = call, let controller = controller else { + return + } + call.setShouldBeRecording(false, title: nil, videoOrientation: nil) + + let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } + let text = presentationData.strings.LiveStream_RecordingSaved + + let _ = text + let _ = controller + + /*strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in + if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + let context = strongSelf.context + strongSelf.controller?.dismiss(completion: { + Queue.mainQueue().justDispatch { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil)) + } + }) + + return true + } + return false + })*/ + })]) + controller.present(alertController, in: .window(.root)) + }), false)) + } else { + let text = presentationData.strings.LiveStream_StartRecording + items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in + return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) + }, action: { [weak call, weak state, weak controller] _, f in + f(.dismissWithoutContent) + + guard let call = call, let state = state, let _ = state.chatPeer, let controller = controller else { + return + } + + let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } + + let title: String + let text: String + let placeholder: String = presentationData.strings.VoiceChat_RecordingTitlePlaceholderVideo + + title = presentationData.strings.LiveStream_StartRecordingTitle + text = presentationData.strings.LiveStream_StartRecordingTextVideo + + let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in + guard let call = call, let controller = controller else { + return + } + + + let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } + + if let title = title { + call.setShouldBeRecording(true, title: title, videoOrientation: false) + + let text = presentationData.strings.LiveStream_RecordingStarted + let _ = text + + let _ = controller + + call.playTone(.recordingStarted) + } + }) + controller.present(editController, in: .window(.root)) + }))) + } + + let credentialsPromise = Promise() + credentialsPromise.set(call.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: call.peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal in return .never() }) + + items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_ViewCredentials, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor, backgroundColor: nil) + }, action: { [weak call, weak controller] _, a in + guard let call = call, let controller = controller else { + return + } + + controller.push(CreateExternalMediaStreamScreen(context: call.accountContext, peerId: call.peerId, credentialsPromise: credentialsPromise, mode: .view)) + + a(.default) + }))) + + items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.VoiceChat_StopRecordingStop, textColor: .destructive, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor, backgroundColor: nil) + }, action: { [weak call] _, a in + guard let call = call else { + return + } + + let _ = call.leave(terminateIfPossible: true).start() + + a(.default) + }))) + + final class ReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } + } + + let contextController = ContextController(account: call.accountContext.account, presentationData: presentationData.withUpdated(theme: defaultDarkPresentationTheme), source: .reference(ReferenceContentSource(sourceView: anchorView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + /*contextController.passthroughTouchEvent = { sourceView, point in + guard let strongSelf = self else { + return .ignore + } + + let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil) + guard let localResult = strongSelf.hitTest(localPoint, with: nil) else { + return .dismiss(consume: true, result: nil) + } + + var testView: UIView? = localResult + while true { + if let testViewValue = testView { + if let node = testViewValue.asyncdisplaykit_node as? PeerInfoHeaderNavigationButton { + node.isUserInteractionEnabled = false + DispatchQueue.main.async { + node.isUserInteractionEnabled = true + } + return .dismiss(consume: false, result: nil) + } else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoVisualMediaPaneNode { + node.brieflyDisableTouchActions() + return .dismiss(consume: false, result: nil) + } else { + testView = testViewValue.superview + } + } else { + break + } + } + + return .dismiss(consume: true, result: nil) + }*/ + controller.presentInGlobalOverlay(contextController) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(moreButtonTag))//)//) + } + let navigationComponent = NavigationBarComponent( + topInset: environment.statusBarHeight, + sideInset: environment.safeInsets.left, + leftItem: topLeftButton/*AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Close, font: Font.regular(17.0), color: .white)), + action: { [weak call] in + let _ = call?.leave(terminateIfPossible: false) + }) + )*/, + rightItems: navigationRightItems, + centerItem: AnyComponent(StreamTitleComponent(text: state.peerTitle, isRecording: state.recordingStartTimestamp != nil, isActive: context.state.videoIsPlayable)) + ) + +// let navigationBar = navigationBar.update( +// component: navigationComponent, +// availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), +// transition: context.transition +// ) + + if context.state.storedIsFullscreen != isFullscreen { + context.state.storedIsFullscreen = isFullscreen + if isFullscreen { + context.state.scheduleDismissUI() + } else { + context.state.cancelScheduledDismissUI() + } + } + + var infoItem: AnyComponent? + if let originInfo = context.state.originInfo { + let memberCountString: String + if originInfo.memberCount == 0 { + memberCountString = environment.strings.LiveStream_NoViewers + } else { + memberCountString = environment.strings.LiveStream_ViewerCount(Int32(originInfo.memberCount)) + } + infoItem = AnyComponent(OriginInfoComponent( + title: state.callTitle ?? originInfo.title, + subtitle: memberCountString + )) + } + let availableSize = context.availableSize + let safeAreaTop = context.view.safeAreaInsets.top + + let onPanGesture: ((Gesture.PanGestureState) -> Void) = { [weak state] panState in + guard let state = state else { + return + } + switch panState { + case .began: + state.initialOffset = state.dismissOffset + case let .updated(offset): + state.updateDismissOffset(value: state.initialOffset + offset.y, interactive: true) + case let .ended(velocity): + if velocity.y > 200.0 { + if state.isFullscreen { + state.isFullscreen = false + state.updateDismissOffset(value: 0.0, interactive: false) + if let controller = controller() as? MediaStreamComponentController { + controller.updateOrientation(orientation: .portrait) + } + } else { + if isFullyDragged || state.initialOffset != 0 { + state.updateDismissOffset(value: 0.0, interactive: false) + } else { + let _ = call.leave(terminateIfPossible: false) + } + } + } else { + if isFullyDragged { + state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false) + } else { + if velocity.y < -200 { + // Expand + state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false) + } else { + state.updateDismissOffset(value: 0.0, interactive: false) + } + } + } + } + } + + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + .gesture(.tap { [weak state] in + guard let state = state, state.isFullscreen else { + return + } + state.toggleDisplayUI() + }) + .gesture(.pan { panState in + onPanGesture(panState) + }) + ) +// var bottomComponent: AnyComponent? +// var fullScreenToolbarComponent: AnyComponent? + + context.add(dismissTapComponent + .position(CGPoint(x: context.availableSize.width / 2, y: dismissTapAreaHeight / 2)) + .gesture(.tap { + _ = call.leave(terminateIfPossible: false) + }) + .gesture(.pan(onPanGesture)) + ) + + if !isFullscreen { + let imageRenderScale = UIScreen.main.scale + let bottomComponent = AnyComponent(ButtonsRowComponent( + bottomInset: environment.safeInsets.bottom, + sideInset: environment.safeInsets.left, + leftItem: AnyComponent(Button( + content: AnyComponent(RoundGradientButtonComponent(// BundleIconComponent( + gradientColors: [UIColor(red: 0.18, green: 0.17, blue: 0.30, alpha: 1).cgColor, UIColor(red: 0.17, green: 0.16, blue: 0.30, alpha: 1).cgColor], + image: generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: .white), + // TODO: localize: + title: "share")), + action: { + guard let controller = controller() as? MediaStreamComponentController else { + return + } + controller.presentShare() + } + ).minSize(CGSize(width: 65, height: 80))), + rightItem: AnyComponent(Button( + content: AnyComponent(RoundGradientButtonComponent( + gradientColors: [UIColor(red: 0.44, green: 0.18, blue: 0.22, alpha: 1).cgColor, UIColor(red: 0.44, green: 0.18, blue: 0.22, alpha: 1).cgColor], + image: generateImage(CGSize(width: 44.0 * imageRenderScale, height: 44 * imageRenderScale), opaque: false, rotatedContext: { size, context in + context.translateBy(x: size.width / 2, y: size.height / 2) + context.scaleBy(x: 0.4, y: 0.4) + context.translateBy(x: -size.width / 2, y: -size.height / 2) + let imageColor = UIColor.white + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + let lineWidth: CGFloat = size.width / 7 + context.setLineWidth(lineWidth - UIScreenPixel) + context.setLineCap(.round) + context.setStrokeColor(imageColor.cgColor) + + context.move(to: CGPoint(x: lineWidth / 2 + UIScreenPixel, y: lineWidth / 2 + UIScreenPixel)) + context.addLine(to: CGPoint(x: size.width - lineWidth / 2 - UIScreenPixel, y: size.height - lineWidth / 2 - UIScreenPixel)) + context.strokePath() + + context.move(to: CGPoint(x: size.width - lineWidth / 2 - UIScreenPixel, y: lineWidth / 2 + UIScreenPixel)) + context.addLine(to: CGPoint(x: lineWidth / 2 + UIScreenPixel, y: size.height - lineWidth / 2 - UIScreenPixel)) + context.strokePath() + }), + title: "leave" + )), + action: { [weak call] in + let _ = call?.leave(terminateIfPossible: false) + } + ).minSize(CGSize(width: 44.0, height: 44.0))), + centerItem: AnyComponent(Button( + content: AnyComponent(RoundGradientButtonComponent( + gradientColors: [UIColor(red: 0.23, green: 0.17, blue: 0.29, alpha: 1).cgColor, UIColor(red: 0.21, green: 0.16, blue: 0.29, alpha: 1).cgColor], + image: generateImage(CGSize(width: 44 * imageRenderScale, height: 44 * imageRenderScale), opaque: false, rotatedContext: { size, context in + + let imageColor = UIColor.white + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setLineWidth(2.4 * imageRenderScale - UIScreenPixel) + context.setLineCap(.round) + context.setStrokeColor(imageColor.cgColor) +// context.setLineJoin(.round) + + let lineSide = size.width / 5 + let centerOffset = size.width / 20 + context.move(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - centerOffset / 2)) + context.addLine(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - lineSide)) + context.addLine(to: CGPoint(x: size.width / 2 + centerOffset / 2, y: size.height / 2 - lineSide)) + context.move(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - lineSide)) + context.addLine(to: CGPoint(x: size.width / 2 + centerOffset, y: size.height / 2 - centerOffset)) + context.strokePath() + + context.move(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + centerOffset / 2)) + context.addLine(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + lineSide)) + context.addLine(to: CGPoint(x: size.width / 2 - centerOffset / 2, y: size.height / 2 + lineSide)) + context.move(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + lineSide)) + context.addLine(to: CGPoint(x: size.width / 2 - centerOffset, y: size.height / 2 + centerOffset)) + context.strokePath() + }), + title: "expand" + )), + action: { + guard state.videoIsPlayable else { + state.isFullscreen = false + return + } + if let controller = controller() as? MediaStreamComponentController { + guard let size = state.videoSize else { return } + state.isFullscreen.toggle() + if state.isFullscreen { + if size.width > size.height { + controller.updateOrientation(orientation: .landscapeRight) + } else { + controller.updateOrientation(orientation: .portrait) + } + } else { + // TODO: Check and mind current device orientation + controller.updateOrientation(orientation: .portrait) + } + if !canEnforceOrientation { + state.updated() // updated(.easeInOut(duration: 0.3)) + } + // controller.updateOrientation(orientation: isLandscape ? .portrait : .landscapeRight) + } + } + ).minSize(CGSize(width: 44.0, height: 44.0))) + )) + + let sheet = sheet.update( + component: StreamSheetComponent( + topComponent: AnyComponent(navigationComponent), + bottomButtonsRow: bottomComponent, + topOffset: context.availableSize.height - sheetHeight + dragOffset, + sheetHeight: max(sheetHeight - dragOffset, sheetHeight), + backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), + bottomPadding: bottomPadding, + participantsCount: context.state.originInfo?.memberCount ?? 0, // Int.random(in: 0...999998)// [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()! + // + isFullyExtended: isFullyDragged, + deviceCornerRadius: (controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 0, + videoHeight: videoHeight + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let sheetOffset: CGFloat = context.availableSize.height - sheetHeight + dragOffset + let sheetPosition = sheetOffset + sheetHeight / 2 + // Sheet underneath the video when in sheet + context.add(sheet + .position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2)) + ) + let videoPos: CGFloat + + if isFullscreen { + videoPos = context.availableSize.height / 2 + dragOffset + } else { + videoPos = sheetPosition - sheetHeight / 2 + videoHeight / 2 + 50 + 12 + } + context.add(video + .position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos)/*sheetPosition + videoHeight / 2 + 50 - context.availableSize.height / 2*/)// context.availableSize.height / 2.0 + context.state.dismissOffset)) + ) + } else { + context.add(video + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2 + dragOffset) + )) + } + + if isFullscreen { + let fullScreenToolbarComponent = AnyComponent(ToolbarComponent( + bottomInset: environment.safeInsets.bottom, + sideInset: environment.safeInsets.left, + leftItem: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Accessory Panels/MessageSelectionForward", + tintColor: .white + )), + action: { + guard let controller = controller() as? MediaStreamComponentController else { + return + } + controller.presentShare() + } + ).minSize(CGSize(width: 64.0, height: 80))), + rightItem: state.hasVideo ? AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: isFullscreen ? "Media Gallery/Minimize" : "Media Gallery/Fullscreen", + tintColor: .white + )), + action: { + state.isFullscreen = false + if let controller = controller() as? MediaStreamComponentController { + if canEnforceOrientation { + controller.updateOrientation(orientation: .portrait) + } else { + state.updated() // updated(.easeInOut(duration: 0.3)) + } + } + } + ).minSize(CGSize(width: 64.0, height: 80))) : nil, + centerItem: infoItem + )) + let fullScreenOverlayComponent = fullscreenOverlay.update( + component: StreamSheetComponent( + topComponent: AnyComponent(navigationComponent), + bottomButtonsRow: fullScreenToolbarComponent, + topOffset: context.availableSize.height - sheetHeight + context.state.dismissOffset, + sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight), + backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), + bottomPadding: 12, + participantsCount: -1, // context.state.originInfo?.memberCount ?? 0 + isFullyExtended: isFullyDragged, + deviceCornerRadius: (controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 0, + videoHeight: videoHeight + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(fullScreenOverlayComponent + .position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2)) + .opacity(state.displayUI ? 1 : 0) + ) + } + +// context.add(navigationBar +// .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height / 2.0)) +// .opacity(context.state.displayUI ? 1.0 : 0.0) +// ) + +// context.add(toolbar +// .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) +// .opacity(context.state.displayUI ? 1.0 : 0.0) +// ) + + return context.availableSize + } + } +} + +// TODO: pass to component properly +//internal var deviceCornerRadius: CGFloat? = nil + +public final class MediaStreamComponentController: ViewControllerComponentContainer, VoiceChatController { + private let context: AccountContext + public let call: PresentationGroupCall + public private(set) var currentOverlayController: VoiceChatOverlayController? = nil + public var parentNavigationController: NavigationController? + + public var onViewDidAppear: (() -> Void)? + public var onViewDidDisappear: (() -> Void)? + + private var initialOrientation: UIInterfaceOrientation? + + private let inviteLinksPromise = Promise(nil) + + public init(call: PresentationGroupCall) { + self.context = call.accountContext + self.call = call + + super.init(context: call.accountContext, component: MediaStreamComponent(call: call as! PresentationGroupCallImpl), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .White + self.view.disablesInteractiveModalDismiss = true + + self.inviteLinksPromise.set(.single(nil) + |> then(call.inviteLinks)) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + DispatchQueue.main.async { + self.onViewDidAppear?() + } + + if let view = self.node.hostView.findTaggedView(tag: MediaStreamVideoComponent.View.Tag()) as? MediaStreamVideoComponent.View { + view.expandFromPictureInPicture() + } + + self.view.clipsToBounds = true + + self.view.layer.animatePosition(from: CGPoint(x: self.view.frame.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), to: self.view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + }) + + self.view.layer.allowsGroupOpacity = true + self.view.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.view.layer.allowsGroupOpacity = false + }) + self.backgroundDimView.layer.animateAlpha(from: 0, to: 1, duration: 0.3) + if backgroundDimView.superview == nil { + guard let superview = view.superview else { return } + superview.insertSubview(backgroundDimView, belowSubview: view) + } + } + + override public func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + DispatchQueue.main.async { + self.onViewDidDisappear?() + } + +// if let initialOrientation = self.initialOrientation { +// self.initialOrientation = nil +// self.call.accountContext.sharedContext.applicationBindings.forceOrientation(initialOrientation) +// } + } + + override public func viewDidLoad() { + super.viewDidLoad() + // TODO: replace with actual color + backgroundDimView.backgroundColor = .black.withAlphaComponent(0.3) + self.view.clipsToBounds = false + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + backgroundDimView.frame = .init(x: 0, y: -view.bounds.height * 3, width: view.bounds.width, height: view.bounds.height * 4) + } + + public func dismiss(closing: Bool, manual: Bool) { + self.dismiss(completion: nil) + } + + let backgroundDimView = UIView() + + override public func dismiss(completion: (() -> Void)? = nil) { + self.view.layer.allowsGroupOpacity = true + self.view.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in + guard let strongSelf = self else { + completion?() + return + } + strongSelf.view.layer.allowsGroupOpacity = false + strongSelf.dismissImpl(completion: completion) + }) + self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0, duration: 0.3, removeOnCompletion: false) + // if let validLayout = self.validLayout { + // self.view.clipsToBounds = true + // self.view.layer.cornerRadius = validLayout.deviceMetrics.screenCornerRadius + // if #available(iOS 13.0, *) { + // self.view.layer.cornerCurve = .continuous + // } + + self.view.layer.animatePosition(from: self.view.center, to: CGPoint(x: self.view.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), duration: 0.4, /*timingFunction: kCAMediaTimingFunctionSpring, */completion: { _ in + }) + // self.view.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + // } + } + + private func dismissImpl(completion: (() -> Void)? = nil) { + super.dismiss(completion: completion) + } + + func updateOrientation(orientation: UIInterfaceOrientation) { + if self.initialOrientation == nil { + self.initialOrientation = orientation == .portrait ? .landscapeRight : .portrait + } else if self.initialOrientation == orientation { + self.initialOrientation = nil + } + self.call.accountContext.sharedContext.applicationBindings.forceOrientation(orientation) + } + + func presentShare() { + let _ = (self.inviteLinksPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] inviteLinks in + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.call.peerId), + TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: strongSelf.call.peerId) + ) + |> map { peer, exportedInvitation -> GroupCallInviteLinks? in + if let inviteLinks = inviteLinks { + return inviteLinks + } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { + return GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil) + } else if let link = exportedInvitation?.link { + return GroupCallInviteLinks(listenerLink: link, speakerLink: nil) + } + return nil + } + |> deliverOnMainQueue).start(next: { links in + guard let strongSelf = self else { + return + } + + if let links = links { + strongSelf.presentShare(links: links) + } + }) + }) + } + + func presentShare(links inviteLinks: GroupCallInviteLinks) { + let formatSendTitle: (String) -> String = { string in + var string = string + if string.contains("[") && string.contains("]") { + if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") { + string.removeSubrange(startIndex ... endIndex) + } + } else { + string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,.")) + } + return string + } + let _ = formatSendTitle + + let _ = (combineLatest(queue: .mainQueue(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.call.state |> take(1)) + |> deliverOnMainQueue).start(next: { [weak self] peer, callState in + if let strongSelf = self { + var inviteLinks = inviteLinks + + if let peer = peer as? TelegramChannel, case .group = peer.info, !peer.flags.contains(.isGigagroup), !(peer.addressName ?? "").isEmpty, let defaultParticipantMuteState = callState.defaultParticipantMuteState { + let isMuted = defaultParticipantMuteState == .muted + + if !isMuted { + inviteLinks = GroupCallInviteLinks(listenerLink: inviteLinks.listenerLink, speakerLink: nil) + } + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + var segmentedValues: [ShareControllerSegmentedValue]? + segmentedValues = nil + let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: defaultDarkPresentationTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) + shareController.completed = { [weak self] peerIds in + if let strongSelf = self { + let _ = (strongSelf.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).start(next: { [weak self] peerList in + if let strongSelf = self { + let peers = peerList.compactMap { $0 } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var isSavedMessages = false + if peers.count == 1, let peer = peers.first { + isSavedMessages = peer.id == strongSelf.context.account.peerId + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.VoiceChat_ForwardTooltip_Chat(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.VoiceChat_ForwardTooltip_TwoChats(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.VoiceChat_ForwardTooltip_ManyChats(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + } + }) + } + } + shareController.actionCompleted = { + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + } + strongSelf.present(shareController, in: .window(.root)) + } + }) + } +} + +// MARK: - Subcomponents + final class StreamTitleComponent: Component { let text: String let isRecording: Bool @@ -79,27 +1286,16 @@ final class StreamTitleComponent: Component { } func toggle(isLive: Bool) { - // TODO: get actual colors if isLive { if !wasLive { - // TODO: animate wasLive = true -// let frame = self.frame let anim = CAKeyframeAnimation(keyPath: "transform.scale") anim.values = [1.0, 1.4, 1.0] anim.keyTimes = [0, 0.5, 1] self.layer.add(anim, forKey: "transform") -// UIView.transition(with: self, duration: <#T##TimeInterval#>, animations: <#T##(() -> Void)?##(() -> Void)?##() -> Void#>) UIView.animate(withDuration: 0.15, animations: { - self.toggle(isLive: true) -// self.transform = .init(scaleX: 1.5, y: 1.5) - }, completion: { _ in -// UIView.animate(withDuration: 0.15) { -// self.transform = .identity -//// self.frame = frame -// } - }) + self.toggle(isLive: true) }) return } self.backgroundColor = UIColor(red: 0.82, green: 0.26, blue: 0.37, alpha: 1) @@ -107,7 +1303,6 @@ final class StreamTitleComponent: Component { stalledAnimatedGradient.removeAllAnimations() } else { if wasLive { - // TODO: animate wasLive = false UIView.animate(withDuration: 0.3) { self.toggle(isLive: false) @@ -116,7 +1311,6 @@ final class StreamTitleComponent: Component { } self.backgroundColor = UIColor(white: 0.36, alpha: 1) stalledAnimatedGradient.opacity = 1 -// stalledAnimatedGradient.add(<#T##anim: CAAnimation##CAAnimation#>, forKey: <#T##String?#>) } wasLive = isLive } @@ -704,1277 +1898,6 @@ final class RoundGradientButtonComponent: Component { } } -public final class _MediaStreamComponent: CombinedComponent { - struct OriginInfo: Equatable { - var title: String - var memberCount: Int - } - - public typealias EnvironmentType = ViewControllerComponentContainer.Environment - - public let call: PresentationGroupCallImpl - - public init(call: PresentationGroupCallImpl) { - self.call = call - } - - public static func ==(lhs: _MediaStreamComponent, rhs: _MediaStreamComponent) -> Bool { - if lhs.call !== rhs.call { - return false - } - - return true - } - - public final class State: ComponentState { - private let call: PresentationGroupCallImpl - - private(set) var hasVideo: Bool = false - private var stateDisposable: Disposable? - private var infoDisposable: Disposable? - private var connectionDisposable: Disposable? - private var networkStateDisposable: Disposable? - - private(set) var originInfo: OriginInfo? - - private(set) var displayUI: Bool = true - var dismissOffset: CGFloat = 0.0 - var initialOffset: CGFloat = 0.0 - // TODO: remove (replaced by isFullscreen) - var storedIsLandscape: Bool? - var isFullscreen: Bool = false - var videoSize: CGSize? - - private(set) var canManageCall: Bool = false - // TODO: also handle pictureInPicturePossible - let isPictureInPictureSupported: Bool - - private(set) var callTitle: String? - private(set) var recordingStartTimestamp: Int32? - - private(set) var peerTitle: String = "" - private(set) var chatPeer: Peer? - - private(set) var isVisibleInHierarchy: Bool = false - private var isVisibleInHierarchyDisposable: Disposable? - - private var scheduledDismissUITimer: SwiftSignalKit.Timer? - var videoStalled: Bool = true - - var videoIsPlayable: Bool { - !videoStalled && hasVideo - } - - let deactivatePictureInPictureIfVisible = StoredActionSlot(Void.self) - - 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 - - if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() { - self.isPictureInPictureSupported = true - } else { - self.isPictureInPictureSupported = AVPictureInPictureController.isPictureInPictureSupported() - } - - super.init() - - self.stateDisposable = (call.state - |> map { state -> Bool in - switch state.networkState { - case .connected: - return true - default: - return false - } - } - |> filter { $0 } - |> take(1)).start(next: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.hasVideo = true - strongSelf.updated(transition: .immediate) - }) - - // TODO: retest to uncomment or delete. Relying only on video frames - /*self.networkStateDisposable = (call.account.networkState |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { return } - switch state { - case .waitingForNetwork, .connecting: - print("[NEW] videoStalled") - strongSelf.videoStalled = true - default: - strongSelf.videoStalled = !strongSelf.hasVideo - } - strongSelf.updated(transition: .immediate) -// if let strongSelf = self, case .standard(previewing: false) = strongSelf.presentationInterfaceState.mode { -// strongSelf.chatTitleView?.networkState = state -// } - }) - - self.connectionDisposable = call.state.start(next: { [weak self] state in - let prev = self?.videoStalled - switch state.networkState { - case .connected: - self?.videoStalled = false - default: - print("[ALERT] video stalled") - self?.videoStalled = true - } - if prev != self?.videoStalled { - self?.updated(transition: .immediate) - } - })*/ - - let callPeer = call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) - - self.infoDisposable = (combineLatest(queue: .mainQueue(), call.state, call.members, callPeer) - |> deliverOnMainQueue).start(next: { [weak self] state, members, callPeer in - guard let strongSelf = self, let members = members, let callPeer = callPeer else { - return - } - - var updated = false -// TODO: remove debug timer -// Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in - strongSelf.infoThrottler.publish(members.totalCount/*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in - print(members.totalCount) - guard let strongSelf = strongSelf else { return } - var updated = false - let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: latestCount) - 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 - } - if strongSelf.peerTitle != callPeer.debugDisplayTitle { - strongSelf.peerTitle = callPeer.debugDisplayTitle - updated = true - } - strongSelf.chatPeer = callPeer._asPeer() - - if strongSelf.callTitle != state.title { - strongSelf.callTitle = state.title - updated = true - } - - if strongSelf.recordingStartTimestamp != state.recordingStartTimestamp { - strongSelf.recordingStartTimestamp = state.recordingStartTimestamp - 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) - } - }) - - self.isVisibleInHierarchyDisposable = (call.accountContext.sharedContext.applicationBindings.applicationInForeground - |> deliverOnMainQueue).start(next: { [weak self] inForeground in - guard let strongSelf = self else { - return - } - if strongSelf.isVisibleInHierarchy != inForeground { - strongSelf.isVisibleInHierarchy = inForeground - strongSelf.updated(transition: .immediate) - - if inForeground { - Queue.mainQueue().after(0.5, { - guard let strongSelf = self, strongSelf.isVisibleInHierarchy else { - return - } - - strongSelf.deactivatePictureInPictureIfVisible.invoke(Void()) - }) - } - } - }) - } - - deinit { - self.stateDisposable?.dispose() - self.infoDisposable?.dispose() - self.isVisibleInHierarchyDisposable?.dispose() - self.connectionDisposable?.dispose() - self.networkStateDisposable?.dispose() - } - - func toggleDisplayUI() { - self.displayUI = !self.displayUI - self.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .easeInOut))) - } - - func cancelScheduledDismissUI() { - self.scheduledDismissUITimer?.invalidate() - self.scheduledDismissUITimer = nil - } - - func scheduleDismissUI() { - if self.scheduledDismissUITimer == nil { - self.scheduledDismissUITimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.scheduledDismissUITimer = nil - if strongSelf.displayUI { - strongSelf.toggleDisplayUI() - } - }, queue: .mainQueue()) - self.scheduledDismissUITimer?.start() - } - } - - func updateDismissOffset(value: CGFloat, interactive: Bool) { - self.dismissOffset = value - if interactive { - self.updated(transition: .immediate) - } else { - self.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) - } - } - } - - public func makeState() -> State { - return State(call: self.call) - } - - public static var body: Body { - let background = Child(Rectangle.self) - let dismissTapComponent = Child(Rectangle.self) - let video = Child(MediaStreamVideoComponent.self) -// let navigationBar = Child(NavigationBarComponent.self) -// let toolbar = Child(ToolbarComponent.self) - - let sheet = Child(StreamSheetComponent.self) - let fullscreenOverlay = Child(StreamSheetComponent.self) - - let activatePictureInPicture = StoredActionSlot(Action.self) - let deactivatePictureInPicture = StoredActionSlot(Void.self) - let moreButtonTag = GenericComponentViewTag() - let moreAnimationTag = GenericComponentViewTag() - - return { context in - let canEnforceOrientation = UIDevice.current.model != "iPad" - var forceFullScreenInLandscape: Bool { canEnforceOrientation && true } - let environment = context.environment[ViewControllerComponentContainer.Environment.self].value - if environment.isVisible { - } else { - context.state.dismissOffset = 0.0 - } - - let background = background.update( - component: Rectangle(color: .black.withAlphaComponent(0.0)), - availableSize: context.availableSize, - transition: context.transition - ) - - let call = context.component.call - let state = context.state - let controller = environment.controller - //? - if environment.isVisible { - state.videoHiddenForPip = false - } - context.state.deactivatePictureInPictureIfVisible.connect { - guard let controller = controller() else { - return - } - if controller.view.window == nil { - return - } - state.videoHiddenForPip = false - state.updated(transition: .easeInOut(duration: 3)) - deactivatePictureInPicture.invoke(Void()) - } - let isFullscreen: Bool // = state.isFullscreen - let isLandscape = context.availableSize.width > context.availableSize.height - -// if let videoSize = context.state.videoSize { - // Always fullscreen in landscape - // TODO: support landscape sheet (wrap in scrollview, video size same as portrait) - if forceFullScreenInLandscape && /*videoSize.width > videoSize.height &&*/ isLandscape && !state.isFullscreen { - state.isFullscreen = true - isFullscreen = true - } else if /*let videoSize = context.state.videoSize, videoSize.width > videoSize.height &&*/ !isLandscape && state.isFullscreen && canEnforceOrientation { - state.isFullscreen = false - isFullscreen = false - } else { - isFullscreen = state.isFullscreen - } - // } - let videoInset: CGFloat - if !isFullscreen { - videoInset = 16 - } else { - videoInset = 0 - } - - let videoHeight: CGFloat = forceFullScreenInLandscape - ? (context.availableSize.width - videoInset * 2) / 16 * 9 - : context.state.videoSize?.height ?? (min(context.availableSize.width, context.availableSize.height) - videoInset * 2) / 16 * 9 - let bottomPadding = 40 + environment.safeInsets.bottom - let sheetHeight: CGFloat = isFullscreen - ? context.availableSize.height - : (44 + videoHeight + 40 + 69 + 16 + 32 + 70 + bottomPadding) - let isFullyDragged = context.availableSize.height - sheetHeight + state.dismissOffset - context.view.safeAreaInsets.top < 30 - - var dragOffset = context.state.dismissOffset - if isFullyDragged { - dragOffset = max(context.state.dismissOffset, sheetHeight - context.availableSize.height + context.view.safeAreaInsets.top)// sheetHeight - UIScreen.main.bounds.height - } - - let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - sheetHeight + dragOffset) - let dismissTapComponent = dismissTapComponent.update( - component: Rectangle(color: .red.withAlphaComponent(0)), - availableSize: CGSize(width: context.availableSize.width, height: dismissTapAreaHeight), - transition: context.transition - ) - - - let video = video.update( - component: MediaStreamVideoComponent( - call: context.component.call, - hasVideo: context.state.hasVideo, - isVisible: environment.isVisible && context.state.isVisibleInHierarchy, - isAdmin: context.state.canManageCall, - peerTitle: context.state.peerTitle, - // TODO: remove // find out how to get image - peerImage: nil, - isFullscreen: isFullscreen, - videoLoading: context.state.videoStalled, - callPeer: context.state.chatPeer, - activatePictureInPicture: activatePictureInPicture, - deactivatePictureInPicture: deactivatePictureInPicture, - bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in - guard let call = call else { - completed() - return - } - - call.accountContext.sharedContext.mainWindow?.inCallNavigate?() - // TODO: bring up sheet - completed() - }, - pictureInPictureClosed: { [weak call] in - let _ = call?.leave(terminateIfPossible: false) - }, - onVideoSizeRetrieved: { [weak state] size in - state?.videoSize = size - }, - onVideoPlaybackLiveChange: { [weak state] isLive in - guard let state else { return } - let wasLive = !state.videoStalled - if isLive != wasLive { - state.videoStalled = !isLive - state.updated() - } - } - ), - availableSize: context.availableSize, - transition: context.transition - )// .opacity(state.videoHiddenForPip ? 0 : 1) - -// let height = context.availableSize.height - var navigationRightItems: [AnyComponentWithIdentity] = [] -// let contextView = context.view - if context.state.isPictureInPictureSupported, context.state.videoIsPlayable { - navigationRightItems.append(AnyComponentWithIdentity(id: "pip", component: AnyComponent(Button( - content: AnyComponent(ZStack([ - AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( - fillColor: .white.withAlphaComponent(0.08), - size: CGSize(width: 32.0, height: 32.0) - ))), - AnyComponentWithIdentity(id: "a", component: AnyComponent(BundleIconComponent( - name: "Call/pip", - tintColor: .white - ))) - ] - )), - action: { - activatePictureInPicture.invoke(Action { - guard let controller = controller() as? MediaStreamComponentController else { - return - } - state.videoHiddenForPip = true - - controller.dismiss(closing: false, manual: true) - }) - } - ).minSize(CGSize(width: 44.0, height: 44.0))))) - } - var topLeftButton: AnyComponent? - if context.state.canManageCall { - let whiteColor = UIColor(white: 1.0, alpha: 1.0) - /*navigationRightItems.append(*/ topLeftButton = //AnyComponentWithIdentity(id: "more", component: - AnyComponent(Button( - content: AnyComponent(ZStack([ - AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( - fillColor: .white.withAlphaComponent(0.08), - size: CGSize(width: 32.0, height: 32.0) - ))), - AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "anim_profilemore", - mode: .still(position: .begin) - ), - colors: [ - "Point 2.Group 1.Fill 1": whiteColor, - "Point 3.Group 1.Fill 1": whiteColor, - "Point 1.Group 1.Fill 1": whiteColor - ], - size: CGSize(width: 32.0, height: 32.0) - ).tagged(moreAnimationTag))), - ])), - action: { [weak call, weak state] in - guard let call = call, let state = state else { - return - } - guard let controller = controller() as? MediaStreamComponentController else { - return - } - guard let anchorView = controller.node.hostView.findTaggedView(tag: moreButtonTag) else { - return - } - - if let animationView = controller.node.hostView.findTaggedView(tag: moreAnimationTag) as? LottieAnimationComponent.View { - animationView.playOnce() - } - - let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_EditTitle, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak call, weak controller, weak state] _, a in - guard let call = call, let controller = controller, let state = state, let chatPeer = state.chatPeer else { - return - } - - let initialTitle = state.callTitle ?? "" - - let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } - - let title: String = presentationData.strings.LiveStream_EditTitle - let text: String = presentationData.strings.LiveStream_EditTitleText - - let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in - guard let call = call else { - return - } - - let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } - - if let title = title, title != initialTitle { - call.updateTitle(title) - - let text: String = title.isEmpty ? presentationData.strings.LiveStream_EditTitleRemoveSuccess : presentationData.strings.LiveStream_EditTitleSuccess(title).string - - let _ = text - //strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false }) - } - }) - controller.present(editController, in: .window(.root)) - - a(.default) - }))) - - if let recordingStartTimestamp = state.recordingStartTimestamp { - items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak call, weak controller] _, f in - f(.dismissWithoutContent) - - guard let call = call, let controller = controller else { - return - } - - let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } - - let alertController = textAlertController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: nil, text: presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_StopRecordingStop, action: { [weak call, weak controller] in - guard let call = call, let controller = controller else { - return - } - call.setShouldBeRecording(false, title: nil, videoOrientation: nil) - - let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } - let text = presentationData.strings.LiveStream_RecordingSaved - - let _ = text - let _ = controller - - /*strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in - if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - let context = strongSelf.context - strongSelf.controller?.dismiss(completion: { - Queue.mainQueue().justDispatch { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - }) - - return true - } - return false - })*/ - })]) - controller.present(alertController, in: .window(.root)) - }), false)) - } else { - let text = presentationData.strings.LiveStream_StartRecording - items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in - return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) - }, action: { [weak call, weak state, weak controller] _, f in - f(.dismissWithoutContent) - - guard let call = call, let state = state, let _ = state.chatPeer, let controller = controller else { - return - } - - let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } - - let title: String - let text: String - let placeholder: String = presentationData.strings.VoiceChat_RecordingTitlePlaceholderVideo - - title = presentationData.strings.LiveStream_StartRecordingTitle - text = presentationData.strings.LiveStream_StartRecordingTextVideo - - let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in - guard let call = call, let controller = controller else { - return - } - - - let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } - - if let title = title { - call.setShouldBeRecording(true, title: title, videoOrientation: false) - - let text = presentationData.strings.LiveStream_RecordingStarted - let _ = text - - let _ = controller - - call.playTone(.recordingStarted) - } - }) - controller.present(editController, in: .window(.root)) - }))) - } - - let credentialsPromise = Promise() - credentialsPromise.set(call.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: call.peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal in return .never() }) - - items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_ViewCredentials, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor, backgroundColor: nil) - }, action: { [weak call, weak controller] _, a in - guard let call = call, let controller = controller else { - return - } - - controller.push(CreateExternalMediaStreamScreen(context: call.accountContext, peerId: call.peerId, credentialsPromise: credentialsPromise, mode: .view)) - - a(.default) - }))) - - items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.VoiceChat_StopRecordingStop, textColor: .destructive, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor, backgroundColor: nil) - }, action: { [weak call] _, a in - guard let call = call else { - return - } - - let _ = call.leave(terminateIfPossible: true).start() - - a(.default) - }))) - - final class ReferenceContentSource: ContextReferenceContentSource { - private let sourceView: UIView - - init(sourceView: UIView) { - self.sourceView = sourceView - } - - func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) - } - } - - let contextController = ContextController(account: call.accountContext.account, presentationData: presentationData.withUpdated(theme: defaultDarkPresentationTheme), source: .reference(ReferenceContentSource(sourceView: anchorView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) - /*contextController.passthroughTouchEvent = { sourceView, point in - guard let strongSelf = self else { - return .ignore - } - - let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil) - guard let localResult = strongSelf.hitTest(localPoint, with: nil) else { - return .dismiss(consume: true, result: nil) - } - - var testView: UIView? = localResult - while true { - if let testViewValue = testView { - if let node = testViewValue.asyncdisplaykit_node as? PeerInfoHeaderNavigationButton { - node.isUserInteractionEnabled = false - DispatchQueue.main.async { - node.isUserInteractionEnabled = true - } - return .dismiss(consume: false, result: nil) - } else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoVisualMediaPaneNode { - node.brieflyDisableTouchActions() - return .dismiss(consume: false, result: nil) - } else { - testView = testViewValue.superview - } - } else { - break - } - } - - return .dismiss(consume: true, result: nil) - }*/ - controller.presentInGlobalOverlay(contextController) - } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(moreButtonTag))//)//) - } - let navigationComponent = NavigationBarComponent( - topInset: environment.statusBarHeight, - sideInset: environment.safeInsets.left, - leftItem: topLeftButton/*AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Close, font: Font.regular(17.0), color: .white)), - action: { [weak call] in - let _ = call?.leave(terminateIfPossible: false) - }) - )*/, - rightItems: navigationRightItems, - centerItem: AnyComponent(StreamTitleComponent(text: state.peerTitle, isRecording: state.recordingStartTimestamp != nil, isActive: context.state.videoIsPlayable)) - ) - -// let navigationBar = navigationBar.update( -// component: navigationComponent, -// availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), -// transition: context.transition -// ) - - if context.state.storedIsLandscape != isLandscape { - context.state.storedIsLandscape = isLandscape - if isLandscape { - context.state.scheduleDismissUI() - } else { - context.state.cancelScheduledDismissUI() - } - } - - var infoItem: AnyComponent? - if let originInfo = context.state.originInfo { - let memberCountString: String - if originInfo.memberCount == 0 { - memberCountString = environment.strings.LiveStream_NoViewers - } else { - memberCountString = environment.strings.LiveStream_ViewerCount(Int32(originInfo.memberCount)) - } - infoItem = AnyComponent(OriginInfoComponent( - title: state.callTitle ?? originInfo.title, - subtitle: memberCountString - )) - } - let availableSize = context.availableSize - let safeAreaTop = context.view.safeAreaInsets.top - - let onPanGesture: ((Gesture.PanGestureState) -> Void) = { [weak state] panState in - guard let state = state else { - return - } - switch panState { - case .began: - state.initialOffset = state.dismissOffset - case let .updated(offset): - state.updateDismissOffset(value: state.initialOffset + offset.y, interactive: true) - case let .ended(velocity): - // TODO: Dismiss sheet depending on velocity - if velocity.y > 200.0 { - if state.isFullscreen { - state.isFullscreen = false - state.updateDismissOffset(value: 0.0, interactive: false) - if let controller = controller() as? MediaStreamComponentController { - controller.updateOrientation(orientation: .portrait) - } - } else { - if isFullyDragged || state.initialOffset != 0 { - state.updateDismissOffset(value: 0.0, interactive: false) - } else { - let _ = call.leave(terminateIfPossible: false) - } - } - /*activatePictureInPicture.invoke(Action { [weak state] in - guard let state = state, let controller = controller() as? MediaStreamComponentController else { - return - } - state.updateDismissOffset(value: velocity.y < 0 ? -height : height, interactive: false) - controller.dismiss(closing: false, manual: true) - })*/ - } else { - if isFullyDragged { - state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false) - } else { - if velocity.y < -200 { - // Expand - state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false) - } else { - state.updateDismissOffset(value: 0.0, interactive: false) - } - } - } - } - } - - context.add(background - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) - .gesture(.tap { [weak state] in - guard let state = state, state.isFullscreen else { - return - } - state.toggleDisplayUI() - }) - .gesture(.pan { panState in - onPanGesture(panState) - }) - ) -// var bottomComponent: AnyComponent? -// var fullScreenToolbarComponent: AnyComponent? - - context.add(dismissTapComponent - .position(CGPoint(x: context.availableSize.width / 2, y: dismissTapAreaHeight / 2)) - .gesture(.tap { - _ = call.leave(terminateIfPossible: false) - }) - .gesture(.pan(onPanGesture)) - ) - - if !isFullscreen { - let imageRenderScale = UIScreen.main.scale - let bottomComponent = AnyComponent(ButtonsRowComponent( - bottomInset: environment.safeInsets.bottom, - sideInset: environment.safeInsets.left, - leftItem: AnyComponent(Button( - content: AnyComponent(RoundGradientButtonComponent(// BundleIconComponent( - gradientColors: [UIColor(red: 0.18, green: 0.17, blue: 0.30, alpha: 1).cgColor, UIColor(red: 0.17, green: 0.16, blue: 0.30, alpha: 1).cgColor], - image: generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: .white),// "Chat/Input/Accessory Panels/MessageSelectionForward" - // TODO: localize: - title: "share")), - action: { - guard let controller = controller() as? MediaStreamComponentController else { - return - } - controller.presentShare() - } - ).minSize(CGSize(width: 65, height: 80))), - // TODO: disable button instead of hiding - rightItem: AnyComponent(Button( - content: AnyComponent(RoundGradientButtonComponent( - gradientColors: [UIColor(red: 0.44, green: 0.18, blue: 0.22, alpha: 1).cgColor, UIColor(red: 0.44, green: 0.18, blue: 0.22, alpha: 1).cgColor], - image: generateImage(CGSize(width: 44.0 * imageRenderScale, height: 44 * imageRenderScale), opaque: false, rotatedContext: { size, context in - context.translateBy(x: size.width / 2, y: size.height / 2) - context.scaleBy(x: 0.4, y: 0.4) - context.translateBy(x: -size.width / 2, y: -size.height / 2) - let imageColor = UIColor.white - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - let lineWidth: CGFloat = size.width / 7 - context.setLineWidth(lineWidth - UIScreenPixel) - context.setLineCap(.round) - context.setStrokeColor(imageColor.cgColor) - - context.move(to: CGPoint(x: lineWidth / 2 + UIScreenPixel, y: lineWidth / 2 + UIScreenPixel)) - context.addLine(to: CGPoint(x: size.width - lineWidth / 2 - UIScreenPixel, y: size.height - lineWidth / 2 - UIScreenPixel)) - context.strokePath() - - context.move(to: CGPoint(x: size.width - lineWidth / 2 - UIScreenPixel, y: lineWidth / 2 + UIScreenPixel)) - context.addLine(to: CGPoint(x: lineWidth / 2 + UIScreenPixel, y: size.height - lineWidth / 2 - UIScreenPixel)) - context.strokePath() - }), - title: "leave" - )), - action: { [weak call] in - let _ = call?.leave(terminateIfPossible: false) - } - ).minSize(CGSize(width: 44.0, height: 44.0))), - centerItem: AnyComponent(Button( - content: AnyComponent(RoundGradientButtonComponent( - gradientColors: [UIColor(red: 0.23, green: 0.17, blue: 0.29, alpha: 1).cgColor, UIColor(red: 0.21, green: 0.16, blue: 0.29, alpha: 1).cgColor], - image: generateImage(CGSize(width: 44 * imageRenderScale, height: 44 * imageRenderScale), opaque: false, rotatedContext: { size, context in - - let imageColor = UIColor.white - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - context.setLineWidth(2.4 * imageRenderScale - UIScreenPixel) - context.setLineCap(.round) - context.setStrokeColor(imageColor.cgColor) -// context.setLineJoin(.round) - - let lineSide = size.width / 5 - let centerOffset = size.width / 20 - context.move(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - centerOffset / 2)) - context.addLine(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - lineSide)) - context.addLine(to: CGPoint(x: size.width / 2 + centerOffset / 2, y: size.height / 2 - lineSide)) - context.move(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - lineSide)) - context.addLine(to: CGPoint(x: size.width / 2 + centerOffset, y: size.height / 2 - centerOffset)) - context.strokePath() - - context.move(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + centerOffset / 2)) - context.addLine(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + lineSide)) - context.addLine(to: CGPoint(x: size.width / 2 - centerOffset / 2, y: size.height / 2 + lineSide)) - context.move(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + lineSide)) - context.addLine(to: CGPoint(x: size.width / 2 - centerOffset, y: size.height / 2 + centerOffset)) - context.strokePath() - }), - title: "expand" - )), - action: { - guard state.videoIsPlayable else { - state.isFullscreen = false - return - } - if let controller = controller() as? MediaStreamComponentController { - guard let size = state.videoSize else { return } - state.isFullscreen.toggle() - if state.isFullscreen { - if size.width > size.height { - controller.updateOrientation(orientation: .landscapeRight) - } else { - controller.updateOrientation(orientation: .portrait) - // TODO: Update to portrait when open from landscape(?) - } - } else { - // TODO: Check and respect current device orientation - controller.updateOrientation(orientation: .portrait) - } - if !canEnforceOrientation { - state.updated() // updated(.easeInOut(duration: 0.3)) - } - // controller.updateOrientation(orientation: isLandscape ? .portrait : .landscapeRight) - } - } - ).minSize(CGSize(width: 44.0, height: 44.0))) - )) - - let sheet = sheet.update( - component: StreamSheetComponent( - topComponent: AnyComponent(navigationComponent), - bottomButtonsRow: bottomComponent, - topOffset: context.availableSize.height - sheetHeight + dragOffset, - sheetHeight: max(sheetHeight - dragOffset, sheetHeight), - backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), - bottomPadding: bottomPadding, - participantsCount: context.state.originInfo?.memberCount ?? 0, // Int.random(in: 0...999998)// [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()! - // - isFullyExtended: isFullyDragged, - deviceCornerRadius: deviceCornerRadius ?? 0, - videoHeight: videoHeight - ), - availableSize: context.availableSize, - transition: context.transition - ) - - // TODO: calculate (although not necessary currently) - let sheetOffset: CGFloat = context.availableSize.height - sheetHeight + dragOffset - let sheetPosition = sheetOffset + sheetHeight / 2 - // Sheet underneath the video when in sheet -// if !isFullscreen { - // TODO: work with sheet here - context.add(sheet - .position(.init(x: context.availableSize.width / 2.0, y: /*isFullscreen ?*/ context.availableSize.height / 2)) //: sheetPosition)) - ) -// } - let videoPos: CGFloat - - if isFullscreen { - videoPos = context.availableSize.height / 2 + dragOffset - } else { - videoPos = sheetPosition - sheetHeight / 2 + videoHeight / 2 + 50 + 12 - } - context.add(video - .position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos)/*sheetPosition + videoHeight / 2 + 50 - context.availableSize.height / 2*/)// context.availableSize.height / 2.0 + context.state.dismissOffset)) - ) - } else { - context.add(video - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2 + dragOffset) - )) - } - - if isFullscreen { - let fullScreenToolbarComponent = AnyComponent(ToolbarComponent( - bottomInset: environment.safeInsets.bottom, - sideInset: environment.safeInsets.left, - leftItem: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Accessory Panels/MessageSelectionForward", - tintColor: .white - )), - action: { - guard let controller = controller() as? MediaStreamComponentController else { - return - } - controller.presentShare() - } - ).minSize(CGSize(width: 64.0, height: 80))), - // TODO: disable button instead of hiding - rightItem: state.hasVideo ? AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: isFullscreen ? "Media Gallery/Minimize" : "Media Gallery/Fullscreen", - tintColor: .white - )), - action: { - /*if let controller = controller() as? MediaStreamComponentController { - guard let size = state.videoSize else { return } - state.isFullscreen = false - if state.isFullscreen { - if size.width > size.height { - controller.updateOrientation(orientation: .landscapeRight) - } else { - controller.updateOrientation(orientation: .portrait) - // TODO: Update to portrait when open from landscape(?) - } - } else { - // TODO: Check and respect current device orientation - controller.updateOrientation(orientation: .portrait) - } - // controller.updateOrientation(orientation: isLandscape ? .portrait : .landscapeRight) - }*/ - state.isFullscreen = false - if let controller = controller() as? MediaStreamComponentController { - if canEnforceOrientation { - controller.updateOrientation(orientation: .portrait) - } else { - state.updated() // updated(.easeInOut(duration: 0.3)) - } - } - } - ).minSize(CGSize(width: 64.0, height: 80))) : nil, - centerItem: infoItem - )) - let fullScreenOverlayComponent = fullscreenOverlay.update( - component: StreamSheetComponent( - topComponent: AnyComponent(navigationComponent), - bottomButtonsRow: fullScreenToolbarComponent, - topOffset: context.availableSize.height - sheetHeight + context.state.dismissOffset, - sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight), - backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), - bottomPadding: 12, - participantsCount: -1, // context.state.originInfo?.memberCount ?? 0 - isFullyExtended: isFullyDragged, - deviceCornerRadius: deviceCornerRadius ?? 0, - videoHeight: videoHeight - ), - availableSize: context.availableSize, - transition: context.transition - ) - context.add(fullScreenOverlayComponent - .position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2)) - .opacity(state.displayUI ? 1 : 0) - ) - } - -// context.add(navigationBar -// .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height / 2.0)) -// .opacity(context.state.displayUI ? 1.0 : 0.0) -// ) - -// context.add(toolbar -// .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) -// .opacity(context.state.displayUI ? 1.0 : 0.0) -// ) - - return context.availableSize - } - } -} - -// TODO: pass to component properly -var deviceCornerRadius: CGFloat? = nil - -public final class _MediaStreamComponentController: ViewControllerComponentContainer, VoiceChatController { - private let context: AccountContext - public let call: PresentationGroupCall - public private(set) var currentOverlayController: VoiceChatOverlayController? = nil - public var parentNavigationController: NavigationController? - - public var onViewDidAppear: (() -> Void)? - public var onViewDidDisappear: (() -> Void)? - - private var initialOrientation: UIInterfaceOrientation? - - private let inviteLinksPromise = Promise(nil) - - public convenience init(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) { - self.init(call: call) - } - - public init(call: PresentationGroupCall) { - self.context = call.accountContext - self.call = call - - super.init(context: call.accountContext, component: MediaStreamComponent(call: call as! PresentationGroupCallImpl), navigationBarAppearance: .none) - - self.statusBar.statusBarStyle = .White - self.view.disablesInteractiveModalDismiss = true - - self.inviteLinksPromise.set(.single(nil) - |> then(call.inviteLinks)) - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - DispatchQueue.main.async { - self.onViewDidAppear?() - } - - if let view = self.node.hostView.findTaggedView(tag: MediaStreamVideoComponent.View.Tag()) as? MediaStreamVideoComponent.View { - view.expandFromPictureInPicture() - } - - if let validLayout = self.validLayout { - self.view.clipsToBounds = true - - // TODO: pass to component properly - deviceCornerRadius = validLayout.deviceMetrics.screenCornerRadius - 1// 0.5 -// self.view.layer.cornerRadius = validLayout.deviceMetrics.screenCornerRadius -// if #available(iOS 13.0, *) { -// self.view.layer.cornerCurve = .continuous -// } - - self.view.layer.animatePosition(from: CGPoint(x: self.view.frame.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), to: self.view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in // [weak self] _ in -// self?.view.layer.cornerRadius = 0.0 - }) -// self.view.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - } - - self.view.layer.allowsGroupOpacity = true - self.view.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.view.layer.allowsGroupOpacity = false - }) - self.backgroundDimView.layer.animateAlpha(from: 0, to: 1, duration: 0.3) - if backgroundDimView.superview == nil { - guard let superview = view.superview else { return } - superview.insertSubview(backgroundDimView, belowSubview: view) - } - // self.view.backgroundColor = .cyan - } - - override public func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - DispatchQueue.main.async { - self.onViewDidDisappear?() - } - -// if let initialOrientation = self.initialOrientation { -// self.initialOrientation = nil -// self.call.accountContext.sharedContext.applicationBindings.forceOrientation(initialOrientation) -// } - } - - override public func viewDidLoad() { - super.viewDidLoad() -// view.insertSubview(backgroundDimView, at: 0) - // TODO: replace with actual color - backgroundDimView.backgroundColor = .black.withAlphaComponent(0.3) - self.view.clipsToBounds = false - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - backgroundDimView.frame = .init(x: 0, y: -view.bounds.height * 3, width: view.bounds.width, height: view.bounds.height * 4) - } - - public func dismiss(closing: Bool, manual: Bool) { - self.dismiss(completion: nil) - } - - let backgroundDimView = UIView() - - override public func dismiss(completion: (() -> Void)? = nil) { - self.view.layer.allowsGroupOpacity = true - self.view.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in - guard let strongSelf = self else { - completion?() - return - } - strongSelf.view.layer.allowsGroupOpacity = false - strongSelf.dismissImpl(completion: completion) - }) - self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0, duration: 0.3, removeOnCompletion: false) - // if let validLayout = self.validLayout { - // self.view.clipsToBounds = true - // self.view.layer.cornerRadius = validLayout.deviceMetrics.screenCornerRadius - // if #available(iOS 13.0, *) { - // self.view.layer.cornerCurve = .continuous - // } - - self.view.layer.animatePosition(from: self.view.center, to: CGPoint(x: self.view.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), duration: 0.4, /*timingFunction: kCAMediaTimingFunctionSpring, */completion: { _ in - }) - // self.view.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - // } - } - - private func dismissImpl(completion: (() -> Void)? = nil) { - super.dismiss(completion: completion) - } - - func updateOrientation(orientation: UIInterfaceOrientation) { - if self.initialOrientation == nil { - self.initialOrientation = orientation == .portrait ? .landscapeRight : .portrait - } else if self.initialOrientation == orientation { - self.initialOrientation = nil - } - self.call.accountContext.sharedContext.applicationBindings.forceOrientation(orientation) - } - - func presentShare() { - let _ = (self.inviteLinksPromise.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] inviteLinks in - guard let strongSelf = self else { - return - } - - let _ = (strongSelf.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.call.peerId), - TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: strongSelf.call.peerId) - ) - |> map { peer, exportedInvitation -> GroupCallInviteLinks? in - if let inviteLinks = inviteLinks { - return inviteLinks - } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { - return GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil) - } else if let link = exportedInvitation?.link { - return GroupCallInviteLinks(listenerLink: link, speakerLink: nil) - } - return nil - } - |> deliverOnMainQueue).start(next: { links in - guard let strongSelf = self else { - return - } - - if let links = links { - strongSelf.presentShare(links: links) - } - }) - }) - } - - func presentShare(links inviteLinks: GroupCallInviteLinks) { - let formatSendTitle: (String) -> String = { string in - var string = string - if string.contains("[") && string.contains("]") { - if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") { - string.removeSubrange(startIndex ... endIndex) - } - } else { - string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,.")) - } - return string - } - let _ = formatSendTitle - - let _ = (combineLatest(queue: .mainQueue(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.call.state |> take(1)) - |> deliverOnMainQueue).start(next: { [weak self] peer, callState in - if let strongSelf = self { - var inviteLinks = inviteLinks - - if let peer = peer as? TelegramChannel, case .group = peer.info, !peer.flags.contains(.isGigagroup), !(peer.addressName ?? "").isEmpty, let defaultParticipantMuteState = callState.defaultParticipantMuteState { - let isMuted = defaultParticipantMuteState == .muted - - if !isMuted { - inviteLinks = GroupCallInviteLinks(listenerLink: inviteLinks.listenerLink, speakerLink: nil) - } - } - - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - var segmentedValues: [ShareControllerSegmentedValue]? - segmentedValues = nil - let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: defaultDarkPresentationTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) - shareController.completed = { [weak self] peerIds in - if let strongSelf = self { - let _ = (strongSelf.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).start(next: { [weak self] peerList in - if let strongSelf = self { - let peers = peerList.compactMap { $0 } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - var isSavedMessages = false - if peers.count == 1, let peer = peers.first { - isSavedMessages = peer.id == strongSelf.context.account.peerId - let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.VoiceChat_ForwardTooltip_Chat(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.VoiceChat_ForwardTooltip_TwoChats(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.VoiceChat_ForwardTooltip_ManyChats(peerName, "\(peers.count - 1)").string - } else { - text = "" - } - - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) - } - }) - } - } - shareController.actionCompleted = { - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - } - strongSelf.present(shareController, in: .window(.root)) - } - }) - } -} - -public typealias MediaStreamComponent = _MediaStreamComponent -public typealias MediaStreamComponentController = _MediaStreamComponentController - public final class Throttler { public var duration: TimeInterval = 0.25 public var queue: DispatchQueue = .main diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 458c7ed2b1..1b51a10400 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import ComponentFlow -import ActivityIndicatorComponent import AccountContext import AVKit import MultilineTextComponent @@ -13,59 +12,24 @@ import SwiftSignalKit import AvatarNode import Postbox -typealias MediaStreamVideoComponent = _MediaStreamVideoComponent - class CustomIntensityVisualEffectView: UIVisualEffectView { init(effect: UIVisualEffect, intensity: CGFloat) { super.init(effect: nil) animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in self.effect = effect } - self.animator?.startAnimation() - self.animator?.pauseAnimation() + animator.startAnimation() + animator.pauseAnimation() animator.fractionComplete = intensity animator.pausesOnCompletion = true -// subviews.forEach { -// if $0.backgroundColor != nil { -// $0.backgroundColor = $0.backgroundColor?.withAlphaComponent(0.5) -// } -// } } - override func didMoveToSuperview() { - super.didMoveToSuperview() -// let effect = self.effect -// self.effect = nil -// animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in self.effect = effect } -// animator.fractionComplete = 0.1// intensity -// animator.pausesOnCompletion = true - } - required init?(coder aDecoder: NSCoder) { fatalError() } var animator: UIViewPropertyAnimator! - -// private var displayLink: CADisplayLink? -// -// func setIntensity(_ intensity: CGFloat, animated: Bool) { -// self.displayLink?.invalidate() -// let displaylink = CADisplayLink( -// target: self, -// selector: #selector(displayLinkStep) -// ) -// self.displayLink = displaylink -// displaylink.add( -// to: .current, -// forMode: RunLoop.Mode.default -// ) -// } -// -// @objc func displayLinkStep(_:) { -// -// } } -final class _MediaStreamVideoComponent: Component { +final class MediaStreamVideoComponent: Component { let call: PresentationGroupCallImpl let hasVideo: Bool let isVisible: Bool @@ -75,7 +39,6 @@ final class _MediaStreamVideoComponent: Component { let deactivatePictureInPicture: ActionSlot let bringBackControllerForPictureInPictureDeactivation: (@escaping () -> Void) -> Void let pictureInPictureClosed: () -> Void - let peerImage: Any? let isFullscreen: Bool let onVideoSizeRetrieved: (CGSize) -> Void let videoLoading: Bool @@ -88,7 +51,6 @@ final class _MediaStreamVideoComponent: Component { isVisible: Bool, isAdmin: Bool, peerTitle: String, - peerImage: Any?, isFullscreen: Bool, videoLoading: Bool, callPeer: Peer?, @@ -112,12 +74,11 @@ final class _MediaStreamVideoComponent: Component { self.onVideoPlaybackLiveChange = onVideoPlaybackLiveChange self.callPeer = callPeer - self.peerImage = peerImage self.isFullscreen = isFullscreen self.onVideoSizeRetrieved = onVideoSizeRetrieved } - public static func ==(lhs: _MediaStreamVideoComponent, rhs: _MediaStreamVideoComponent) -> Bool { + public static func ==(lhs: MediaStreamVideoComponent, rhs: MediaStreamVideoComponent) -> Bool { if lhs.call !== rhs.call { return false } @@ -160,7 +121,6 @@ final class _MediaStreamVideoComponent: Component { private let blurTintView: UIView private var videoBlurView: VideoRenderingView? private var videoView: VideoRenderingView? - private var activityIndicatorView: ComponentHostView? private var loadingView: ComponentHostView? private var videoPlaceholderView: UIView? @@ -169,7 +129,7 @@ final class _MediaStreamVideoComponent: Component { private let shimmerOverlayView = CALayer() private var pictureInPictureController: AVPictureInPictureController? - private var component: _MediaStreamVideoComponent? + private var component: MediaStreamVideoComponent? private var hadVideo: Bool = false private var requestedExpansion: Bool = false @@ -209,9 +169,7 @@ final class _MediaStreamVideoComponent: Component { } let maskGradientLayer = CAGradientLayer() private var wasVisible = true - var shimmer = StandaloneShimmerEffect() var borderShimmer = StandaloneShimmerEffect() - let shimmerOverlayLayer = CALayer() let shimmerBorderLayer = CALayer() let placeholderView = UIImageView() @@ -230,24 +188,12 @@ final class _MediaStreamVideoComponent: Component { private func updateVideoStalled(isStalled: Bool) { if isStalled { guard let component = self.component else { return } -// let effect = UIBlurEffect(style: .light) -// let intensity: CGFloat = 0.4 -// self.loadingBlurView.effect = nil -// self.loadingBlurView.animator.stopAnimation(true) -// self.loadingBlurView.animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned loadingBlurView] in loadingBlurView.effect = effect } -// self.loadingBlurView.animator.fractionComplete = intensity -// self.loadingBlurView.animator.fractionComplete = 0.4 -// self.loadingBlurView.effect = UIBlurEffect(style: .light) + if let frameView = lastFrame[component.call.peerId.id.description] { frameView.removeFromSuperview() placeholderView.subviews.forEach { $0.removeFromSuperview() } placeholderView.addSubview(frameView) frameView.frame = placeholderView.bounds - // placeholderView.backgroundColor = .green - } else { -// placeholderView.addSubview(avatarPlaceholderView) - // placeholderView.subviews.forEach { $0.removeFromSuperview() } - // placeholderView.backgroundColor = .red } if !hadVideo && placeholderView.superview == nil { @@ -273,19 +219,11 @@ final class _MediaStreamVideoComponent: Component { loadingBlurView.contentView.layer.addSublayer(shimmerBorderLayer) } loadingBlurView.clipsToBounds = true -// if shimmerOverlayLayer.mask == nil { -// shimmer = .init() -// shimmer.layer = shimmerOverlayLayer -// shimmerOverlayView.compositingFilter = "softLightBlendMode" -// shimmer.testUpdate(background: .clear, foreground: .white.withAlphaComponent(0.4)) -// } -// loadingBlurView.layer.cornerRadius = 10 let cornerRadius = loadingBlurView.layer.cornerRadius -// shimmerOverlayLayer.opacity = 0.6 - shimmerBorderLayer.cornerRadius = cornerRadius // TODO: check isFullScreeen + shimmerBorderLayer.cornerRadius = cornerRadius shimmerBorderLayer.masksToBounds = true - shimmerBorderLayer.compositingFilter = "overlayBlendMode"// "softLightBlendMode" + shimmerBorderLayer.compositingFilter = "softLightBlendMode" shimmerBorderLayer.frame = loadingBlurView.bounds let borderMask = CAShapeLayer() borderMask.path = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) @@ -293,27 +231,13 @@ final class _MediaStreamVideoComponent: Component { borderMask.strokeColor = UIColor.white.withAlphaComponent(0.7).cgColor borderMask.lineWidth = 3 shimmerBorderLayer.mask = borderMask -// borderMask.frame = shimmerBorderLayer.bounds -// let testBorder = CAShapeLayer() -// testBorder.path = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: 10, cornerHeight: 10, transform: nil) -// testBorder.fillColor = UIColor.white.withAlphaComponent(0.2).cgColor -// testBorder.strokeColor = UIColor.white.cgColor -// testBorder.lineWidth = 4 -// testBorder.frame = shimmerBorderLayer.bounds -// let borderMask = CALayer() -// shimmerBorderLayer.removeAllAnimations() - // if shimmerBorderLayer.mask == nil { borderShimmer = .init() borderShimmer.layer = shimmerBorderLayer -// shimmerBorderLayer.backgroundColor = UIColor.clear.cgColor - // shimmerBorderLayer.backgroundColor = UIColor.green.withAlphaComponent(0.4).cgColor borderShimmer.testUpdate(background: .clear, foreground: .white) - // } loadingBlurView.alpha = 1 } else { if hadVideo { - loadingBlurView.layer.removeAllAnimations() let anim = CABasicAnimation(keyPath: "opacity") anim.duration = 0.5 anim.fromValue = 1 @@ -324,6 +248,7 @@ final class _MediaStreamVideoComponent: Component { guard self?.videoStalled == false else { return } self?.loadingBlurView.removeFromSuperview() self?.placeholderView.removeFromSuperview() + self?.loadingBlurView.layer.removeAllAnimations() } loadingBlurView.layer.add(anim, forKey: "opacity") } else { @@ -376,7 +301,7 @@ final class _MediaStreamVideoComponent: Component { frameInputDisposable?.dispose() } - func update(component: _MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { + func update(component: MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { self.state = state // placeholderView.alpha = 0.7 // placeholderView.image = lastFrame[component.call.peerId.id.description] @@ -422,25 +347,12 @@ final class _MediaStreamVideoComponent: Component { // TODO: use mapToThrottled (?) frameInputDisposable = input.start(next: { [weak self] input in guard let strongSelf = self else { return } -// print("input") - // strongSelf.stallTimer?.invalidate() - // TODO: optimize with throttle + strongSelf.timeLastFrameReceived = CFAbsoluteTimeGetCurrent() -// DispatchQueue.main.async { - // strongSelf.stallTimer = _stallTimer - // DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - // print(strongSelf.videoStalled) - // if strongSelf.videoStalled { - // strongSelf.stallTimer?.fire() - // } - // RunLoop.main.add(strongSelf.stallTimer!, forMode: .common) strongSelf.videoLoadingThrottler.publish(false, includingLatest: true) { isStalled in strongSelf.videoStalled = isStalled strongSelf.onVideoPlaybackChange(!isStalled) } -// strongSelf.videoStalled = false -// strongSelf.onVideoPlaybackChange(true) -// } }) stallTimer = _stallTimer // RunLoop.main.add(stallTimer!, forMode: .common) @@ -542,9 +454,6 @@ final class _MediaStreamVideoComponent: Component { strongSelf.hadVideo = true - strongSelf.activityIndicatorView?.removeFromSuperview() - strongSelf.activityIndicatorView = nil - strongSelf.noSignalTimer?.invalidate() strongSelf.noSignalTimer = nil strongSelf.noSignalTimeout = false @@ -582,11 +491,10 @@ final class _MediaStreamVideoComponent: Component { let videoSize: CGSize let videoCornerRadius: CGFloat = component.isFullscreen ? 0 : 10 if let videoView = self.videoView { - // TODO: REMOVE FROM HERE and move to call end (or at least to background) -// if let presentation = videoView.snapshotView(afterScreenUpdates: false) { if videoView.bounds.size.width > 0, videoView.alpha > 0, self.hadVideo, + // TODO: remove from here and move to call end (or at least to background) let snapshot = videoView.snapshotView(afterScreenUpdates: false) ?? videoView.snapshotView(afterScreenUpdates: true) { lastFrame[component.call.peerId.id.description] = snapshot// ()! } @@ -632,14 +540,6 @@ final class _MediaStreamVideoComponent: Component { videoView.updateIsEnabled(isVideoVisible) videoView.clipsToBounds = true videoView.layer.cornerRadius = videoCornerRadius - // var aspect = videoView.getAspect() -// if aspect <= 0.01 { - // TODO: remove debug -// if component.videoLoading { -// videoView.alpha = 0.5 -// } else { -// videoView.alpha = 1 -// } transition.withAnimation(.none).setFrame(view: videoView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize), completion: nil) @@ -685,31 +585,6 @@ final class _MediaStreamVideoComponent: Component { // loadingBlurView.removeFromSuperview() } if !self.hadVideo { - // TODO: hide fullscreen button without video - let aspect: CGFloat = 16.0 / 9 - let videoSize = CGSize(width: aspect * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height)) - // loadingpreview.frame = .init(, videoSize) - print(videoSize) - // TODO: remove activity indicator - var activityIndicatorTransition = transition - let activityIndicatorView: ComponentHostView - if let current = self.activityIndicatorView { - activityIndicatorView = current - } else { - activityIndicatorTransition = transition.withAnimation(.none) - activityIndicatorView = ComponentHostView() - self.activityIndicatorView = activityIndicatorView -// self.addSubview(activityIndicatorView) - } - - let activityIndicatorSize = activityIndicatorView.update( - transition: transition, - component: AnyComponent(ActivityIndicatorComponent(color: .white)), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - let activityIndicatorFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - activityIndicatorSize.width) / 2.0), y: floor((availableSize.height - activityIndicatorSize.height) / 2.0)), size: activityIndicatorSize) - activityIndicatorTransition.setFrame(view: activityIndicatorView, frame: activityIndicatorFrame, completion: nil) if self.noSignalTimer == nil { if #available(iOS 10.0, *) { @@ -750,7 +625,7 @@ final class _MediaStreamVideoComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 1000.0) ) - noSignalTransition.setFrame(view: noSignalView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - noSignalSize.width) / 2.0), y: (availableSize.height - noSignalSize.height) / 2.0/*activityIndicatorFrame.maxY + 24.0*/), size: noSignalSize), completion: nil) + noSignalTransition.setFrame(view: noSignalView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - noSignalSize.width) / 2.0), y: (availableSize.height - noSignalSize.height) / 2.0), size: noSignalSize), completion: nil) } } @@ -825,7 +700,7 @@ final class _MediaStreamVideoComponent: Component { } else { self.component?.pictureInPictureClosed() } - // TODO: extract precise animation or observe window changes + // TODO: extract precise animation timing or observe window changes DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.videoView?.alpha = 1 } @@ -850,23 +725,4 @@ final class _MediaStreamVideoComponent: Component { } // TODO: move to appropriate place -var lastFrame: [String: UIView] = [:] - -extension UIView { - func snapshot() -> UIImage? { - UIGraphicsBeginImageContextWithOptions(bounds.size, true, UIScreen.main.scale) - - guard let currentContext = UIGraphicsGetCurrentContext() else { - UIGraphicsEndImageContext() - return nil - } - - layer.render(in: currentContext) - - let image = UIGraphicsGetImageFromCurrentImageContext() - - UIGraphicsEndImageContext() - - return image - } -} +fileprivate var lastFrame: [String: UIView] = [:] diff --git a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift index d15603a52e..486847bc73 100644 --- a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift @@ -8,10 +8,7 @@ import MultilineTextComponent import Display final class StreamSheetComponent: CombinedComponent { -// let color: UIColor -// let leftItem: AnyComponent? let topComponent: AnyComponent? -// let viewerCounter: AnyComponent? let bottomButtonsRow: AnyComponent? // TODO: sync let sheetHeight: CGFloat @@ -24,7 +21,6 @@ final class StreamSheetComponent: CombinedComponent { let videoHeight: CGFloat init( -// color: UIColor, topComponent: AnyComponent, bottomButtonsRow: AnyComponent, topOffset: CGFloat, @@ -36,9 +32,7 @@ final class StreamSheetComponent: CombinedComponent { deviceCornerRadius: CGFloat, videoHeight: CGFloat ) { -// self.leftItem = leftItem self.topComponent = topComponent -// self.viewerCounter = AnyComponent(ViewerCountComponent(count: 0)) self.bottomButtonsRow = bottomButtonsRow self.topOffset = topOffset self.sheetHeight = sheetHeight @@ -83,7 +77,7 @@ final class StreamSheetComponent: CombinedComponent { } return true } -// + final class View: UIView { var overlayComponentsFrames = [CGRect]() @@ -95,7 +89,6 @@ final class StreamSheetComponent: CombinedComponent { } func update(component: StreamSheetComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { -// self.backgroundColor = .purple.withAlphaComponent(0.6) return availableSize } diff --git a/submodules/TelegramCallsUI/Sources/MediaStreamingController.swift b/submodules/TelegramCallsUI/Sources/MediaStreamingController.swift deleted file mode 100644 index eb8caf9148..0000000000 --- a/submodules/TelegramCallsUI/Sources/MediaStreamingController.swift +++ /dev/null @@ -1,7118 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramPresentationData -import TelegramUIPreferences -import TelegramStringFormatting -import TelegramVoip -import TelegramAudio -import AccountContext -import Postbox -import TelegramCore -import MergeLists -import ItemListUI -import AppBundle -import ContextUI -import ShareController -import DeleteChatPeerActionSheetItem -import UndoUI -import AlertUI -import PresentationDataUtils -import DirectionalPanGesture -import PeerInfoUI -import AvatarNode -import TooltipUI -import LegacyUI -import LegacyComponents -import LegacyMediaPickerUI -import WebSearchUI -import MapResourceToAvatarSizes -import SolidRoundedButtonNode -import AudioBlob -import DeviceAccess - -//let panelBackgroundColor = UIColor(rgb: 0x1c1c1e) -//let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e) -//let fullscreenBackgroundColor = UIColor(rgb: 0x000000) -//private let smallButtonSize = CGSize(width: 36.0, height: 36.0) -//private let sideButtonSize = CGSize(width: 56.0, height: 56.0) -//private let topPanelHeight: CGFloat = 63.0 -//let bottomAreaHeight: CGFloat = 206.0 -//private let fullscreenBottomAreaHeight: CGFloat = 80.0 -//private let bottomGradientHeight: CGFloat = 70.0 - -/*func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? { - if !top && !bottom { - return nil - } - return generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { (size, context) in - let bounds = CGRect(origin: CGPoint(), size: size) - context.setFillColor((dark ? fullscreenBackgroundColor : panelBackgroundColor).cgColor) - context.fill(bounds) - - context.setBlendMode(.clear) - - var corners: UIRectCorner = [] - if top { - corners.insert(.topLeft) - corners.insert(.topRight) - } - if bottom { - corners.insert(.bottomLeft) - corners.insert(.bottomRight) - } - let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0)) - context.addPath(path.cgPath) - context.fillPath() - })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25) -} - -func decorationTopCornersImage(dark: Bool) -> UIImage? { - return generateImage(CGSize(width: 50.0, height: 110.0), rotatedContext: { (size, context) in - let bounds = CGRect(origin: CGPoint(), size: size) - context.setFillColor((dark ? fullscreenBackgroundColor : panelBackgroundColor).cgColor) - context.fill(bounds) - - context.setBlendMode(.clear) - - var corners: UIRectCorner = [] - corners.insert(.topLeft) - corners.insert(.topRight) - - let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 60.0, width: 50.0, height: 50.0), byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0)) - context.addPath(path.cgPath) - context.fillPath() - })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 32) -} - -func decorationBottomCornersImage(dark: Bool) -> UIImage? { - return generateImage(CGSize(width: 50.0, height: 110.0), rotatedContext: { (size, context) in - let bounds = CGRect(origin: CGPoint(), size: size) - context.setFillColor((dark ? fullscreenBackgroundColor : panelBackgroundColor).cgColor) - context.fill(bounds) - - context.setBlendMode(.clear) - - var corners: UIRectCorner = [] - corners.insert(.bottomLeft) - corners.insert(.bottomRight) - - let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 50.0, height: 50.0), byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0)) - context.addPath(path.cgPath) - context.fillPath() - })?.resizableImage(withCapInsets: UIEdgeInsets(top: 25.0, left: 25.0, bottom: 0.0, right: 25.0), resizingMode: .stretch) -} -*/ -private func decorationBottomGradientImage(dark: Bool) -> UIImage? { - return generateImage(CGSize(width: 24.0, height: bottomGradientHeight), rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - let color = dark ? fullscreenBackgroundColor : panelBackgroundColor - let colorsArray = [color.withAlphaComponent(0.0).cgColor, color.cgColor] as CFArray - var locations: [CGFloat] = [1.0, 0.0] - let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! - context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - }) -} -/* -struct VoiceChatPeerEntry: Identifiable { - enum State { - case listening - case speaking - case invited - case raisedHand - } - - var peer: Peer - var about: String? - var isMyPeer: Bool - var videoEndpointId: String? - var videoPaused: Bool - var presentationEndpointId: String? - var presentationPaused: Bool - var effectiveSpeakerVideoEndpointId: String? - var state: State - var muteState: GroupCallParticipantsContext.Participant.MuteState? - var canManageCall: Bool - var volume: Int32? - var raisedHand: Bool - var displayRaisedHandStatus: Bool - var active: Bool - var isLandscape: Bool - - var effectiveVideoEndpointId: String? { - return self.presentationEndpointId ?? self.videoEndpointId - } - - init( - peer: Peer, - about: String?, - isMyPeer: Bool, - videoEndpointId: String?, - videoPaused: Bool, - presentationEndpointId: String?, - presentationPaused: Bool, - effectiveSpeakerVideoEndpointId: String?, - state: State, - muteState: GroupCallParticipantsContext.Participant.MuteState?, - canManageCall: Bool, - volume: Int32?, - raisedHand: Bool, - displayRaisedHandStatus: Bool, - active: Bool, - isLandscape: Bool - ) { - self.peer = peer - self.about = about - self.isMyPeer = isMyPeer - self.videoEndpointId = videoEndpointId - self.videoPaused = videoPaused - self.presentationEndpointId = presentationEndpointId - self.presentationPaused = presentationPaused - self.effectiveSpeakerVideoEndpointId = effectiveSpeakerVideoEndpointId - self.state = state - self.muteState = muteState - self.canManageCall = canManageCall - self.volume = volume - self.raisedHand = raisedHand - self.displayRaisedHandStatus = displayRaisedHandStatus - self.active = active - self.isLandscape = isLandscape - } - - var stableId: PeerId { - return self.peer.id - } - - static func ==(lhs: VoiceChatPeerEntry, rhs: VoiceChatPeerEntry) -> Bool { - if !lhs.peer.isEqual(rhs.peer) { - return false - } - if lhs.about != rhs.about { - return false - } - if lhs.isMyPeer != rhs.isMyPeer { - return false - } - if lhs.videoEndpointId != rhs.videoEndpointId { - return false - } - if lhs.videoPaused != rhs.videoPaused { - return false - } - if lhs.presentationEndpointId != rhs.presentationEndpointId { - return false - } - if lhs.presentationPaused != rhs.presentationPaused { - return false - } - if lhs.effectiveSpeakerVideoEndpointId != rhs.effectiveSpeakerVideoEndpointId { - return false - } - if lhs.state != rhs.state { - return false - } - if lhs.muteState != rhs.muteState { - return false - } - if lhs.canManageCall != rhs.canManageCall { - return false - } - if lhs.volume != rhs.volume { - return false - } - if lhs.raisedHand != rhs.raisedHand { - return false - } - if lhs.displayRaisedHandStatus != rhs.displayRaisedHandStatus { - return false - } - if lhs.active != rhs.active { - return false - } - if lhs.isLandscape != rhs.isLandscape { - return false - } - return true - } -} -*/ - -//public protocol VoiceChatController: ViewController { -// var call: PresentationGroupCall { get } -// var currentOverlayController: VoiceChatOverlayController? { get } -// var parentNavigationController: NavigationController? { get set } -// -// func dismiss(closing: Bool, manual: Bool) -//} - -public final class MediaStreamingControllerImpl: ViewController, VoiceChatController { - enum DisplayMode { - case modal(isExpanded: Bool, isFilled: Bool) - case fullscreen(controlsHidden: Bool) - } - - fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { - private struct ListTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] - let isLoading: Bool - let isEmpty: Bool - let canInvite: Bool - let crossFade: Bool - let count: Int - let animated: Bool - } - - private final class Interaction { - let updateIsMuted: (PeerId, Bool) -> Void - let switchToPeer: (PeerId, String?, Bool) -> Void - let openInvite: () -> Void - let peerContextAction: (VoiceChatPeerEntry, ASDisplayNode, ContextGesture?, Bool) -> Void - let getPeerVideo: (String, GroupVideoNode.Position) -> GroupVideoNode? - var isExpanded: Bool = false - - private var audioLevels: [PeerId: ValuePipe] = [:] - - var updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil) - - init( - updateIsMuted: @escaping (PeerId, Bool) -> Void, - switchToPeer: @escaping (PeerId, String?, Bool) -> Void, - openInvite: @escaping () -> Void, - peerContextAction: @escaping (VoiceChatPeerEntry, ASDisplayNode, ContextGesture?, Bool) -> Void, - getPeerVideo: @escaping (String, GroupVideoNode.Position) -> GroupVideoNode? - ) { - self.updateIsMuted = updateIsMuted - self.switchToPeer = switchToPeer - self.openInvite = openInvite - self.peerContextAction = peerContextAction - self.getPeerVideo = getPeerVideo - } - - func getAudioLevel(_ peerId: PeerId) -> Signal { - let signal: Signal - if let current = self.audioLevels[peerId] { - signal = current.signal() - } else { - let value = ValuePipe() - self.audioLevels[peerId] = value - signal = value.signal() - } - return signal - |> mapToSignal { value in - return .single(value) - } - } - - func updateAudioLevels(_ levels: [(PeerId, UInt32, Float, Bool)], reset: Bool = false) { - var updated = Set() - for (peerId, _, level, _) in levels { - if let pipe = self.audioLevels[peerId] { - if reset { - pipe.putNext(level) - } else { - pipe.putNext(max(0.001, level)) - } - updated.insert(peerId) - } - } - if !reset { - for (peerId, pipe) in self.audioLevels { - if !updated.contains(peerId) { - pipe.putNext(0.0) - } - } - } - } - } - - private enum EntryId: Hashable { - case tiles - case invite - case peerId(PeerId) - - static func <(lhs: EntryId, rhs: EntryId) -> Bool { - return lhs.hashValue < rhs.hashValue - } - - static func ==(lhs: EntryId, rhs: EntryId) -> Bool { - switch lhs { - case .tiles: - switch rhs { - case .tiles: - return true - default: - return false - } - case .invite: - switch rhs { - case .invite: - return true - default: - return false - } - case let .peerId(lhsId): - switch rhs { - case let .peerId(rhsId): - return lhsId == rhsId - default: - return false - } - } - } - } - - private enum ListEntry: Comparable, Identifiable { - case tiles([VoiceChatTileItem], VoiceChatTileLayoutMode, Int32, Bool) - case invite(PresentationTheme, PresentationStrings, String, Bool) - case peer(VoiceChatPeerEntry, Int32) - - var stableId: EntryId { - switch self { - case .tiles: - return .tiles - case .invite: - return .invite - case let .peer(peerEntry, _): - return .peerId(peerEntry.peer.id) - } - } - - static func ==(lhs: ListEntry, rhs: ListEntry) -> Bool { - switch lhs { - case let .tiles(lhsTiles, lhsLayoutMode, lhsVideoLimit, lhsReachedLimit): - if case let .tiles(rhsTiles, rhsLayoutMode, rhsVideoLimit, rhsReachedLimit) = rhs, lhsTiles == rhsTiles, lhsLayoutMode == rhsLayoutMode, lhsVideoLimit == rhsVideoLimit, lhsReachedLimit == rhsReachedLimit { - return true - } else { - return false - } - case let .invite(lhsTheme, lhsStrings, lhsText, lhsIsLink): - if case let .invite(rhsTheme, rhsStrings, rhsText, rhsIsLink) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText, lhsIsLink == rhsIsLink { - return true - } else { - return false - } - case let .peer(lhsPeerEntry, lhsIndex): - switch rhs { - case let .peer(rhsPeerEntry, rhsIndex): - return lhsPeerEntry == rhsPeerEntry && lhsIndex == rhsIndex - default: - return false - } - } - } - - static func <(lhs: ListEntry, rhs: ListEntry) -> Bool { - switch lhs { - case .tiles: - return true - case .invite: - return false - case let .peer(_, lhsIndex): - switch rhs { - case .tiles: - return false - case let .peer(_, rhsIndex): - return lhsIndex < rhsIndex - case .invite: - return true - } - } - } - - func tileItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction, isTablet: Bool, videoEndpointId: String, videoReady: Bool, videoTimeouted: Bool, videoIsPaused: Bool, showAsPresentation: Bool, secondary: Bool) -> VoiceChatTileItem? { - guard case let .peer(peerEntry, _) = self else { - return nil - } - let peer = peerEntry.peer - - let icon: VoiceChatTileItem.Icon - var text: VoiceChatParticipantItem.ParticipantText - var additionalText: VoiceChatParticipantItem.ParticipantText? - var speaking = false - - var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() - let yourText: String - if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil { - yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio - } else if peer.smallProfileImage == nil { - yourText = presentationData.strings.VoiceChat_TapToAddPhoto - } else if (peerEntry.about?.isEmpty ?? true) { - yourText = presentationData.strings.VoiceChat_TapToAddBio - } else { - yourText = presentationData.strings.VoiceChat_You - } - - var state = peerEntry.state - if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { - state = .listening - } - switch state { - case .listening: - if peerEntry.isMyPeer { - text = .text(yourText, textIcon, .accent) - } else if let muteState = peerEntry.muteState, muteState.mutedByYou { - text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) - } else if let about = peerEntry.about, !about.isEmpty { - text = .text(about, textIcon, .generic) - } else { - text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) - } - if let muteState = peerEntry.muteState, muteState.mutedByYou { - icon = .microphone(true) - additionalText = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) - } else { - icon = .microphone(peerEntry.muteState != nil) - } - case .speaking: - if let muteState = peerEntry.muteState, muteState.mutedByYou { - text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) - icon = .microphone(true) - additionalText = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) - } else { - if peerEntry.volume != nil { - textIcon.insert(.volume) - } - let volumeValue = peerEntry.volume.flatMap { $0 / 100 } - if let volume = volumeValue, volume != 100 { - text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").string, textIcon, .constructive) - } else { - text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) - } - icon = .microphone(false) - speaking = true - } - case .raisedHand, .invited: - text = .none - icon = .none - } - - if let about = peerEntry.about, !about.isEmpty { - textIcon = [] - text = .text(about, textIcon, .generic) - } - - return VoiceChatTileItem(account: context.account, peer: peerEntry.peer, videoEndpointId: videoEndpointId, videoReady: videoReady, videoTimeouted: videoTimeouted, isVideoLimit: false, videoLimit: 0, isPaused: videoIsPaused, isOwnScreencast: peerEntry.presentationEndpointId == videoEndpointId && peerEntry.isMyPeer, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, speaking: speaking, secondary: secondary, isTablet: isTablet, icon: showAsPresentation ? .presentation : icon, text: text, additionalText: additionalText, action: { - interaction.switchToPeer(peer.id, videoEndpointId, !secondary) - }, contextAction: { node, gesture in - interaction.peerContextAction(peerEntry, node, gesture, false) - }, getVideo: { position in - return interaction.getPeerVideo(videoEndpointId, position) - }, getAudioLevel: { - return interaction.getAudioLevel(peerEntry.peer.id) - }) - } - - func fullscreenItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem { - switch self { - case .tiles: - return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: "", icon: .none, action: { - }) - case .invite: - return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: "", icon: .generic(UIImage(bundleImageName: "Chat/Context Menu/AddUser")!), action: { - interaction.openInvite() - }) - case let .peer(peerEntry, _): - let peer = peerEntry.peer - var textColor: VoiceChatFullscreenParticipantItem.Color = .generic - var color: VoiceChatFullscreenParticipantItem.Color = .generic - let icon: VoiceChatFullscreenParticipantItem.Icon - var text: VoiceChatParticipantItem.ParticipantText - - var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() - let yourText: String - if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil { - yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio - } else if peer.smallProfileImage == nil { - yourText = presentationData.strings.VoiceChat_TapToAddPhoto - } else if (peerEntry.about?.isEmpty ?? true) { - yourText = presentationData.strings.VoiceChat_TapToAddBio - } else { - yourText = presentationData.strings.VoiceChat_You - } - - var state = peerEntry.state - if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { - state = .listening - } - switch state { - case .listening: - if peerEntry.isMyPeer { - text = .text(yourText, textIcon, .accent) - } else if let muteState = peerEntry.muteState, muteState.mutedByYou { - text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) - } else if let about = peerEntry.about, !about.isEmpty { - text = .text(about, textIcon, .generic) - } else { - text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) - } - if let muteState = peerEntry.muteState, muteState.mutedByYou { - textColor = .destructive - color = .destructive - icon = .microphone(true, UIColor(rgb: 0xff3b30)) - } else { - icon = .microphone(peerEntry.muteState != nil, UIColor.white) - color = .accent - } - case .speaking: - if let muteState = peerEntry.muteState, muteState.mutedByYou { - text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) - textColor = .destructive - color = .destructive - icon = .microphone(true, UIColor(rgb: 0xff3b30)) - } else { - if peerEntry.volume != nil { - textIcon.insert(.volume) - } - let volumeValue = peerEntry.volume.flatMap { $0 / 100 } - if let volume = volumeValue, volume != 100 { - text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").string, textIcon, .constructive) - } else { - text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) - } - icon = .microphone(false, UIColor(rgb: 0x34c759)) - textColor = .constructive - color = .constructive - } - case .raisedHand: - text = .none - textColor = .accent - icon = .wantsToSpeak - case .invited: - text = .none - icon = .none - } - - if let about = peerEntry.about, !about.isEmpty { - textIcon = [] - text = .text(about, textIcon, .generic) - } - - var videoEndpointId = peerEntry.effectiveVideoEndpointId - var otherVideoEndpointId: String? - let hasBothVideos = peerEntry.presentationEndpointId != nil && peerEntry.videoEndpointId != nil - if hasBothVideos { - if let effectiveVideoEndpointId = peerEntry.effectiveSpeakerVideoEndpointId { - if effectiveVideoEndpointId == peerEntry.videoEndpointId { - videoEndpointId = peerEntry.presentationEndpointId - otherVideoEndpointId = videoEndpointId - } else if effectiveVideoEndpointId == peerEntry.presentationEndpointId { - videoEndpointId = peerEntry.videoEndpointId - otherVideoEndpointId = videoEndpointId - } - } - } - - var isPaused = false - if videoEndpointId == peerEntry.videoEndpointId { - isPaused = peerEntry.videoPaused - } else if videoEndpointId == peerEntry.presentationEndpointId { - isPaused = peerEntry.presentationPaused - } - - return VoiceChatFullscreenParticipantItem(presentationData: ItemListPresentationData(presentationData), nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peerEntry.peer, videoEndpointId: videoEndpointId, isPaused: isPaused, icon: icon, text: text, textColor: textColor, color: color, isLandscape: peerEntry.isLandscape, active: peerEntry.active, showVideoWhenActive: otherVideoEndpointId != nil, getAudioLevel: { return interaction.getAudioLevel(peerEntry.peer.id) }, getVideo: { - if let videoEndpointId = videoEndpointId { - return interaction.getPeerVideo(videoEndpointId, .list) - } else { - return nil - } - }, action: { _ in - interaction.switchToPeer(peerEntry.peer.id, otherVideoEndpointId, false) - }, contextAction: { node, gesture in - interaction.peerContextAction(peerEntry, node, gesture, true) - }, getUpdatingAvatar: { - return interaction.updateAvatarPromise.get() - }) - } - } - - func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem { - switch self { - case let .tiles(tiles, layoutMode, videoLimit, reachedLimit): - return VoiceChatTilesGridItem(context: context, tiles: tiles, layoutMode: layoutMode, videoLimit: videoLimit, reachedLimit: reachedLimit, getIsExpanded: { - return interaction.isExpanded - }) - case let .invite(_, _, text, isLink): - return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .generic(UIImage(bundleImageName: isLink ? "Chat/Context Menu/Link" : "Chat/Context Menu/AddUser")!), action: { - interaction.openInvite() - }) - case let .peer(peerEntry, _): - let peer = peerEntry.peer - - var text: VoiceChatParticipantItem.ParticipantText - var expandedText: VoiceChatParticipantItem.ParticipantText? - let icon: VoiceChatParticipantItem.Icon - - var state = peerEntry.state - if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { - state = .listening - } - - var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() - let yourText: String - if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil { - yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio - } else if peer.smallProfileImage == nil { - yourText = presentationData.strings.VoiceChat_TapToAddPhoto - } else if (peerEntry.about?.isEmpty ?? true) { - yourText = presentationData.strings.VoiceChat_TapToAddBio - } else { - yourText = presentationData.strings.VoiceChat_You - } - - switch state { - case .listening: - if peerEntry.isMyPeer { - text = .text(yourText, textIcon, .accent) - } else if let muteState = peerEntry.muteState, muteState.mutedByYou { - text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) - } else if let about = peerEntry.about, !about.isEmpty { - text = .text(about, textIcon, .generic) - } else { - text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) - } - let microphoneColor: UIColor - if let muteState = peerEntry.muteState, !muteState.canUnmute || muteState.mutedByYou { - microphoneColor = UIColor(rgb: 0xff3b30) - } else { - microphoneColor = UIColor(rgb: 0x979797) - } - icon = .microphone(peerEntry.muteState != nil, microphoneColor) - case .speaking: - if let muteState = peerEntry.muteState, muteState.mutedByYou { - text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) - icon = .microphone(true, UIColor(rgb: 0xff3b30)) - } else { - if peerEntry.volume != nil { - textIcon.insert(.volume) - } - let volumeValue = peerEntry.volume.flatMap { $0 / 100 } - if let volume = volumeValue, volume != 100 { - text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").string, textIcon, .constructive) - } else { - text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) - } - icon = .microphone(false, UIColor(rgb: 0x34c759)) - } - case .invited: - text = .text(presentationData.strings.VoiceChat_StatusInvited, textIcon, .generic) - icon = .invite(true) - case .raisedHand: - if peerEntry.isMyPeer && !peerEntry.displayRaisedHandStatus { - text = .text(yourText, textIcon, .accent) - } else if let about = peerEntry.about, !about.isEmpty && !peerEntry.displayRaisedHandStatus { - text = .text(about, textIcon, .generic) - } else { - text = .text(presentationData.strings.VoiceChat_StatusWantsToSpeak, textIcon, .accent) - } - icon = .wantsToSpeak - } - - if let about = peerEntry.about, !about.isEmpty { - textIcon = [] - expandedText = .text(about, textIcon, .generic) - } - - return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, text: text, expandedText: expandedText, icon: icon, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, action: { node in - if let node = node { - interaction.peerContextAction(peerEntry, node, nil, false) - } - }, contextAction: { node, gesture in - interaction.peerContextAction(peerEntry, node, gesture, false) - }, getIsExpanded: { - return interaction.isExpanded - }, getUpdatingAvatar: { - return interaction.updateAvatarPromise.get() - }) - } - } - } - - private func preparedTransition(from fromEntries: [ListEntry], to toEntries: [ListEntry], isLoading: Bool, isEmpty: Bool, canInvite: Bool, crossFade: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } - - return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, canInvite: canInvite, crossFade: crossFade, count: toEntries.count, animated: animated) - } - - private func preparedFullscreenTransition(from fromEntries: [ListEntry], to toEntries: [ListEntry], isLoading: Bool, isEmpty: Bool, canInvite: Bool, crossFade: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.fullscreenItem(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.fullscreenItem(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } - - return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, canInvite: canInvite, crossFade: crossFade, count: toEntries.count, animated: animated) - } - - private let currentAvatarMixin = Atomic(value: nil) - - private var configuration: VoiceChatConfiguration? - - private weak var controller: MediaStreamingControllerImpl? - private let sharedContext: SharedAccountContext - private let context: AccountContext - private let call: PresentationGroupCall - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - private var darkTheme: PresentationTheme - - private let dimNode: ASDisplayNode - private let contentContainer: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let listContainer: ASDisplayNode - private let listNode: ListView - private let fullscreenListContainer: ASDisplayNode - private let fullscreenListNode: ListView - private let tileGridNode: VoiceChatTileGridNode - private let topPanelNode: ASDisplayNode - private let topPanelEdgeNode: ASDisplayNode - private let topPanelBackgroundNode: ASDisplayNode - private let optionsButton: VoiceChatHeaderButton - private let closeButton: VoiceChatHeaderButton - private let panelButton: VoiceChatHeaderButton - private let topCornersNode: ASImageNode - fileprivate let bottomPanelNode: ASDisplayNode - private let bottomGradientNode: ASDisplayNode - private let bottomPanelBackgroundNode: ASDisplayNode - private let bottomCornersNode: ASImageNode - fileprivate let audioButton: CallControllerButtonItemNode - fileprivate let cameraButton: CallControllerButtonItemNode - fileprivate let switchCameraButton: CallControllerButtonItemNode - fileprivate let leaveButton: CallControllerButtonItemNode - fileprivate let actionButton: VoiceChatActionButton - private let leftBorderNode: ASDisplayNode - private let rightBorderNode: ASDisplayNode - private let mainStageContainerNode: ASDisplayNode - private let mainStageBackgroundNode: ASDisplayNode - private let mainStageNode: VoiceChatMainStageNode - - private let transitionMaskView: UIView - private let transitionMaskTopFillLayer: CALayer - private let transitionMaskFillLayer: CALayer - private let transitionMaskGradientLayer: CAGradientLayer - private let transitionMaskBottomFillLayer: CALayer - private let transitionContainerNode: ASDisplayNode - - private var isScheduling = false - private let timerNode: VoiceChatTimerNode - private var pickerView: UIDatePicker? - private let dateFormatter: DateFormatter - private let scheduleTextNode: ImmediateTextNode - private let scheduleCancelButton: SolidRoundedButtonNode - private var scheduleButtonTitle = "" - - private let titleNode: VoiceChatTitleNode - private let participantsNode: VoiceChatTimerNode - - private var enqueuedTransitions: [ListTransition] = [] - private var enqueuedFullscreenTransitions: [ListTransition] = [] - - private var validLayout: (ContainerViewLayout, CGFloat)? - private var didSetContentsReady: Bool = false - private var didSetDataReady: Bool = false - - private var isFirstTime = true - private var topInset: CGFloat? - - private var animatingInsertion = false - private var animatingExpansion = false - private var animatingAppearance = false - private var animatingButtonsSwap = false - private var animatingMainStage = false - private var animatingContextMenu = false - private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)? - private var isPanning = false - - private var peer: Peer? - private var currentTitle: String = "" - private var currentTitleIsCustom = false - private var currentSubtitle: String = "" - private var currentSpeakingSubtitle: String? - private var currentCallMembers: ([GroupCallParticipantsContext.Participant], String?)? - private var currentTotalCount: Int32 = 0 - private var currentInvitedPeers: [EnginePeer]? - private var currentSpeakingPeers: Set? - private var currentContentOffset: CGFloat? - private var currentNormalButtonColor: UIColor? - private var currentActiveButtonColor: UIColor? - - private var myEntry: VoiceChatPeerEntry? - private var mainEntry: VoiceChatPeerEntry? - private var currentEntries: [ListEntry] = [] - private var currentFullscreenEntries: [ListEntry] = [] - private var currentTileItems: [VoiceChatTileItem] = [] - private var displayPanelVideos = false - private var joinedVideo: Bool? - - private var peerViewDisposable: Disposable? - private let leaveDisposable = MetaDisposable() - - private var isMutedDisposable: Disposable? - private var isNoiseSuppressionEnabled: Bool = true - private var isNoiseSuppressionEnabledDisposable: Disposable? - private var callStateDisposable: Disposable? - - private var pushingToTalk = false - private var temporaryPushingToTalk = false - private let hapticFeedback = HapticFeedback() - - private var callState: PresentationGroupCallState? - - private var currentLoadToken: String? - - private var scrollAtTop = true - - private var effectiveMuteState: GroupCallParticipantsContext.Participant.MuteState? { - if self.pushingToTalk { - return nil - } else { - return self.callState?.muteState - } - } - - private var audioOutputStateDisposable: Disposable? - private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)? - - private var audioLevelsDisposable: Disposable? - private var myAudioLevelDisposable: Disposable? - private var isSpeakingDisposable: Disposable? - private var memberStatesDisposable: Disposable? - private var actionButtonColorDisposable: Disposable? - - private var itemInteraction: Interaction? - - private let inviteDisposable = MetaDisposable() - private let memberEventsDisposable = MetaDisposable() - private let reconnectedAsEventsDisposable = MetaDisposable() - private let stateVersionDisposable = MetaDisposable() - private var applicationStateDisposable: Disposable? - - private let displayAsPeersPromise = Promise<[FoundPeer]>([]) - private let inviteLinksPromise = Promise(nil) - - private var raisedHandDisplayDisposables: [PeerId: Disposable] = [:] - private var displayedRaisedHands = Set() { - didSet { - self.displayedRaisedHandsPromise.set(self.displayedRaisedHands) - } - } - private let displayedRaisedHandsPromise = ValuePromise>(Set()) - - private var requestedVideoSources = Set() - private var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = [] - - private var videoRenderingContext: VideoRenderingContext - private var videoNodes: [String: GroupVideoNode] = [:] - private var wideVideoNodes = Set() - private var videoOrder: [String] = [] - private var readyVideoEndpointIds = Set() - private var readyVideoEndpointIdsPromise = ValuePromise>(Set()) - private var timeoutedEndpointIds = Set() - private var readyVideoDisposables = DisposableDict() - private var myPeerVideoReadyDisposable = MetaDisposable() - - private var peerIdToEndpointId: [PeerId: String] = [:] - - private var currentSpeakers: [PeerId] = [] - private var currentDominantSpeaker: (PeerId, String?, Double)? - private var currentForcedSpeaker: (PeerId, String?)? - private var effectiveSpeaker: (PeerId, String?, Bool, Bool, Bool)? - - private var updateAvatarDisposable = MetaDisposable() - private let updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil) - private var currentUpdatingAvatar: TelegramMediaImageRepresentation? - - private var connectedOnce = false - private var ignoreConnecting = false - private var ignoreConnectingTimer: SwiftSignalKit.Timer? - - private var displayUnmuteTooltipTimer: SwiftSignalKit.Timer? - private var dismissUnmuteTooltipTimer: SwiftSignalKit.Timer? - private var lastUnmuteTooltipDisplayTimestamp: Double? - - private var panelHidden = false - private var displayMode: DisplayMode = .modal(isExpanded: false, isFilled: false) { - didSet { - if case let .modal(isExpanded, _) = self.displayMode { - self.itemInteraction?.isExpanded = isExpanded - } else { - self.itemInteraction?.isExpanded = true - } - } - } - - private var isExpanded: Bool { - switch self.displayMode { - case .modal(true, _), .fullscreen: - return true - default: - return false - } - } - - private var statsDisposable: Disposable? - - init(controller: MediaStreamingControllerImpl, sharedContext: SharedAccountContext, call: PresentationGroupCall) { - self.controller = controller - self.sharedContext = sharedContext - self.context = call.accountContext - self.call = call - - self.videoRenderingContext = VideoRenderingContext() - - self.isScheduling = call.schedulePending - - let presentationData = sharedContext.currentPresentationData.with { $0 } - self.presentationData = presentationData - - self.darkTheme = defaultDarkColorPresentationTheme - self.currentSubtitle = self.presentationData.strings.SocksProxySetup_ProxyStatusConnecting - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - - self.contentContainer = ASDisplayNode() - self.contentContainer.isHidden = true - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = self.isScheduling ? panelBackgroundColor : secondaryPanelBackgroundColor - self.backgroundNode.clipsToBounds = false - - self.listContainer = ASDisplayNode() - - self.listNode = ListView() - self.listNode.alpha = self.isScheduling ? 0.0 : 1.0 - self.listNode.isUserInteractionEnabled = !self.isScheduling - self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3) - self.listNode.clipsToBounds = true - self.listNode.scroller.bounces = false - self.listNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - self.fullscreenListContainer = ASDisplayNode() - self.fullscreenListContainer.isHidden = true - - self.fullscreenListNode = ListView() - self.fullscreenListNode.transform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0) - self.fullscreenListNode.clipsToBounds = true - self.fullscreenListNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - self.tileGridNode = VoiceChatTileGridNode(context: self.context) - - self.topPanelNode = ASDisplayNode() - self.topPanelNode.clipsToBounds = false - - self.topPanelBackgroundNode = ASDisplayNode() - self.topPanelBackgroundNode.backgroundColor = panelBackgroundColor - self.topPanelBackgroundNode.isUserInteractionEnabled = false - - self.topPanelEdgeNode = ASDisplayNode() - self.topPanelEdgeNode.backgroundColor = panelBackgroundColor - self.topPanelEdgeNode.cornerRadius = 12.0 - self.topPanelEdgeNode.isUserInteractionEnabled = false - if #available(iOS 11.0, *) { - self.topPanelEdgeNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - } - - self.optionsButton = VoiceChatHeaderButton(context: self.context) - self.optionsButton.setContent(.more(optionsCircleImage(dark: false))) - self.closeButton = VoiceChatHeaderButton(context: self.context) - self.closeButton.setContent(.image(closeButtonImage(dark: false))) - self.panelButton = VoiceChatHeaderButton(context: self.context, wide: true) - self.panelButton.setContent(.image(panelButtonImage(dark: false))) - - self.titleNode = VoiceChatTitleNode(theme: self.presentationData.theme) - - self.topCornersNode = ASImageNode() - self.topCornersNode.displaysAsynchronously = false - self.topCornersNode.displayWithoutProcessing = true - self.topCornersNode.image = decorationTopCornersImage(dark: false) - self.topCornersNode.isUserInteractionEnabled = false - - self.bottomPanelNode = ASDisplayNode() - self.bottomPanelNode.clipsToBounds = false - - self.bottomPanelBackgroundNode = ASDisplayNode() - self.bottomPanelBackgroundNode.backgroundColor = panelBackgroundColor - self.bottomPanelBackgroundNode.isUserInteractionEnabled = false - - self.bottomGradientNode = ASDisplayNode() - self.bottomGradientNode.displaysAsynchronously = false - self.bottomGradientNode.backgroundColor = decorationBottomGradientImage(dark: false).flatMap { UIColor(patternImage: $0) } - - self.bottomCornersNode = ASImageNode() - self.bottomCornersNode.displaysAsynchronously = false - self.bottomCornersNode.displayWithoutProcessing = true - self.bottomCornersNode.image = decorationBottomCornersImage(dark: false) - self.bottomCornersNode.isUserInteractionEnabled = false - - self.audioButton = CallControllerButtonItemNode() - self.cameraButton = CallControllerButtonItemNode(largeButtonSize: sideButtonSize.width) - self.switchCameraButton = CallControllerButtonItemNode() - self.switchCameraButton.alpha = 0.0 - self.switchCameraButton.isUserInteractionEnabled = false - self.leaveButton = CallControllerButtonItemNode() - self.actionButton = VoiceChatActionButton() - - if self.isScheduling { - self.cameraButton.alpha = 0.0 - self.cameraButton.isUserInteractionEnabled = false - self.audioButton.alpha = 0.0 - self.audioButton.isUserInteractionEnabled = false - self.leaveButton.alpha = 0.0 - self.leaveButton.isUserInteractionEnabled = false - } - - self.leftBorderNode = ASDisplayNode() - self.leftBorderNode.backgroundColor = panelBackgroundColor - self.leftBorderNode.isUserInteractionEnabled = false - self.leftBorderNode.clipsToBounds = false - - self.rightBorderNode = ASDisplayNode() - self.rightBorderNode.backgroundColor = panelBackgroundColor - self.rightBorderNode.isUserInteractionEnabled = false - self.rightBorderNode.clipsToBounds = false - - self.mainStageContainerNode = ASDisplayNode() - self.mainStageContainerNode.clipsToBounds = true - self.mainStageContainerNode.isUserInteractionEnabled = false - self.mainStageContainerNode.isHidden = true - - self.mainStageBackgroundNode = ASDisplayNode() - self.mainStageBackgroundNode.backgroundColor = .black - self.mainStageBackgroundNode.alpha = 0.0 - self.mainStageBackgroundNode.isUserInteractionEnabled = false - - self.mainStageNode = VoiceChatMainStageNode(context: self.context, call: self.call) - - self.transitionMaskView = UIView() - self.transitionMaskTopFillLayer = CALayer() - self.transitionMaskTopFillLayer.backgroundColor = UIColor.white.cgColor - self.transitionMaskTopFillLayer.opacity = 0.0 - - self.transitionMaskFillLayer = CALayer() - self.transitionMaskFillLayer.backgroundColor = UIColor.white.cgColor - - self.transitionMaskGradientLayer = CAGradientLayer() - self.transitionMaskGradientLayer.colors = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] - self.transitionMaskGradientLayer.locations = [0.0, 1.0] - self.transitionMaskGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0) - self.transitionMaskGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) - - self.transitionMaskBottomFillLayer = CALayer() - self.transitionMaskBottomFillLayer.backgroundColor = UIColor.white.cgColor - self.transitionMaskBottomFillLayer.opacity = 0.0 - - self.transitionMaskView.layer.addSublayer(self.transitionMaskTopFillLayer) - self.transitionMaskView.layer.addSublayer(self.transitionMaskFillLayer) - self.transitionMaskView.layer.addSublayer(self.transitionMaskGradientLayer) - self.transitionMaskView.layer.addSublayer(self.transitionMaskBottomFillLayer) - - self.transitionContainerNode = ASDisplayNode() - self.transitionContainerNode.clipsToBounds = true - self.transitionContainerNode.isUserInteractionEnabled = false - self.transitionContainerNode.view.mask = self.transitionMaskView -// self.transitionContainerNode.view.addSubview(self.transitionMaskView) - - self.scheduleTextNode = ImmediateTextNode() - self.scheduleTextNode.isHidden = !self.isScheduling - self.scheduleTextNode.isUserInteractionEnabled = false - self.scheduleTextNode.textAlignment = .center - self.scheduleTextNode.maximumNumberOfLines = 4 - - self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0) - self.scheduleCancelButton.isHidden = !self.isScheduling - - self.dateFormatter = DateFormatter() - self.dateFormatter.timeStyle = .none - self.dateFormatter.dateStyle = .short - self.dateFormatter.timeZone = TimeZone.current - - self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) - self.timerNode.isHidden = true - - self.participantsNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) - - super.init() - - let context = self.context - let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId) - |> map { peer in - return [FoundPeer(peer: peer, subscribers: nil)] - } - - self.isNoiseSuppressionEnabledDisposable = (call.isNoiseSuppressionEnabled - |> deliverOnMainQueue).start(next: { [weak self] value in - guard let strongSelf = self else { - return - } - strongSelf.isNoiseSuppressionEnabled = value - }) - - let displayAsPeers: Signal<[FoundPeer], NoError> = currentAccountPeer - |> then( - combineLatest(currentAccountPeer, context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: call.peerId)) - |> map { currentAccountPeer, availablePeers -> [FoundPeer] in - var result = currentAccountPeer - result.append(contentsOf: availablePeers) - return result - } - ) - self.displayAsPeersPromise.set(displayAsPeers) - - self.inviteLinksPromise.set(.single(nil) - |> then(call.inviteLinks)) - - self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in - let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted) - }, switchToPeer: { [weak self] peerId, videoEndpointId, expand in - if let strongSelf = self, strongSelf.connectedOnce { - if expand, let videoEndpointId = videoEndpointId { - strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime() + 3.0) - strongSelf.updateDisplayMode(.fullscreen(controlsHidden: false)) - } else { - strongSelf.currentForcedSpeaker = nil - if peerId != strongSelf.currentDominantSpeaker?.0 || (videoEndpointId != nil && videoEndpointId != strongSelf.currentDominantSpeaker?.1) { - strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) - } - strongSelf.updateMainVideo(waitForFullSize: true, updateMembers: true, force: true) - } - } - }, openInvite: { [weak self] in - guard let strongSelf = self else { - return - } - - let groupPeer = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.call.peerId)) - let _ = combineLatest(queue: Queue.mainQueue(), groupPeer, strongSelf.inviteLinksPromise.get() |> take(1)).start(next: { groupPeer, inviteLinks in - guard let strongSelf = self else { - return - } - guard let groupPeer = groupPeer else { - return - } - - if case let .channel(groupPeer) = groupPeer { - var canInviteMembers = true - if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) { - canInviteMembers = false - } - if !canInviteMembers { - if let inviteLinks = inviteLinks { - strongSelf.presentShare(inviteLinks) - } - return - } - } - - var filters: [ChannelMembersSearchFilter] = [] - if let (currentCallMembers, _) = strongSelf.currentCallMembers { - filters.append(.disable(Array(currentCallMembers.map { $0.peer.id }))) - } - if case let .channel(groupPeer) = groupPeer { - if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil { - filters.append(.excludeNonMembers) - } - } else if case let .legacyGroup(groupPeer) = groupPeer { - if groupPeer.hasBannedPermission(.banAddMembers) { - filters.append(.excludeNonMembers) - } - } - filters.append(.excludeBots) - - var dismissController: (() -> Void)? - let controller = ChannelMembersSearchController(context: strongSelf.context, peerId: groupPeer.id, forceTheme: strongSelf.darkTheme, mode: .inviteToCall, filters: filters, openPeer: { peer, participant in - guard let strongSelf = self else { - dismissController?() - return - } - - let peer = EnginePeer(peer) - - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - if peer.id == strongSelf.callState?.myPeerId { - return - } - if let participant = participant { - dismissController?() - - if strongSelf.call.invitePeer(participant.peer.id) { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), text: text, action: nil), action: { _ in return false }) - } - } else { - if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { - let text = strongSelf.presentationData.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - - strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in - dismissController?() - - if let strongSelf = self { - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self { - strongSelf.presentUndoOverlay(content: .forward(savedMessages: false, text: strongSelf.presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return true }) - } - }) - } - })]), in: .window(.root)) - } else { - let text: String - if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info { - text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - - strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { - guard let strongSelf = self else { - return - } - - if case let .channel(groupPeer) = groupPeer { - let selfController = strongSelf.controller - let inviteDisposable = strongSelf.inviteDisposable - var inviteSignal = strongSelf.context.peerChannelMemberCategoriesContextsManager.addMembers(engine: strongSelf.context.engine, peerId: groupPeer.id, memberIds: [peer.id]) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak selfController] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - selfController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - inviteSignal = inviteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - inviteDisposable.set(nil) - } - - inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in - dismissController?() - guard let strongSelf = self else { - return - } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - switch error { - case .limitExceeded: - text = presentationData.strings.Channel_ErrorAddTooMuch - case .tooMuchJoined: - text = presentationData.strings.Invite_ChannelsTooMuch - case .generic: - text = presentationData.strings.Login_UnknownError - case .restricted: - text = presentationData.strings.Channel_ErrorAddBlocked - case .notMutualContact: - if case .broadcast = groupPeer.info { - text = presentationData.strings.Channel_AddUserLeftError - } else { - text = presentationData.strings.GroupInfo_AddUserLeftError - } - case .botDoesntSupportGroups: - text = presentationData.strings.Channel_BotDoesntSupportGroups - case .tooMuchBots: - text = presentationData.strings.Channel_TooMuchBots - case .bot: - text = presentationData.strings.Login_UnknownError - case .kicked: - text = presentationData.strings.Channel_AddUserKickedError - } - strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { - guard let strongSelf = self else { - dismissController?() - return - } - dismissController?() - - if strongSelf.call.invitePeer(peer.id) { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil), action: { _ in return false }) - } - })) - } else if case let .legacyGroup(groupPeer) = groupPeer { - let selfController = strongSelf.controller - let inviteDisposable = strongSelf.inviteDisposable - var inviteSignal = strongSelf.context.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak selfController] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - selfController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - inviteSignal = inviteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - inviteDisposable.set(nil) - } - - inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in - dismissController?() - guard let strongSelf = self else { - return - } - let context = strongSelf.context - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - switch error { - case .privacy: - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peer.id) - |> deliverOnMainQueue).start(next: { peer in - self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }) - case .notMutualContact: - strongSelf.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - case .tooManyChannels: - strongSelf.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - case .groupFull, .generic: - strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - }, completed: { - guard let strongSelf = self else { - dismissController?() - return - } - dismissController?() - - if strongSelf.call.invitePeer(peer.id) { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil), action: { _ in return false }) - } - })) - } - })]), in: .window(.root)) - } - } - }) - controller.copyInviteLink = { - dismissController?() - - guard let strongSelf = self else { - return - } - let callPeerId = strongSelf.call.peerId - - let _ = (strongSelf.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId), - TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId) - ) - |> map { peer, exportedInvitation -> String? in - if let link = inviteLinks?.listenerLink { - return link - } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { - return "https://t.me/\(addressName)" - } else if let link = exportedInvitation?.link { - return link - } else { - return nil - } - } - |> deliverOnMainQueue).start(next: { link in - guard let strongSelf = self else { - return - } - - if let link = link { - UIPasteboard.general.string = link - - strongSelf.presentUndoOverlay(content: .linkCopied(text: strongSelf.presentationData.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false }) - } - }) - } - dismissController = { [weak controller] in - controller?.dismiss() - } - strongSelf.controller?.push(controller) - }) - }, peerContextAction: { [weak self] entry, sourceNode, gesture, fullscreen in - guard let strongSelf = self, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { - return - } - - let muteStatePromise = Promise(entry.muteState) - - let itemsForEntry: (VoiceChatPeerEntry, GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { entry, muteState in - var items: [ContextMenuItem] = [] - - var hasVolumeSlider = false - let peer = entry.peer - if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou { - } else { - if entry.canManageCall || !entry.isMyPeer { - hasVolumeSlider = true - - let minValue: CGFloat - if let callState = strongSelf.callState, callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil { - minValue = 0.01 - } else { - minValue = 0.0 - } - items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: entry.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { newValue, finished in - if finished && newValue.isZero { - let updatedMuteState = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true) - muteStatePromise.set(.single(updatedMuteState)) - } else { - strongSelf.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished) - } - }), true)) - } - } - - if entry.isMyPeer && !hasVolumeSlider && ((entry.about?.isEmpty ?? true) || entry.peer.smallProfileImage == nil) { - items.append(.custom(VoiceChatInfoContextItem(text: strongSelf.presentationData.strings.VoiceChat_ImproveYourProfileText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) - }), true)) - } - - if peer.id == strongSelf.callState?.myPeerId { - if entry.raisedHand { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_CancelSpeakRequest, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - let _ = strongSelf.call.lowerHand() - f(.default) - }))) - } - items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? strongSelf.presentationData.strings.VoiceChat_AddPhoto : strongSelf.presentationData.strings.VoiceChat_ChangePhoto, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - f(.default) - Queue.mainQueue().after(0.1) { - strongSelf.openAvatarForEditing(fromGallery: false, completion: {}) - } - }))) - - items.append(.action(ContextMenuActionItem(text: (entry.about?.isEmpty ?? true) ? strongSelf.presentationData.strings.VoiceChat_AddBio : strongSelf.presentationData.strings.VoiceChat_EditBio, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - f(.default) - - Queue.mainQueue().after(0.1) { - let maxBioLength: Int - if peer.id.namespace == Namespaces.Peer.CloudUser { - maxBioLength = 70 - } else { - maxBioLength = 100 - } - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditBioTitle, text: presentationData.strings.VoiceChat_EditBioText, placeholder: presentationData.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, value: entry.about, maxLength: maxBioLength, apply: { bio in - if let strongSelf = self, let bio = bio { - if peer.id.namespace == Namespaces.Peer.CloudUser { - let _ = (strongSelf.context.engine.accountData.updateAbout(about: bio) - |> `catch` { _ -> Signal in - return .complete() - }).start() - } else { - let _ = (strongSelf.context.engine.peers.updatePeerDescription(peerId: peer.id, description: bio) - |> `catch` { _ -> Signal in - return .complete() - }).start() - } - - strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false }) - } - }) - self?.controller?.present(controller, in: .window(.root)) - } - }))) - - if let peer = peer as? TelegramUser { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ChangeName, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - f(.default) - - Queue.mainQueue().after(0.1) { - let controller = voiceChatUserNameController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: presentationData.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: presentationData.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { firstAndLastName in - if let strongSelf = self, let (firstName, lastName) = firstAndLastName { - let _ = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).start() - - strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess), action: { _ in return false }) - } - }) - self?.controller?.present(controller, in: .window(.root)) - } - }))) - } - } else { - if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) { - if callState.adminIds.contains(peer.id) { - if let _ = muteState { - } else { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } else { - if let muteState = muteState, !muteState.canUnmute { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: entry.raisedHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false) - f(.default) - - strongSelf.presentUndoOverlay(content: .voiceChatCanSpeak(text: presentationData.strings.VoiceChat_UserCanNowSpeak(EnginePeer(entry.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return true }) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } - } else { - if let muteState = muteState, muteState.mutedByYou { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmuteForMe, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false) - f(.default) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MuteForMe, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } - - let openTitle: String - let openIcon: UIImage? - if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - openTitle = strongSelf.presentationData.strings.VoiceChat_OpenChannel - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") - } else { - openTitle = strongSelf.presentationData.strings.VoiceChat_OpenGroup - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups") - } - } else { - openTitle = strongSelf.presentationData.strings.Conversation_ContextMenuSendMessage - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message") - } - items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in - return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self, let navigationController = strongSelf.controller?.parentNavigationController else { - return - } - - let context = strongSelf.context - strongSelf.controller?.dismiss(completion: { - Queue.mainQueue().after(0.3) { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - }) - - f(.dismissWithoutContent) - }))) - - if let callState = strongSelf.callState, (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != strongSelf.call.peerId { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) - }, action: { [weak self] c, _ in - c.dismiss(completion: { - guard let strongSelf = self else { - return - } - - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.call.peerId) - |> deliverOnMainQueue).start(next: { [weak self] chatPeer in - guard let strongSelf = self else { - return - } - - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme)) - var items: [ActionSheetItem] = [] - - items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder)) - - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { - return - } - - let _ = strongSelf.context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: strongSelf.context.engine, peerId: strongSelf.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() - strongSelf.call.removedPeer(peer.id) - - strongSelf.presentUndoOverlay(content: .banned(text: strongSelf.presentationData.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return false }) - })) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - strongSelf.controller?.present(actionSheet, in: .window(.root)) - }) - }) - }))) - } - } - return items - } - - let items = muteStatePromise.get() - |> map { muteState -> [ContextMenuItem] in - return itemsForEntry(entry, muteState) - } - - var centerVertically = entry.peer.smallProfileImage != nil || (!fullscreen && entry.effectiveVideoEndpointId != nil) - if let (layout, _) = strongSelf.validLayout, case .regular = layout.metrics.widthClass { - centerVertically = false - } - - var useMaskView = true - if case .fullscreen = strongSelf.displayMode { - useMaskView = false - } - - let dismissPromise = ValuePromise(false) - let source = VoiceChatContextExtractedContentSource(sourceNode: sourceNode, maskView: useMaskView ? strongSelf.transitionMaskView : nil, keepInPlace: false, blurBackground: true, centerVertically: centerVertically, shouldBeDismissed: dismissPromise.get(), animateTransitionIn: { [weak self] in - if let strongSelf = self { - strongSelf.animatingContextMenu = true - strongSelf.updateDecorationsLayout(transition: .immediate) - if strongSelf.isLandscape { - strongSelf.transitionMaskTopFillLayer.opacity = 1.0 - } - strongSelf.transitionContainerNode.view.mask = nil - strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, removeOnCompletion: false, completion: { [weak self] _ in - Queue.mainQueue().after(0.3) { - self?.transitionMaskTopFillLayer.opacity = 0.0 - self?.transitionMaskBottomFillLayer.removeAllAnimations() - self?.animatingContextMenu = false - self?.updateDecorationsLayout(transition: .immediate) - } - }) - } - }, animateTransitionOut: { [weak self] in - if let strongSelf = self { - strongSelf.animatingContextMenu = true - strongSelf.updateDecorationsLayout(transition: .immediate) - strongSelf.transitionMaskTopFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) - strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, completion: { [weak self] _ in - self?.animatingContextMenu = false - self?.updateDecorationsLayout(transition: .immediate) - self?.transitionContainerNode.view.mask = self?.transitionMaskView - }) - } - }) - sourceNode.requestDismiss = { - dismissPromise.set(true) - } - - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) - contextController.useComplexItemsTransitionAnimation = true - strongSelf.controller?.presentInGlobalOverlay(contextController) - }, getPeerVideo: { [weak self] endpointId, position in - guard let strongSelf = self else { - return nil - } - var ignore = false - if case .mainstage = position { - ignore = false - } else if case .fullscreen = strongSelf.displayMode, !strongSelf.isPanning { - ignore = ![.mainstage, .list].contains(position) - } else { - ignore = position != .tile - } - if ignore { - return nil - } - if !strongSelf.readyVideoEndpointIds.contains(endpointId) { - return nil - } - for (listEndpointId, videoNode) in strongSelf.videoNodes { - if listEndpointId == endpointId { - if position != .mainstage && videoNode.isMainstageExclusive { - return nil - } - return videoNode - } - } - return nil - }) - self.itemInteraction?.updateAvatarPromise = self.updateAvatarPromise - - self.topPanelNode.addSubnode(self.topPanelEdgeNode) - self.topPanelNode.addSubnode(self.topPanelBackgroundNode) - self.topPanelNode.addSubnode(self.titleNode) - self.topPanelNode.addSubnode(self.optionsButton) - self.topPanelNode.addSubnode(self.closeButton) - self.topPanelNode.addSubnode(self.panelButton) - - self.bottomPanelNode.addSubnode(self.cameraButton) - self.bottomPanelNode.addSubnode(self.audioButton) - self.bottomPanelNode.addSubnode(self.switchCameraButton) - self.bottomPanelNode.addSubnode(self.leaveButton) - self.bottomPanelNode.addSubnode(self.actionButton) - self.bottomPanelNode.addSubnode(self.scheduleCancelButton) - - self.addSubnode(self.dimNode) - self.addSubnode(self.contentContainer) - - self.contentContainer.addSubnode(self.backgroundNode) - -// self.contentContainer.addSubnode(self.listContainer) -// self.contentContainer.addSubnode(self.topPanelNode) -// self.listContainer.addSubnode(self.listNode) -// self.listContainer.addSubnode(self.leftBorderNode) -// self.listContainer.addSubnode(self.rightBorderNode) -// self.listContainer.addSubnode(self.bottomCornersNode) - self.listContainer.addSubnode(self.topCornersNode) - self.contentContainer.addSubnode(self.bottomGradientNode) -// self.contentContainer.addSubnode(self.bottomPanelBackgroundNode) - self.contentContainer.addSubnode(self.participantsNode) - self.contentContainer.addSubnode(self.tileGridNode) - self.contentContainer.addSubnode(self.mainStageContainerNode) - self.contentContainer.addSubnode(self.transitionContainerNode) - self.contentContainer.addSubnode(self.bottomPanelNode) - self.contentContainer.addSubnode(self.timerNode) - self.contentContainer.addSubnode(self.scheduleTextNode) - self.contentContainer.addSubnode(self.fullscreenListContainer) - self.fullscreenListContainer.addSubnode(self.fullscreenListNode) - - self.mainStageContainerNode.addSubnode(self.mainStageBackgroundNode) - self.mainStageContainerNode.addSubnode(self.mainStageNode) - - self.updateDecorationsColors() - - let invitedPeers: Signal<[EnginePeer], NoError> = self.call.invitedPeers - |> mapToSignal { ids -> Signal<[EnginePeer], NoError> in - return context.engine.data.get(EngineDataList( - ids.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - )) - |> map { itemList -> [EnginePeer] in - return itemList.compactMap { $0 } - } - } - - self.presentationDataDisposable = (sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.presentationData = presentationData - - let sourceColor = presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor - let color: UIColor - if sourceColor.alpha < 1.0 { - color = presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor.mixedWith(sourceColor.withAlphaComponent(1.0), alpha: sourceColor.alpha) - } else { - color = sourceColor - } - strongSelf.actionButton.connectingColor = color - } - }) - - self.memberStatesDisposable = (combineLatest(queue: .mainQueue(), - self.call.state, - self.call.members, - invitedPeers, - self.displayAsPeersPromise.get(), - self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) - ) - |> mapToThrottled { values in - return .single(values) - |> then(.complete() |> delay(0.1, queue: Queue.mainQueue())) - }).start(next: { [weak self] state, callMembers, invitedPeers, displayAsPeers, preferencesView in - guard let strongSelf = self else { - return - } - - let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue - let configuration = VoiceChatConfiguration.with(appConfiguration: appConfiguration) - strongSelf.configuration = configuration - - var animate = false - if strongSelf.callState != state { - if let previousCallState = strongSelf.callState { - var networkStateUpdated = false - if case .connecting = previousCallState.networkState, case .connected = state.networkState { - networkStateUpdated = true - strongSelf.connectedOnce = true - } - var canUnmuteUpdated = false - if previousCallState.muteState?.canUnmute != state.muteState?.canUnmute { - canUnmuteUpdated = true - } - if previousCallState.isVideoEnabled != state.isVideoEnabled || (state.isVideoEnabled && networkStateUpdated) || canUnmuteUpdated { - strongSelf.animatingButtonsSwap = true - animate = true - } - } - strongSelf.callState = state - strongSelf.mainStageNode.callState = state - - if let muteState = state.muteState, !muteState.canUnmute { - if strongSelf.pushingToTalk { - strongSelf.pushingToTalk = false - strongSelf.actionButton.pressing = false - strongSelf.actionButton.isUserInteractionEnabled = false - strongSelf.actionButton.isUserInteractionEnabled = true - } - } - } - - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: (callMembers?.participants ?? [], callMembers?.loadMoreToken), invitedPeers: invitedPeers, speakingPeers: callMembers?.speakingParticipants ?? []) - - let totalCount = Int32(max(1, callMembers?.totalCount ?? 0)) - strongSelf.currentTotalCount = totalCount - - let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(totalCount) - strongSelf.currentSubtitle = subtitle - - if strongSelf.isScheduling { - strongSelf.optionsButton.isUserInteractionEnabled = false - strongSelf.optionsButton.alpha = 0.0 - strongSelf.closeButton.isUserInteractionEnabled = false - strongSelf.closeButton.alpha = 0.0 - strongSelf.panelButton.isUserInteractionEnabled = false - strongSelf.panelButton.alpha = 0.0 - } else { - if let (layout, _) = strongSelf.validLayout { - if case .regular = layout.metrics.widthClass, !strongSelf.peerIdToEndpointId.isEmpty { - strongSelf.panelButton.isUserInteractionEnabled = true - } else { - strongSelf.panelButton.isUserInteractionEnabled = false - } - } - if let callState = strongSelf.callState, callState.canManageCall { - strongSelf.optionsButton.isUserInteractionEnabled = true - } else if displayAsPeers.count > 1 { - strongSelf.optionsButton.isUserInteractionEnabled = true - } else { - strongSelf.optionsButton.isUserInteractionEnabled = true - } - } - - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: animate ? .animated(duration: 0.4, curve: .spring) : .immediate) - } - }) - - let titleAndRecording: Signal<(String?, Bool), NoError> = self.call.state - |> map { state -> (String?, Bool) in - return (state.title, state.recordingStartTimestamp != nil) - } - self.peerViewDisposable = combineLatest(queue: Queue.mainQueue(), self.context.account.viewTracker.peerView(self.call.peerId), titleAndRecording).start(next: { [weak self] view, titleAndRecording in - guard let strongSelf = self else { - return - } - - let (title, isRecording) = titleAndRecording - if let peer = peerViewMainPeer(view) { - let isLivestream: Bool - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - isLivestream = true - } else { - isLivestream = false - } - strongSelf.participantsNode.isHidden = !isLivestream || strongSelf.isScheduled - - let hadPeer = strongSelf.peer != nil - strongSelf.peer = peer - strongSelf.currentTitleIsCustom = title != nil - strongSelf.currentTitle = title ?? EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) - - strongSelf.updateTitle(transition: .immediate) - strongSelf.titleNode.isRecording = isRecording - - if strongSelf.isScheduling && !hadPeer { - strongSelf.updateScheduleButtonTitle() - } - } - if !strongSelf.didSetDataReady { - strongSelf.didSetDataReady = true - strongSelf.updateMembers() - strongSelf.controller?.dataReady.set(true) - } - }) - - self.audioOutputStateDisposable = (self.call.audioOutputState - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { - return - } - - var existingOutputs = Set() - var filteredOutputs: [AudioSessionOutput] = [] - for output in state.0 { - if case let .port(port) = output { - if !existingOutputs.contains(port.name) { - existingOutputs.insert(port.name) - filteredOutputs.append(output) - } - } else { - filteredOutputs.append(output) - } - } - - let wasEmpty = strongSelf.audioOutputState == nil - strongSelf.audioOutputState = (filteredOutputs, state.1) - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - } - if wasEmpty { - strongSelf.controller?.audioOutputStateReady.set(true) - } - }) - - self.audioLevelsDisposable = (self.call.audioLevels - |> deliverOnMainQueue).start(next: { [weak self] levels in - guard let strongSelf = self else { - return - } - var levels = levels - if strongSelf.effectiveMuteState != nil { - levels = levels.filter { $0.0 != strongSelf.callState?.myPeerId } - } - - var maxLevelWithVideo: (PeerId, Float)? - for (peerId, source, level, hasSpeech) in levels { - let hasVideo = strongSelf.peerIdToEndpointId[peerId] != nil - if hasSpeech && source != 0 && hasVideo { - if let (_, currentLevel) = maxLevelWithVideo { - if currentLevel < level { - maxLevelWithVideo = (peerId, level) - } - } else { - maxLevelWithVideo = (peerId, level) - } - } - } - - if maxLevelWithVideo == nil { - if let (peerId, _, _) = strongSelf.currentDominantSpeaker { - maxLevelWithVideo = (peerId, 0.0) - } else if strongSelf.peerIdToEndpointId.count > 0 { - for entry in strongSelf.currentFullscreenEntries { - if case let .peer(peerEntry, _) = entry { - if let _ = peerEntry.effectiveVideoEndpointId { - maxLevelWithVideo = (peerEntry.peer.id, 0.0) - break - } - } - } - } - } - - if case .fullscreen = strongSelf.displayMode, !strongSelf.mainStageNode.animating && !strongSelf.animatingExpansion { - if let (peerId, _) = maxLevelWithVideo { - if let (currentPeerId, _, timestamp) = strongSelf.currentDominantSpeaker { - if CACurrentMediaTime() - timestamp > 2.5 && peerId != currentPeerId { - strongSelf.currentDominantSpeaker = (peerId, nil, CACurrentMediaTime()) - strongSelf.updateMainVideo(waitForFullSize: true) - } - } - } - } - - strongSelf.itemInteraction?.updateAudioLevels(levels) - }) - - self.myAudioLevelDisposable = (self.call.myAudioLevel - |> deliverOnMainQueue).start(next: { [weak self] level in - guard let strongSelf = self else { - return - } - var effectiveLevel: Float = 0.0 - if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk { - effectiveLevel = level - } else if level > 0.1 { - effectiveLevel = level * 0.5 - } - strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel)) - }) - - self.isSpeakingDisposable = (self.call.isSpeaking - |> deliverOnMainQueue).start(next: { [weak self] isSpeaking in - guard let strongSelf = self else { - return - } - if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk { - strongSelf.displayUnmuteTooltipTimer?.invalidate() - strongSelf.displayUnmuteTooltipTimer = nil - strongSelf.dismissUnmuteTooltipTimer?.invalidate() - strongSelf.dismissUnmuteTooltipTimer = nil - } else { - if isSpeaking { - var shouldDisplayTooltip = false - if let previousTimstamp = strongSelf.lastUnmuteTooltipDisplayTimestamp, CACurrentMediaTime() > previousTimstamp + 45.0 { - shouldDisplayTooltip = true - } else if strongSelf.lastUnmuteTooltipDisplayTimestamp == nil { - shouldDisplayTooltip = true - } - if shouldDisplayTooltip { - strongSelf.dismissUnmuteTooltipTimer?.invalidate() - strongSelf.dismissUnmuteTooltipTimer = nil - - if strongSelf.displayUnmuteTooltipTimer == nil { - let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.lastUnmuteTooltipDisplayTimestamp = CACurrentMediaTime() - strongSelf.displayUnmuteTooltip() - strongSelf.displayUnmuteTooltipTimer?.invalidate() - strongSelf.displayUnmuteTooltipTimer = nil - strongSelf.dismissUnmuteTooltipTimer?.invalidate() - strongSelf.dismissUnmuteTooltipTimer = nil - }, queue: Queue.mainQueue()) - timer.start() - strongSelf.displayUnmuteTooltipTimer = timer - } - } - } else if strongSelf.dismissUnmuteTooltipTimer == nil && strongSelf.displayUnmuteTooltipTimer != nil { - let timer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.displayUnmuteTooltipTimer?.invalidate() - strongSelf.displayUnmuteTooltipTimer = nil - - strongSelf.dismissUnmuteTooltipTimer?.invalidate() - strongSelf.dismissUnmuteTooltipTimer = nil - }, queue: Queue.mainQueue()) - timer.start() - strongSelf.dismissUnmuteTooltipTimer = timer - } - } - }) - - self.leaveButton.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside) - self.actionButton.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside) - self.audioButton.addTarget(self, action: #selector(self.audioPressed), forControlEvents: .touchUpInside) - self.cameraButton.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside) - self.switchCameraButton.addTarget(self, action: #selector(self.switchCameraPressed), forControlEvents: .touchUpInside) - self.optionsButton.contextAction = { [weak self] sourceNode, gesture in - self?.openSettingsMenu(sourceNode: sourceNode, gesture: gesture) - } - self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside) - self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) - self.panelButton.addTarget(self, action: #selector(self.panelPressed), forControlEvents: .touchUpInside) - - self.actionButtonColorDisposable = (self.actionButton.outerColor - |> deliverOnMainQueue).start(next: { [weak self] normalColor, activeColor in - if let strongSelf = self { - let animated = strongSelf.currentNormalButtonColor != nil || strongSelf.currentActiveButtonColor == nil - strongSelf.currentNormalButtonColor = normalColor - strongSelf.currentActiveButtonColor = activeColor - strongSelf.updateButtons(transition: animated ? .animated(duration: 0.3, curve: .linear) : .immediate) - } - }) - - self.fullscreenListNode.updateFloatingHeaderOffset = { [weak self] _, _ in - guard let strongSelf = self else { - return - } - - var visiblePeerIds = Set() - strongSelf.fullscreenListNode.forEachVisibleItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { - if item.videoEndpointId == nil { - visiblePeerIds.insert(item.peer.id) - } - } - } - strongSelf.mainStageNode.update(visiblePeerIds: visiblePeerIds) - } - - self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in - if let strongSelf = self { - strongSelf.currentContentOffset = offset - if !(strongSelf.animatingExpansion || strongSelf.animatingInsertion || strongSelf.animatingAppearance) && (strongSelf.panGestureArguments == nil || strongSelf.isExpanded) { - strongSelf.updateDecorationsLayout(transition: transition) - } - } - } - - self.listNode.visibleContentOffsetChanged = { [weak self] offset in - guard let strongSelf = self else { - return - } - var scrollAtTop = false - if case let .known(value) = offset, value < 180.0 { - scrollAtTop = true - } else { - scrollAtTop = false - } - if scrollAtTop != strongSelf.scrollAtTop { - strongSelf.scrollAtTop = scrollAtTop - strongSelf.updateTitle(transition: .immediate) - } - } - - self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in - guard let strongSelf = self else { - return - } - if case let .known(value) = offset, value < 200.0 { - if let loadMoreToken = strongSelf.currentCallMembers?.1 { - strongSelf.currentLoadToken = loadMoreToken - strongSelf.call.loadMoreMembers(token: loadMoreToken) - } - } - } - - self.memberEventsDisposable.set((self.call.memberEvents - |> deliverOnMainQueue).start(next: { [weak self] event in - guard let strongSelf = self else { - return - } - if event.joined { - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - return - } - let text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(EnginePeer(event.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(event.peer), text: text, action: nil), action: { _ in return false }) - } - })) - - self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let strongSelf = self else { - return - } - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_DisplayAsSuccess(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(peer), text: text, action: nil), action: { _ in return false }) - })) - - self.stateVersionDisposable.set((self.call.stateVersion - |> distinctUntilChanged - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.callStateDidReset() - })) - - self.titleNode.tapped = { [weak self] in - if let strongSelf = self, !strongSelf.isScheduling { - if strongSelf.callState?.canManageCall ?? false { - strongSelf.openTitleEditing() - } else if !strongSelf.titleNode.recordingIconNode.isHidden { - var hasTooltipAlready = false - strongSelf.controller?.forEachController { controller -> Bool in - if controller is TooltipScreen { - hasTooltipAlready = true - } - return true - } - if !hasTooltipAlready { - let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil) - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = presentationData.strings.LiveStream_RecordingInProgress - } else { - text = presentationData.strings.VoiceChat_RecordingInProgress - } - strongSelf.controller?.present(TooltipScreen(account: strongSelf.context.account, text: text, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in - return .dismiss(consume: true) - }), in: .window(.root)) - } - } - } - } - - self.scheduleCancelButton.pressed = { [weak self] in - if let strongSelf = self { - strongSelf.dismissScheduled() - } - } - - self.mainStageNode.controlsHidden = { [weak self] hidden in - if let strongSelf = self { - if hidden { - strongSelf.fullscreenListNode.alpha = 0.0 - } else { - strongSelf.fullscreenListNode.alpha = 1.0 - strongSelf.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - } - - self.mainStageNode.tapped = { [weak self] in - if let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout, !strongSelf.animatingExpansion && !strongSelf.animatingMainStage && !strongSelf.mainStageNode.animating { - if case .regular = layout.metrics.widthClass { - strongSelf.panelHidden = !strongSelf.panelHidden - - strongSelf.animatingExpansion = true - let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) - strongSelf.updateDecorationsLayout(transition: transition) - } else { - let effectiveDisplayMode = strongSelf.displayMode - let nextDisplayMode: DisplayMode - switch effectiveDisplayMode { - case .modal: - nextDisplayMode = effectiveDisplayMode - case let .fullscreen(controlsHidden): - if controlsHidden { - nextDisplayMode = .fullscreen(controlsHidden: false) - } else { - nextDisplayMode = .fullscreen(controlsHidden: true) - } - } - strongSelf.updateDisplayMode(nextDisplayMode) - } - } - } - - self.mainStageNode.stopScreencast = { [weak self] in - if let strongSelf = self { - strongSelf.call.disableScreencast() - } - } - - self.mainStageNode.back = { [weak self] in - if let strongSelf = self, !strongSelf.isPanning && !strongSelf.animatingExpansion && !strongSelf.mainStageNode.animating { - strongSelf.currentForcedSpeaker = nil - strongSelf.updateDisplayMode(.modal(isExpanded: true, isFilled: true), fromPan: true) - strongSelf.effectiveSpeaker = nil - } - } - - self.mainStageNode.togglePin = { [weak self] in - if let strongSelf = self { - if let (peerId, videoEndpointId, _, _, _) = strongSelf.effectiveSpeaker { - if let _ = strongSelf.currentForcedSpeaker { - strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) - strongSelf.currentForcedSpeaker = nil - } else { - strongSelf.currentForcedSpeaker = (peerId, videoEndpointId) - } - } - strongSelf.updateMembers() - } - } - - self.mainStageNode.switchTo = { [weak self] peerId in - if let strongSelf = self, let interaction = strongSelf.itemInteraction { - interaction.switchToPeer(peerId, nil, false) - } - } - - self.mainStageNode.getAudioLevel = { [weak self] peerId in - return self?.itemInteraction?.getAudioLevel(peerId) ?? .single(0.0) - } - - self.mainStageNode.getVideo = { [weak self] endpointId, isMyPeer, completion in - if let strongSelf = self { - if isMyPeer { - if strongSelf.readyVideoEndpointIds.contains(endpointId) { - completion(strongSelf.itemInteraction?.getPeerVideo(endpointId, .mainstage)) - } else { - strongSelf.myPeerVideoReadyDisposable.set((strongSelf.readyVideoEndpointIdsPromise.get() - |> filter { $0.contains(endpointId) } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self { - completion(strongSelf.itemInteraction?.getPeerVideo(endpointId, .mainstage)) - } - })) - } - } else { - if let input = (strongSelf.call as! PresentationGroupCallImpl).video(endpointId: endpointId) { - if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) { - completion(GroupVideoNode(videoView: videoView, backdropVideoView: strongSelf.videoRenderingContext.makeView(input: input, blur: true))) - } - } - - /*strongSelf.call.makeIncomingVideoView(endpointId: endpointId, requestClone: GroupVideoNode.useBlurTransparency, completion: { videoView, backdropVideoView in - if let videoView = videoView { - completion(GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView)) - } else { - completion(nil) - } - })*/ - } - } - } - - self.applicationStateDisposable = (self.context.sharedContext.applicationBindings.applicationIsActive - |> deliverOnMainQueue).start(next: { [weak self] active in - guard let strongSelf = self else { - return - } - strongSelf.appIsActive = active - }) - - if self.context.sharedContext.immediateExperimentalUISettings.enableDebugDataDisplay { - self.statsDisposable = ((call as! PresentationGroupCallImpl).getStats() - |> deliverOnMainQueue - |> then(.complete() |> delay(1.0, queue: .mainQueue())) - |> restart).start(next: { [weak self] stats in - guard let strongSelf = self else { - return - } - for (endpointId, videoNode) in strongSelf.videoNodes { - if let incomingVideoStats = stats.incomingVideoStats[endpointId] { - videoNode.updateDebugInfo(text: "in: \(incomingVideoStats.receivingQuality)\n srv: \(incomingVideoStats.availableQuality)") - } - } - if let (_, maybeEndpointId, _, _, _) = strongSelf.mainStageNode.currentPeer, let endpointId = maybeEndpointId { - if let incomingVideoStats = stats.incomingVideoStats[endpointId] { - strongSelf.mainStageNode.currentVideoNode?.updateDebugInfo(text: "in: \(incomingVideoStats.receivingQuality)\n srv: \(incomingVideoStats.availableQuality)") - } - } - }) - } - } - - deinit { - self.presentationDataDisposable?.dispose() - self.peerViewDisposable?.dispose() - self.leaveDisposable.dispose() - self.isMutedDisposable?.dispose() - self.isNoiseSuppressionEnabledDisposable?.dispose() - self.callStateDisposable?.dispose() - self.audioOutputStateDisposable?.dispose() - self.memberStatesDisposable?.dispose() - self.audioLevelsDisposable?.dispose() - self.myAudioLevelDisposable?.dispose() - self.isSpeakingDisposable?.dispose() - self.inviteDisposable.dispose() - self.memberEventsDisposable.dispose() - self.reconnectedAsEventsDisposable.dispose() - self.stateVersionDisposable.dispose() - self.updateAvatarDisposable.dispose() - self.ignoreConnectingTimer?.invalidate() - self.readyVideoDisposables.dispose() - self.applicationStateDisposable?.dispose() - self.myPeerVideoReadyDisposable.dispose() - self.statsDisposable?.dispose() - } - - private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { - let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() - if let controller = self.controller { - let contextController = ContextController(account: self.context.account, 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) - controller.presentInGlobalOverlay(contextController) - } - } - - private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> { - guard let myPeerId = self.callState?.myPeerId else { - return .single([]) - } - - let canManageCall = self.callState?.canManageCall == true - let avatarSize = CGSize(width: 28.0, height: 28.0) - return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.inviteLinksPromise.get()) - |> take(1) - |> deliverOnMainQueue - |> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in - guard let strongSelf = self else { - return [] - } - - var items: [ContextMenuItem] = [] - - if peers.count > 1 { - for peer in peers { - if peer.peer.id == myPeerId { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { c, _ in - guard let strongSelf = self else { - return - } - c.setItems(strongSelf.contextMenuDisplayAsItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) - }))) - items.append(.separator) - break - } - } - } - - if let (availableOutputs, currentOutput) = strongSelf.audioOutputState, availableOutputs.count > 1 { - var currentOutputTitle = "" - for output in availableOutputs { - if output == currentOutput { - let title: String - switch output { - case .builtin: - title = UIDevice.current.model - case .speaker: - title = strongSelf.presentationData.strings.Call_AudioRouteSpeaker - case .headphones: - title = strongSelf.presentationData.strings.Call_AudioRouteHeadphones - case let .port(port): - title = port.name - } - currentOutputTitle = title - break - } - } - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ContextAudio, textLayout: .secondLineWithValue(currentOutputTitle), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Audio"), color: theme.actionSheet.primaryTextColor) - }, action: { c, _ in - guard let strongSelf = self else { - return - } - c.setItems(strongSelf.contextMenuAudioItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) - }))) - } - - if canManageCall { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_EditTitle - } else { - text = strongSelf.presentationData.strings.VoiceChat_EditTitle - } - items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.default) - - guard let strongSelf = self else { - return - } - strongSelf.openTitleEditing() - }))) - - var hasPermissions = true - if let chatPeer = chatPeer as? TelegramChannel { - if case .broadcast = chatPeer.info { - hasPermissions = false - } else if chatPeer.flags.contains(.isGigagroup) { - hasPermissions = false - } - } - if hasPermissions { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) - }, action: { c, _ in - guard let strongSelf = self else { - return - } - c.setItems(strongSelf.contextMenuPermissionItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) - }))) - } - } - - if let inviteLinks = inviteLinks { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.default) - - self?.presentShare(inviteLinks) - }))) - } - - let isScheduled = strongSelf.isScheduled - - let canSpeak: Bool - if let callState = strongSelf.callState { - if let muteState = callState.muteState { - canSpeak = muteState.canUnmute - } else { - canSpeak = true - } - } else { - canSpeak = false - } - - if !isScheduled && canSpeak { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - if let strongSelf = self { - strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled) - } - }))) - } - - if let callState = strongSelf.callState, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { - if #available(iOS 12.0, *) { - if strongSelf.call.hasScreencast { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StopScreenSharing, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.default) - - self?.call.disableScreencast() - }))) - } else { - items.append(.custom(VoiceChatShareScreenContextItem(context: strongSelf.context, text: strongSelf.presentationData.strings.VoiceChat_ShareScreen, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) - }, action: { _, _ in }), false)) - } - } - } - - if canManageCall { - if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp { - items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self else { - return - } - - let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: { - if let strongSelf = self { - strongSelf.call.setShouldBeRecording(false, title: nil, videoOrientation: nil) - - - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_RecordingSaved - } else { - text = strongSelf.presentationData.strings.VideoChat_RecordingSaved - } - - strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in - if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - let context = strongSelf.context - strongSelf.controller?.dismiss(completion: { - Queue.mainQueue().justDispatch { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) - }) - } - }) - - return true - } - return false - }) - } - })]) - self?.controller?.present(alertController, in: .window(.root)) - }), false)) - } else { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_StartRecording - } else { - text = strongSelf.presentationData.strings.VoiceChat_StartRecording - } - if strongSelf.callState?.scheduleTimestamp == nil { - items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in - return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self, let peer = strongSelf.peer else { - return - } - - let controller = VoiceChatRecordingSetupController(context: strongSelf.context, peer: peer, completion: { [weak self] videoOrientation in - if let strongSelf = self { - let title: String - let text: String - let placeholder: String - if let _ = videoOrientation { - placeholder = strongSelf.presentationData.strings.VoiceChat_RecordingTitlePlaceholderVideo - } else { - placeholder = strongSelf.presentationData.strings.VoiceChat_RecordingTitlePlaceholder - } - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - title = strongSelf.presentationData.strings.LiveStream_StartRecordingTitle - if let _ = videoOrientation { - text = strongSelf.presentationData.strings.LiveStream_StartRecordingTextVideo - } else { - text = strongSelf.presentationData.strings.LiveStream_StartRecordingText - } - } else { - title = strongSelf.presentationData.strings.VoiceChat_StartRecordingTitle - if let _ = videoOrientation { - text = strongSelf.presentationData.strings.VoiceChat_StartRecordingTextVideo - } else { - text = strongSelf.presentationData.strings.VoiceChat_StartRecordingText - } - } - - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { title in - if let strongSelf = self, let title = title { - strongSelf.call.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation) - - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_RecordingStarted - } else { - text = strongSelf.presentationData.strings.VoiceChat_RecordingStarted - } - - strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false }) - strongSelf.call.playTone(.recordingStarted) - } - }) - strongSelf.controller?.present(controller, in: .window(.root)) - } - }) - self?.controller?.present(controller, in: .window(.root)) - }))) - } - } - } - - if canManageCall { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelLiveStream : strongSelf.presentationData.strings.VoiceChat_EndLiveStream - } else { - text = isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelVoiceChat : strongSelf.presentationData.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)) - }))) - } else { - let leaveText: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - leaveText = strongSelf.presentationData.strings.LiveStream_LeaveVoiceChat - } else { - leaveText = strongSelf.presentationData.strings.VoiceChat_LeaveVoiceChat - } - items.append(.action(ContextMenuActionItem(text: leaveText, 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 _ = (strongSelf.call.leave(terminateIfPossible: false) - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(completed: { - self?.controller?.dismiss() - }) - }))) - } - return items - } - } - - private func contextMenuAudioItems() -> Signal<[ContextMenuItem], NoError> { - guard let (availableOutputs, currentOutput) = self.audioOutputState else { - return .single([]) - } - - var items: [ContextMenuItem] = [] - for output in availableOutputs { - let title: String - switch output { - case .builtin: - title = UIDevice.current.model - case .speaker: - title = self.presentationData.strings.Call_AudioRouteSpeaker - case .headphones: - title = self.presentationData.strings.Call_AudioRouteHeadphones - case let .port(port): - title = port.name - } - items.append(.action(ContextMenuActionItem(text: title, icon: { theme in - if output == currentOutput { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) - } else { - return nil - } - }, action: { [weak self] _, f in - f(.default) - self?.call.setCurrentAudioOutput(output) - }))) - } - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] (c, _) in - guard let strongSelf = self else { - return - } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) - }))) - return .single(items) - } - - private func contextMenuDisplayAsItems() -> Signal<[ContextMenuItem], NoError> { - guard let myPeerId = self.callState?.myPeerId else { - return .single([]) - } - - let avatarSize = CGSize(width: 28.0, height: 28.0) - let darkTheme = self.darkTheme - - return self.displayAsPeersPromise.get() - |> take(1) - |> map { [weak self] peers -> [ContextMenuItem] in - guard let strongSelf = self else { - return [] - } - - var items: [ContextMenuItem] = [] - - var isGroup = false - for peer in peers { - if peer.peer is TelegramGroup { - isGroup = true - break - } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { - isGroup = true - break - } - } - - items.append(.custom(VoiceChatInfoContextItem(text: isGroup ? strongSelf.presentationData.strings.VoiceChat_DisplayAsInfoGroup : strongSelf.presentationData.strings.VoiceChat_DisplayAsInfo, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor) - }), true)) - - for peer in peers { - var subtitle: String? - if peer.peer.id.namespace == Namespaces.Peer.CloudUser { - subtitle = strongSelf.presentationData.strings.VoiceChat_PersonalAccount - } else if let subscribers = peer.subscribers { - if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { - subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) - } else { - subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers) - } - } - - let isSelected = peer.peer.id == myPeerId - let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) - let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: EnginePeer(peer.peer), size: avatarSize) - |> map { image -> UIImage? in - if isSelected, let image = image { - return generateImage(extendedAvatarSize, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height)) - - let lineWidth = 1.0 + UIScreenPixel - context.setLineWidth(lineWidth) - context.setStrokeColor(darkTheme.actionSheet.controlAccentColor.cgColor) - context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) - }) - } else { - return image - } - } - - items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { _, f in - f(.default) - - guard let strongSelf = self else { - return - } - - if peer.peer.id != myPeerId { - strongSelf.call.reconnect(as: peer.peer.id) - } - }))) - - if peer.peer.id.namespace == Namespaces.Peer.CloudUser { - items.append(.separator) - } - } - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { (c, _) in - guard let strongSelf = self else { - return - } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) - }))) - return items - } - } - - private func contextMenuPermissionItems() -> Signal<[ContextMenuItem], NoError> { - var items: [ContextMenuItem] = [] - if let callState = self.callState, callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState { - let isMuted = defaultParticipantMuteState == .muted - - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in - if isMuted { - return nil - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) - } - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self else { - return - } - strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: false) - }))) - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in - if !isMuted { - return nil - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) - } - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self else { - return - } - strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: true) - }))) - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] (c, _) in - guard let strongSelf = self else { - return - } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) - }))) - } - return .single(items) - } - - override func didLoad() { - super.didLoad() - - self.view.disablesInteractiveTransitionGestureRecognizer = true - self.view.disablesInteractiveModalDismiss = true - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - - let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:))) - longTapRecognizer.minimumPressDuration = 0.001 - longTapRecognizer.delegate = self - self.actionButton.view.addGestureRecognizer(longTapRecognizer) - - let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - panRecognizer.delegate = self - panRecognizer.delaysTouchesBegan = false - panRecognizer.cancelsTouchesInView = true - self.view.addGestureRecognizer(panRecognizer) - - if self.isScheduling { - self.setupSchedulePickerView() - self.updateScheduleButtonTitle() - } - } - - private func updateSchedulePickerLimits() { - let timeZone = TimeZone(secondsFromGMT: 0)! - var calendar = Calendar(identifier: .gregorian) - calendar.timeZone = timeZone - let currentDate = Date() - var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate) - components.second = 0 - - let roundedDate = calendar.date(from: components)! - let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: roundedDate) - - let minute = components.minute ?? 0 - components.minute = 0 - let roundedToHourDate = calendar.date(from: components)! - components.hour = 0 - - let roundedToMidnightDate = calendar.date(from: components)! - let nextTwoHourDate = calendar.date(byAdding: .hour, value: minute > 30 ? 4 : 3, to: roundedToHourDate) - let maxDate = calendar.date(byAdding: .day, value: 8, to: roundedToMidnightDate) - - if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) { - self.pickerView?.maximumDate = date - } - if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate { - self.pickerView?.minimumDate = next1MinDate - self.pickerView?.maximumDate = maxDate - self.pickerView?.date = nextTwoHourDate - } - } - - private func setupSchedulePickerView() { - var currentDate: Date? - if let pickerView = self.pickerView { - currentDate = pickerView.date - pickerView.removeFromSuperview() - } - - let textColor = UIColor.white - UILabel.setDateLabel(textColor) - - let pickerView = UIDatePicker() - pickerView.timeZone = TimeZone(secondsFromGMT: 0) - pickerView.datePickerMode = .countDownTimer - pickerView.datePickerMode = .dateAndTime - pickerView.locale = Locale.current - pickerView.timeZone = TimeZone.current - pickerView.minuteInterval = 1 - self.contentContainer.view.addSubview(pickerView) - pickerView.addTarget(self, action: #selector(self.scheduleDatePickerUpdated), for: .valueChanged) - if #available(iOS 13.4, *) { - pickerView.preferredDatePickerStyle = .wheels - } - pickerView.setValue(textColor, forKey: "textColor") - self.pickerView = pickerView - - self.updateSchedulePickerLimits() - if let currentDate = currentDate { - pickerView.date = currentDate - } - } - - private let calendar = Calendar(identifier: .gregorian) - private func updateScheduleButtonTitle() { - guard let date = self.pickerView?.date else { - return - } - - let calendar = Calendar(identifier: .gregorian) - let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - let timestamp = Int32(date.timeIntervalSince1970) - let time = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: self.presentationData.dateTimeFormat) - let buttonTitle: String - if calendar.isDateInToday(date) { - buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).string - } else if calendar.isDateInTomorrow(date) { - buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleTomorrow(time).string - } else { - buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).string - } - self.scheduleButtonTitle = buttonTitle - - let delta = timestamp - currentTimestamp - - var isGroup = true - if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info { - isGroup = false - } - let intervalString = scheduledTimeIntervalString(strings: self.presentationData.strings, value: max(60, delta)) - self.scheduleTextNode.attributedText = NSAttributedString(string: isGroup ? self.presentationData.strings.ScheduleVoiceChat_GroupText(intervalString).string : self.presentationData.strings.ScheduleLiveStream_ChannelText(intervalString).string, font: Font.regular(14.0), textColor: UIColor(rgb: 0x8e8e93)) - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) - } - } - - @objc private func scheduleDatePickerUpdated() { - self.updateScheduleButtonTitle() - } - - private func schedule() { - if let date = self.pickerView?.date, date > Date() { - self.call.schedule(timestamp: Int32(date.timeIntervalSince1970)) - - self.isScheduling = false - self.transitionToScheduled() - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) - } - } - } - - private func dismissScheduled() { - self.leaveDisposable.set((self.call.leave(terminateIfPossible: true) - |> deliverOnMainQueue).start(completed: { [weak self] in - self?.controller?.dismiss(closing: true) - })) - } - - private func transitionToScheduled() { - let springDuration: Double = 0.6 - let springDamping: CGFloat = 100.0 - - self.optionsButton.alpha = 1.0 - self.optionsButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.optionsButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping) - self.optionsButton.isUserInteractionEnabled = true - - self.closeButton.alpha = 1.0 - self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.closeButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping) - self.closeButton.isUserInteractionEnabled = true - - self.audioButton.alpha = 1.0 - self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping) - self.audioButton.isUserInteractionEnabled = true - - self.leaveButton.alpha = 1.0 - self.leaveButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.leaveButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping) - self.leaveButton.isUserInteractionEnabled = true - - self.scheduleCancelButton.alpha = 0.0 - self.scheduleCancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) - self.scheduleCancelButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 26.0), duration: 0.2, removeOnCompletion: false, additive: true) - - self.actionButton.titleLabel.layer.animatePosition(from: CGPoint(x: 0.0, y: -26.0), to: CGPoint(), duration: 0.2, additive: true) - - if let pickerView = self.pickerView { - self.pickerView = nil - pickerView.alpha = 0.0 - pickerView.layer.animateScale(from: 1.0, to: 0.25, duration: 0.15, removeOnCompletion: false) - pickerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak pickerView] _ in - pickerView?.removeFromSuperview() - }) - pickerView.isUserInteractionEnabled = false - } - - self.timerNode.isHidden = false - self.timerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - self.timerNode.animateIn() - - self.scheduleTextNode.alpha = 0.0 - self.scheduleTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) - - self.updateTitle(slide: true, transition: .animated(duration: 0.2, curve: .easeInOut)) - } - - private func transitionToCall() { - self.updateDecorationsColors() - - self.listNode.alpha = 1.0 - self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.listNode.isUserInteractionEnabled = true - - self.timerNode.alpha = 0.0 - self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in - self?.timerNode.isHidden = true - }) - - if self.audioButton.isHidden { - self.audioButton.isHidden = false - self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0) - } - - self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut)) - } - - @objc private func optionsPressed() { - self.optionsButton.play() - self.optionsButton.contextAction?(self.optionsButton.containerNode, nil) - } - - @objc private func closePressed() { - self.controller?.dismiss(closing: false) - self.controller?.dismissAllTooltips() - } - - @objc private func panelPressed() { - guard let (layout, navigationHeight) = self.validLayout, !self.animatingExpansion && !self.animatingMainStage && !self.mainStageNode.animating else { - return - } - self.panelHidden = !self.panelHidden - - self.animatingExpansion = true - let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) - self.updateDecorationsLayout(transition: transition) - } - - @objc private func leavePressed() { - self.hapticFeedback.impact(.light) - self.controller?.dismissAllTooltips() - - if let callState = self.callState, callState.canManageCall { - let action: () -> Void = { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.leaveDisposable.set((strongSelf.call.leave(terminateIfPossible: true) - |> deliverOnMainQueue).start(completed: { - self?.controller?.dismiss() - })) - } - - let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: self.darkTheme)) - var items: [ActionSheetItem] = [] - - let leaveTitle: String - let leaveAndCancelTitle: String - - if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { - leaveTitle = self.presentationData.strings.LiveStream_LeaveConfirmation - leaveAndCancelTitle = self.isScheduled ? self.presentationData.strings.LiveStream_LeaveAndCancelVoiceChat : self.presentationData.strings.LiveStream_LeaveAndEndVoiceChat - } else { - leaveTitle = self.presentationData.strings.VoiceChat_LeaveConfirmation - leaveAndCancelTitle = self.isScheduled ? self.presentationData.strings.VoiceChat_LeaveAndCancelVoiceChat : self.presentationData.strings.VoiceChat_LeaveAndEndVoiceChat - } - - items.append(ActionSheetTextItem(title: leaveTitle)) - items.append(ActionSheetButtonItem(title: leaveAndCancelTitle, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - if let strongSelf = self { - let title: String - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - title = strongSelf.isScheduled ? strongSelf.presentationData.strings.LiveStream_CancelConfirmationTitle : strongSelf.presentationData.strings.LiveStream_EndConfirmationTitle - text = strongSelf.isScheduled ? strongSelf.presentationData.strings.LiveStream_CancelConfirmationText : strongSelf.presentationData.strings.LiveStream_EndConfirmationText - } else { - title = strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationTitle : strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle - text = strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationText : strongSelf.presentationData.strings.VoiceChat_EndConfirmationText - } - - if let (members, _) = strongSelf.currentCallMembers, members.count >= 10 || true { - 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: strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationEnd : strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: { - action() - })]) - strongSelf.controller?.present(alertController, in: .window(.root)) - } else { - action() - } - } - })) - - let leaveText: String - if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { - leaveText = self.presentationData.strings.LiveStream_LeaveVoiceChat - } else { - leaveText = self.presentationData.strings.VoiceChat_LeaveVoiceChat - } - - items.append(ActionSheetButtonItem(title: leaveText, color: .accent, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { - return - } - - strongSelf.leaveDisposable.set((strongSelf.call.leave(terminateIfPossible: false) - |> deliverOnMainQueue).start(completed: { [weak self] in - self?.controller?.dismiss(closing: true) - })) - })) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.controller?.present(actionSheet, in: .window(.root)) - } else { - self.leaveDisposable.set((self.call.leave(terminateIfPossible: false) - |> deliverOnMainQueue).start(completed: { [weak self] in - self?.controller?.dismiss(closing: true) - })) - } - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if self.isScheduling { - self.dismissScheduled() - } else { - self.controller?.dismiss(closing: false) - self.controller?.dismissAllTooltips() - } - } - } - - private func presentUndoOverlay(content: UndoOverlayContent, action: @escaping (UndoOverlayAction) -> Bool) { - var animateInAsReplacement = false - self.controller?.forEachController { c in - if let c = c as? UndoOverlayController { - animateInAsReplacement = true - c.dismiss() - } - return true - } - self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: action), in: .current) - } - - private func presentShare(_ inviteLinks: GroupCallInviteLinks) { - let formatSendTitle: (String) -> String = { string in - var string = string - if string.contains("[") && string.contains("]") { - if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") { - string.removeSubrange(startIndex ... endIndex) - } - } else { - string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,.")) - } - return string - } - - let _ = (self.context.account.postbox.loadedPeerWithId(self.call.peerId) - |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self { - var inviteLinks = inviteLinks - - if let peer = peer as? TelegramChannel, case .group = peer.info, !peer.flags.contains(.isGigagroup), !(peer.addressName ?? "").isEmpty, let callState = strongSelf.callState, let defaultParticipantMuteState = callState.defaultParticipantMuteState { - let isMuted = defaultParticipantMuteState == .muted - - if !isMuted { - inviteLinks = GroupCallInviteLinks(listenerLink: inviteLinks.listenerLink, speakerLink: nil) - } - } - - let presentationData = strongSelf.presentationData - - var segmentedValues: [ShareControllerSegmentedValue]? - if let speakerLink = inviteLinks.speakerLink { - segmentedValues = [ShareControllerSegmentedValue(title: presentationData.strings.VoiceChat_InviteLink_Speaker, subject: .url(speakerLink), actionTitle: presentationData.strings.VoiceChat_InviteLink_CopySpeakerLink, formatSendTitle: { count in - return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteSpeakers(Int32(count))) - }), ShareControllerSegmentedValue(title: presentationData.strings.VoiceChat_InviteLink_Listener, subject: .url(inviteLinks.listenerLink), actionTitle: presentationData.strings.VoiceChat_InviteLink_CopyListenerLink, formatSendTitle: { count in - return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count))) - })] - } - let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: strongSelf.darkTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) - shareController.completed = { [weak self] peerIds in - if let strongSelf = self { - let _ = (strongSelf.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).start(next: { [weak self] peerList in - if let strongSelf = self { - let peers = peerList.compactMap { $0 } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - var isSavedMessages = false - if peers.count == 1, let peer = peers.first { - isSavedMessages = peer.id == strongSelf.context.account.peerId - let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.VoiceChat_ForwardTooltip_Chat(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.VoiceChat_ForwardTooltip_TwoChats(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.VoiceChat_ForwardTooltip_ManyChats(peerName, "\(peers.count - 1)").string - } else { - text = "" - } - - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) - } - }) - } - } - shareController.actionCompleted = { [weak self] in - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - } - strongSelf.controller?.present(shareController, in: .window(.root)) - } - }) - } - - private var actionButtonPressTimer: SwiftSignalKit.Timer? - private var actionButtonPressedTimestamp: Double? - private func startActionButtonPressTimer() { - self.actionButtonPressTimer?.invalidate() - let pressTimer = SwiftSignalKit.Timer(timeout: 0.185, repeat: false, completion: { [weak self] in - self?.actionButtonPressedTimestamp = CACurrentMediaTime() - self?.actionButtonPressTimerFired() - self?.actionButtonPressTimer = nil - }, queue: Queue.mainQueue()) - self.actionButtonPressTimer = pressTimer - pressTimer.start() - } - - private func stopActionButtonPressTimer() { - self.actionButtonPressTimer?.invalidate() - self.actionButtonPressTimer = nil - } - - private func actionButtonPressTimerFired() { - guard let callState = self.callState else { - return - } - if callState.muteState != nil { - self.pushingToTalk = true - self.call.setIsMuted(action: .muted(isPushToTalkActive: true)) - } - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) - } - - self.updateMembers() - } - - @objc private func actionButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) { - guard let callState = self.callState else { - return - } - if case .connecting = callState.networkState, callState.scheduleTimestamp == nil && !self.isScheduling { - return - } - if callState.scheduleTimestamp != nil || self.isScheduling { - switch gestureRecognizer.state { - case .began: - self.actionButton.pressing = true - self.hapticFeedback.impact(.light) - case .ended, .cancelled: - self.actionButton.pressing = false - - let location = gestureRecognizer.location(in: self.actionButton.view) - if self.actionButton.hitTest(location, with: nil) != nil { - if self.isScheduling { - self.schedule() - } else if callState.canManageCall { - self.call.startScheduled() - } else { - if !callState.subscribedToScheduled { - let location = self.actionButton.view.convert(self.actionButton.bounds, to: self.view).center - let point = CGRect(origin: CGPoint(x: location.x - 5.0, y: location.y - 5.0 - 68.0), size: CGSize(width: 10.0, height: 10.0)) - self.controller?.present(TooltipScreen(account: self.context.account, text: self.presentationData.strings.VoiceChat_ReminderNotify, style: .gradient(UIColor(rgb: 0x262c5a), UIColor(rgb: 0x5d2835)), icon: nil, location: .point(point, .bottom), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in - return .dismiss(consume: false) - }), in: .window(.root)) - } - self.call.toggleScheduledSubscription(!callState.subscribedToScheduled) - } - } - default: - break - } - return - } - if let muteState = callState.muteState { - if !muteState.canUnmute { - switch gestureRecognizer.state { - case .began: - self.actionButton.pressing = true - self.hapticFeedback.impact(.light) - case .ended, .cancelled: - self.actionButton.pressing = false - - let location = gestureRecognizer.location(in: self.actionButton.view) - if self.actionButton.hitTest(location, with: nil) != nil { - self.call.raiseHand() - self.actionButton.playAnimation() - } - default: - break - } - return - } - } - switch gestureRecognizer.state { - case .began: - self.actionButton.pressing = true - self.hapticFeedback.impact(.light) - self.actionButtonPressedTimestamp = nil - self.startActionButtonPressTimer() - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) - } - case .ended, .cancelled: - if self.actionButtonPressTimer != nil { - self.pushingToTalk = false - self.actionButton.pressing = false - - self.stopActionButtonPressTimer() - self.call.toggleIsMuted() - } else { - self.hapticFeedback.impact(.light) - if self.pushingToTalk, let timestamp = self.actionButtonPressedTimestamp, CACurrentMediaTime() < timestamp + 0.5 { - self.pushingToTalk = false - self.temporaryPushingToTalk = true - self.call.setIsMuted(action: .unmuted) - - Queue.mainQueue().after(0.1) { - self.temporaryPushingToTalk = false - self.actionButton.pressing = false - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) - } - } - } else { - self.pushingToTalk = false - self.actionButton.pressing = false - - self.call.setIsMuted(action: .muted(isPushToTalkActive: false)) - } - } - - if let callState = self.callState { - self.itemInteraction?.updateAudioLevels([(callState.myPeerId, 0, 0.0, false)], reset: true) - } - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) - } - self.updateMembers() - default: - break - } - } - - @objc private func actionPressed() { - if self.isScheduling { - self.schedule() - } - } - - @objc private func audioPressed() { - self.hapticFeedback.impact(.light) - - if let _ = self.callState?.scheduleTimestamp { - if let callState = self.callState, let peer = self.peer, !callState.canManageCall && (peer.addressName?.isEmpty ?? true) { - return - } - - let _ = (self.inviteLinksPromise.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] inviteLinks in - guard let strongSelf = self else { - return - } - - let _ = (strongSelf.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.call.peerId), - TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: strongSelf.call.peerId) - ) - |> map { peer, exportedInvitation -> GroupCallInviteLinks? in - if let inviteLinks = inviteLinks { - return inviteLinks - } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { - return GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil) - } else if let link = exportedInvitation?.link { - return GroupCallInviteLinks(listenerLink: link, speakerLink: nil) - } - return nil - } - |> deliverOnMainQueue).start(next: { links in - guard let strongSelf = self else { - return - } - - if let links = links { - strongSelf.presentShare(links) - } - }) - }) - return - } - - guard let (availableOutputs, currentOutput) = self.audioOutputState else { - return - } - guard availableOutputs.count >= 2 else { - return - } - - if availableOutputs.count == 2 { - for output in availableOutputs { - if output != currentOutput { - self.call.setCurrentAudioOutput(output) - break - } - } - } else { - let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: self.darkTheme)) - var items: [ActionSheetItem] = [] - for output in availableOutputs { - let title: String - var icon: UIImage? - switch output { - case .builtin: - title = UIDevice.current.model - case .speaker: - title = self.presentationData.strings.Call_AudioRouteSpeaker - icon = generateScaledImage(image: UIImage(bundleImageName: "Call/CallSpeakerButton"), size: CGSize(width: 48.0, height: 48.0), opaque: false) - case .headphones: - title = self.presentationData.strings.Call_AudioRouteHeadphones - case let .port(port): - title = port.name - if port.type == .bluetooth { - var image = UIImage(bundleImageName: "Call/CallBluetoothButton") - let portName = port.name.lowercased() - if portName.contains("airpods max") { - image = UIImage(bundleImageName: "Call/CallAirpodsMaxButton") - } else if portName.contains("airpods pro") { - image = UIImage(bundleImageName: "Call/CallAirpodsProButton") - } else if portName.contains("airpods") { - image = UIImage(bundleImageName: "Call/CallAirpodsButton") - } - icon = generateScaledImage(image: image, size: CGSize(width: 48.0, height: 48.0), opaque: false) - } - } - items.append(CallRouteActionSheetItem(title: title, icon: icon, selected: output == currentOutput, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - self?.call.setCurrentAudioOutput(output) - })) - } - - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Call_AudioRouteHide, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.controller?.present(actionSheet, in: .window(.calls)) - } - } - - @objc private func cameraPressed() { - self.hapticFeedback.impact(.light) - if self.call.hasVideo { - self.call.disableVideo() - - if let (layout, navigationHeight) = self.validLayout { - self.animatingButtonsSwap = true - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - } - } else { - DeviceAccess.authorizeAccess(to: .camera(.videoCall), onlyCheck: true, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), present: { [weak self] c, a in - self?.controller?.present(c, in: .window(.root), with: a) - }, openSettings: { [weak self] in - self?.context.sharedContext.applicationBindings.openSettings() - }, _: { [weak self] ready in - guard let strongSelf = self, ready else { - return - } - var isFrontCamera = true - let videoCapturer = OngoingCallVideoCapturer() - let input = videoCapturer.video() - if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) { - videoView.updateIsEnabled(true) - - let cameraNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil) - let controller = VoiceChatCameraPreviewController(sharedContext: strongSelf.context.sharedContext, cameraNode: cameraNode, shareCamera: { [weak self] _, unmuted in - if let strongSelf = self { - strongSelf.call.setIsMuted(action: unmuted ? .unmuted : .muted(isPushToTalkActive: false)) - (strongSelf.call as! PresentationGroupCallImpl).requestVideo(capturer: videoCapturer, useFrontCamera: isFrontCamera) - - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.animatingButtonsSwap = true - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - } - } - }, switchCamera: { - Queue.mainQueue().after(0.1) { - isFrontCamera = !isFrontCamera - videoCapturer.switchVideoInput(isFront: isFrontCamera) - } - }) - strongSelf.controller?.present(controller, in: .window(.root)) - } - }) - } - } - - @objc private func switchCameraPressed() { - self.hapticFeedback.impact(.light) - Queue.mainQueue().after(0.1) { - self.call.switchVideoCamera() - } - - if let callState = self.callState { - for entry in self.currentFullscreenEntries { - if case let .peer(peerEntry, _) = entry { - if peerEntry.peer.id == callState.myPeerId { - if let videoEndpointId = peerEntry.videoEndpointId, let videoNode = self.videoNodes[videoEndpointId] { - videoNode.flip(withBackground: false) - } - break - } - } - } - } - self.mainStageNode.flipVideoIfNeeded() - - let springDuration: Double = 0.7 - let springDamping: CGFloat = 100.0 - self.switchCameraButton.isUserInteractionEnabled = false - self.switchCameraButton.layer.animateSpring(from: 0.0 as NSNumber, to: CGFloat.pi as NSNumber, keyPath: "transform.rotation.z", duration: springDuration, damping: springDamping, completion: { [weak self] _ in - self?.switchCameraButton.isUserInteractionEnabled = true - }) - } - - private var isLandscape: Bool { - if let (layout, _) = self.validLayout, layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { - return true - } else { - return false - } - } - - private var effectiveBottomAreaHeight: CGFloat { - if let (layout, _) = self.validLayout, case .regular = layout.metrics.widthClass { - return bottomAreaHeight - } - switch self.displayMode { - case .modal: - return bottomAreaHeight - case let .fullscreen(controlsHidden): - return controlsHidden ? 0.0 : fullscreenBottomAreaHeight - } - } - - private var isFullscreen: Bool { - switch self.displayMode { - case .fullscreen(_), .modal(_, true): - return true - default: - return false - } - } - - private func updateDecorationsLayout(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { - guard let (layout, _) = self.validLayout else { - return - } - - let isLandscape = self.isLandscape - - let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) - let listTopInset = isLandscape ? topPanelHeight : layoutTopInset + topPanelHeight - let bottomPanelHeight = isLandscape ? layout.intrinsicInsets.bottom : bottomAreaHeight + layout.intrinsicInsets.bottom - - let size = layout.size - let contentWidth: CGFloat - var contentLeftInset: CGFloat = 0.0 - var forceUpdate = false - if case .regular = layout.metrics.widthClass { - contentWidth = max(320.0, min(375.0, floor(size.width * 0.3))) - if self.peerIdToEndpointId.isEmpty { - contentLeftInset = 0.0 - } else { - contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth - } - forceUpdate = true - } else { - contentWidth = isLandscape ? min(530.0, size.width - 210.0) : size.width - } - - let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - bottomPanelHeight + bottomGradientHeight) - let topInset: CGFloat - if let (panInitialTopInset, panOffset) = self.panGestureArguments { - if self.isExpanded { - topInset = min(self.topInset ?? listSize.height, panInitialTopInset + max(0.0, panOffset)) - } else { - topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) - } - } else if case .regular = layout.metrics.widthClass { - topInset = 0.0 - } else if let currentTopInset = self.topInset { - topInset = self.isExpanded ? 0.0 : currentTopInset - } else { - topInset = listSize.height - 46.0 - floor(56.0 * 3.5) - } - - var bottomEdge: CGFloat = 0.0 - if case .regular = layout.metrics.widthClass { - bottomEdge = size.height - } else { - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ListViewItemNode { - let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.contentContainer.view) - if convertedFrame.maxY > bottomEdge { - bottomEdge = convertedFrame.maxY - } - } - } - if bottomEdge.isZero { - bottomEdge = self.listNode.frame.minY + 46.0 + 56.0 - } - } - - let rawPanelOffset = topInset + listTopInset - topPanelHeight - let panelOffset = max(layoutTopInset, rawPanelOffset) - let topPanelFrame: CGRect - if isLandscape { - topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: topPanelHeight)) - } else { - topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: size.width, height: topPanelHeight)) - } - - let sideInset: CGFloat = 14.0 - - let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom - var bottomGradientFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: size.width, height: bottomGradientHeight)) - if isLandscape { - bottomGradientFrame.origin.y = layout.size.height - } - - let transitionContainerFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - transition.updateFrame(node: self.transitionContainerNode, frame: transitionContainerFrame) - transition.updateFrame(view: self.transitionMaskView, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: transitionContainerFrame.height)) - let updateMaskLayers = { - var topPanelFrame = topPanelFrame - if self.animatingContextMenu { - topPanelFrame.origin.y = 0.0 - } - transition.updateFrame(layer: self.transitionMaskTopFillLayer, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: topPanelFrame.maxY)) - transition.updateFrame(layer: self.transitionMaskFillLayer, frame: CGRect(x: 0.0, y: topPanelFrame.maxY, width: transitionContainerFrame.width, height: bottomGradientFrame.minY - topPanelFrame.maxY)) - transition.updateFrame(layer: self.transitionMaskGradientLayer, frame: CGRect(x: 0.0, y: bottomGradientFrame.minY, width: transitionContainerFrame.width, height: bottomGradientFrame.height)) - transition.updateFrame(layer: self.transitionMaskBottomFillLayer, frame: CGRect(x: 0.0, y: bottomGradientFrame.minY, width: transitionContainerFrame.width, height: max(0.0, transitionContainerFrame.height - bottomGradientFrame.minY))) - } - if transition.isAnimated { - updateMaskLayers() - } else { - CATransaction.begin() - CATransaction.setDisableActions(true) - updateMaskLayers() - CATransaction.commit() - } - - var bottomInset: CGFloat = 0.0 - if case .compact = layout.metrics.widthClass, case let .fullscreen(controlsHidden) = self.displayMode { - if !controlsHidden { - bottomInset = 80.0 - } - } - transition.updateAlpha(node: self.bottomGradientNode, alpha: self.isLandscape ? 0.0 : 1.0) - - var isTablet = false - let videoFrame: CGRect - let videoContainerFrame: CGRect - if case .regular = layout.metrics.widthClass { - isTablet = true - let videoTopEdgeY = topPanelFrame.maxY - let videoBottomEdgeY = layout.size.height - layout.intrinsicInsets.bottom - videoFrame = CGRect(x: sideInset, y: 0.0, width: contentLeftInset - sideInset, height: videoBottomEdgeY - videoTopEdgeY) - videoContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: videoTopEdgeY), size: CGSize(width: contentLeftInset, height: layout.size.height)) - } else { - let videoTopEdgeY = isLandscape ? 0.0 : layoutTopInset - let videoBottomEdgeY = self.isLandscape ? layout.size.height : layout.size.height - layout.intrinsicInsets.bottom - 92.0 - videoFrame = CGRect(x: 0.0, y: videoTopEdgeY, width: isLandscape ? max(0.0, layout.size.width - layout.safeInsets.right - 92.0) : layout.size.width, height: videoBottomEdgeY - videoTopEdgeY) - videoContainerFrame = CGRect(origin: CGPoint(), size: layout.size) - } - transition.updateFrame(node: self.mainStageContainerNode, frame: videoContainerFrame) - transition.updateFrame(node: self.mainStageBackgroundNode, frame: videoFrame) - if !self.mainStageNode.animating { - transition.updateFrame(node: self.mainStageNode, frame: videoFrame) - } - self.mainStageNode.update(size: videoFrame.size, sideInset: layout.safeInsets.left, bottomInset: self.isLandscape ? 0.0 : bottomInset, isLandscape: videoFrame.width > videoFrame.height, isTablet: isTablet, transition: transition) - - let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: size.width, height: layout.size.height)) - - let leftBorderFrame: CGRect - let rightBorderFrame: CGRect - let additionalInset: CGFloat = 60.0 - let additionalSideInset = (size.width - contentWidth) / 2.0 - let additionalLeftInset = size.width / 2.0 - if isLandscape { - leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY - additionalInset), size: CGSize(width: (size.width - contentWidth) / 2.0 + sideInset, height: layout.size.height)) - rightBorderFrame = CGRect(origin: CGPoint(x: size.width - (size.width - contentWidth) / 2.0 - sideInset, y: topPanelFrame.maxY - additionalInset), size: CGSize(width: layout.safeInsets.right + (size.width - contentWidth) / 2.0 + sideInset, height: layout.size.height)) - } else { - var isFullscreen = false - if case .fullscreen = self.displayMode { - isFullscreen = true - forceUpdate = true - } - leftBorderFrame = CGRect(origin: CGPoint(x: -additionalInset - additionalLeftInset, y: topPanelFrame.maxY - additionalInset * (isFullscreen ? 0.95 : 0.8)), size: CGSize(width: sideInset + additionalInset + additionalLeftInset + (contentLeftInset.isZero ? additionalSideInset : contentLeftInset), height: layout.size.height)) - rightBorderFrame = CGRect(origin: CGPoint(x: size.width - sideInset - (contentLeftInset.isZero ? additionalSideInset : 0.0), y: topPanelFrame.maxY - additionalInset * (isFullscreen ? 0.95 : 0.8)), size: CGSize(width: sideInset + additionalInset + additionalLeftInset + additionalSideInset, height: layout.size.height)) - } - - let topCornersFrame = CGRect(x: sideInset + (contentLeftInset.isZero ? floorToScreenPixels((size.width - contentWidth) / 2.0) : contentLeftInset), y: topPanelFrame.maxY - 60.0, width: contentWidth - sideInset * 2.0, height: 50.0 + 60.0) - - let previousTopPanelFrame = self.topPanelNode.frame - let previousBackgroundFrame = self.backgroundNode.frame - let previousLeftBorderFrame = self.leftBorderNode.frame - let previousRightBorderFrame = self.rightBorderNode.frame - - if !topPanelFrame.equalTo(previousTopPanelFrame) || forceUpdate { - if topPanelFrame.width != previousTopPanelFrame.width { - transition.updateFrame(node: self.topPanelNode, frame: topPanelFrame) - transition.updateFrame(node: self.topCornersNode, frame: topCornersFrame) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.leftBorderNode, frame: leftBorderFrame) - transition.updateFrame(node: self.rightBorderNode, frame: rightBorderFrame) - } else { - self.topPanelNode.frame = topPanelFrame - let positionDelta = CGPoint(x: 0.0, y: topPanelFrame.minY - previousTopPanelFrame.minY) - transition.animateOffsetAdditive(layer: self.topPanelNode.layer, offset: positionDelta.y, completion: completion) - - transition.updateFrame(node: self.topCornersNode, frame: topCornersFrame) - - self.backgroundNode.frame = backgroundFrame - let backgroundPositionDelta = CGPoint(x: 0.0, y: previousBackgroundFrame.minY - backgroundFrame.minY) - transition.animatePositionAdditive(node: self.backgroundNode, offset: backgroundPositionDelta) - - self.leftBorderNode.frame = leftBorderFrame - let leftBorderPositionDelta = CGPoint(x: previousLeftBorderFrame.maxX - leftBorderFrame.maxX, y: previousLeftBorderFrame.minY - leftBorderFrame.minY) - transition.animatePositionAdditive(node: self.leftBorderNode, offset: leftBorderPositionDelta) - - self.rightBorderNode.frame = rightBorderFrame - let rightBorderPositionDelta = CGPoint(x: previousRightBorderFrame.minX - rightBorderFrame.minX, y: previousRightBorderFrame.minY - rightBorderFrame.minY) - transition.animatePositionAdditive(node: self.rightBorderNode, offset: rightBorderPositionDelta) - } - } else { - completion?() - } - - self.topPanelBackgroundNode.frame = CGRect(x: 0.0, y: topPanelHeight - 24.0, width: size.width, height: min(topPanelFrame.height, 24.0)) - - let listMaxY = listTopInset + listSize.height - let bottomOffset = min(0.0, bottomEdge - listMaxY) + layout.size.height - bottomPanelHeight - - let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset + floorToScreenPixels((size.width - contentWidth) / 2.0), y: -50.0 + bottomOffset + bottomGradientHeight), size: CGSize(width: contentWidth - sideInset * 2.0, height: 50.0 + 60.0)) - let bottomPanelBackgroundFrame = CGRect(x: 0.0, y: bottomOffset + bottomGradientHeight, width: size.width, height: 2000.0) - let previousBottomCornersFrame = self.bottomCornersNode.frame - if !bottomCornersFrame.equalTo(previousBottomCornersFrame) { - if bottomCornersFrame.width != previousBottomCornersFrame.width { - transition.updateFrame(node: self.bottomCornersNode, frame: bottomCornersFrame) - transition.updateFrame(node: self.bottomPanelBackgroundNode, frame: bottomPanelBackgroundFrame) - } else { - self.bottomCornersNode.frame = bottomCornersFrame - self.bottomPanelBackgroundNode.frame = bottomPanelBackgroundFrame - - let positionDelta = CGPoint(x: 0.0, y: previousBottomCornersFrame.minY - bottomCornersFrame.minY) - transition.animatePositionAdditive(node: self.bottomCornersNode, offset: positionDelta) - transition.animatePositionAdditive(node: self.bottomPanelBackgroundNode, offset: positionDelta) - } - } - - let participantsFrame = CGRect(x: 0.0, y: bottomCornersFrame.maxY - 100.0, width: size.width, height: 216.0) - transition.updateFrame(node: self.participantsNode, frame: participantsFrame) - self.participantsNode.update(size: participantsFrame.size, participants: Int32.random(in: 0..<999999999)/*self.currentTotalCount*/, groupingSeparator: self.presentationData.dateTimeFormat.groupingSeparator, transition: .immediate) - } - - private var decorationsAreDark: Bool? - private var ignoreLayout = false - private func updateDecorationsColors() { - guard let (layout, _) = self.validLayout else { - return - } - - let isFullscreen = self.isFullscreen - let effectiveDisplayMode = self.displayMode - - self.ignoreLayout = true - self.controller?.statusBar.updateStatusBarStyle(isFullscreen ? .White : .Ignore, animated: true) - self.ignoreLayout = false - - let size = layout.size - let topEdgeFrame: CGRect - if isFullscreen { - let offset: CGFloat - if let statusBarHeight = layout.statusBarHeight { - offset = statusBarHeight - } else { - offset = 44.0 - } - topEdgeFrame = CGRect(x: 0.0, y: -offset, width: size.width, height: topPanelHeight + offset) - } else { - topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight) - } - - let backgroundColor: UIColor - if case .fullscreen = effectiveDisplayMode { - backgroundColor = isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor - } else if self.isScheduling || self.callState?.scheduleTimestamp != nil { - backgroundColor = panelBackgroundColor - } else { - backgroundColor = isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor - } - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear) - transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame) - transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0) - transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - transition.updateBackgroundColor(node: self.backgroundNode, color: backgroundColor) - transition.updateBackgroundColor(node: self.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - transition.updateBackgroundColor(node: self.leftBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - - var gridNode: VoiceChatTilesGridItemNode? - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatTilesGridItemNode { - gridNode = itemNode - } - } - if let gridNode = gridNode { - transition.updateBackgroundColor(node: gridNode.backgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - } - - let previousDark = self.decorationsAreDark - self.decorationsAreDark = isFullscreen - if previousDark != self.decorationsAreDark { - if let snapshotView = self.topCornersNode.view.snapshotContentTree() { - snapshotView.frame = self.topCornersNode.bounds - self.topCornersNode.view.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - self.topCornersNode.image = decorationTopCornersImage(dark: isFullscreen) - - if let snapshotView = self.bottomCornersNode.view.snapshotContentTree() { - snapshotView.frame = self.bottomCornersNode.bounds - self.bottomCornersNode.view.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - self.bottomCornersNode.image = decorationBottomCornersImage(dark: isFullscreen) - - if let gridNode = gridNode { - if let snapshotView = gridNode.cornersNode.view.snapshotContentTree() { - snapshotView.frame = gridNode.cornersNode.bounds - gridNode.cornersNode.view.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - gridNode.cornersNode.image = decorationCornersImage(top: true, bottom: false, dark: isFullscreen) - gridNode.supernode?.addSubnode(gridNode) - } - - UIView.transition(with: self.bottomGradientNode.view, duration: 0.3, options: [.transitionCrossDissolve, .curveLinear]) { - self.bottomGradientNode.backgroundColor = decorationBottomGradientImage(dark: isFullscreen).flatMap { UIColor(patternImage: $0) } - } completion: { _ in - } - - self.closeButton.setContent(.image(closeButtonImage(dark: isFullscreen)), animated: transition.isAnimated) - self.optionsButton.setContent(.more(optionsCircleImage(dark: isFullscreen)), animated: transition.isAnimated) - self.panelButton.setContent(.image(panelButtonImage(dark: isFullscreen)), animated: transition.isAnimated) - } - - self.updateTitle(transition: transition) - } - - private func updateTitle(slide: Bool = false, transition: ContainedViewLayoutTransition) { - guard let _ = self.validLayout else { - return - } - - var title = self.currentTitle - if self.isScheduling { - if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info { - title = self.presentationData.strings.ScheduleLiveStream_Title - } else { - title = self.presentationData.strings.ScheduleVoiceChat_Title - } - } else if case .modal(_, false) = self.displayMode, !self.currentTitleIsCustom { - if let navigationController = self.controller?.navigationController as? NavigationController { - for controller in navigationController.viewControllers.reversed() { - if let controller = controller as? ChatController, case let .peer(peerId) = controller.chatLocation, peerId == self.call.peerId { - if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info { - title = self.presentationData.strings.VoiceChatChannel_Title - } else { - title = self.presentationData.strings.VoiceChat_Title - } - } - } - } - } - - var subtitle = "" - var speaking = false - if self.scrollAtTop { - subtitle = self.currentSubtitle - speaking = false - } else { - subtitle = self.currentSpeakingSubtitle ?? self.currentSubtitle - speaking = self.currentSpeakingSubtitle != nil - } - if self.isScheduling { - subtitle = "" - speaking = false - } else if self.callState?.scheduleTimestamp != nil { - if self.callState?.canManageCall ?? false { - subtitle = self.presentationData.strings.VoiceChat_TapToEditTitle - } else { - subtitle = self.presentationData.strings.VoiceChat_Scheduled - } - speaking = false - } - - self.titleNode.update(size: CGSize(width: self.titleNode.bounds.width, height: 44.0), title: title, subtitle: subtitle, speaking: speaking, slide: slide, transition: transition) - } - - private func updateButtons(transition: ContainedViewLayoutTransition) { - guard let (layout, _) = self.validLayout else { - return - } - var audioMode: CallControllerButtonsSpeakerMode = .none - //var hasAudioRouteMenu: Bool = false - if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput { - //hasAudioRouteMenu = availableOutputs.count > 2 - switch currentOutput { - case .builtin: - audioMode = .builtin - case .speaker: - audioMode = .speaker - case .headphones: - audioMode = .headphones - case let .port(port): - var type: CallControllerButtonsSpeakerMode.BluetoothType = .generic - let portName = port.name.lowercased() - if portName.contains("airpods max") { - type = .airpodsMax - } else if portName.contains("airpods pro") { - type = .airpodsPro - } else if portName.contains("airpods") { - type = .airpods - } - audioMode = .bluetooth(type) - } - if availableOutputs.count <= 1 { - audioMode = .none - } - } - - let normalButtonAppearance: CallControllerButtonItemNode.Content.Appearance - let activeButtonAppearance: CallControllerButtonItemNode.Content.Appearance - if let color = self.currentNormalButtonColor { - normalButtonAppearance = .color(.custom(color.rgb, 1.0)) - } else { - normalButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0)) - } - if let color = self.currentActiveButtonColor { - activeButtonAppearance = .color(.custom(color.rgb, 1.0)) - } else { - activeButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0)) - } - - var soundImage: CallControllerButtonItemNode.Content.Image - var soundAppearance: CallControllerButtonItemNode.Content.Appearance = normalButtonAppearance - var soundTitle: String = self.presentationData.strings.Call_Speaker - switch audioMode { - case .none, .builtin: - soundImage = .speaker - case .speaker: - soundImage = .speaker - soundAppearance = activeButtonAppearance - case .headphones: - soundImage = .headphones - soundTitle = self.presentationData.strings.Call_Audio - case let .bluetooth(type): - switch type { - case .generic: - soundImage = .bluetooth - case .airpods: - soundImage = .airpods - case .airpodsPro: - soundImage = .airpodsPro - case .airpodsMax: - soundImage = .airpodsMax - } - soundTitle = self.presentationData.strings.Call_Audio - } - - let isScheduled = self.isScheduling || self.callState?.scheduleTimestamp != nil - - var isSoundEnabled = true - if isScheduled { - if let callState = self.callState, let peer = self.peer, !callState.canManageCall && (peer.addressName?.isEmpty ?? true) { - isSoundEnabled = false - } else { - soundImage = .share - soundTitle = self.presentationData.strings.VoiceChat_ShareShort - soundAppearance = normalButtonAppearance - } - } - - let audioButtonSize: CGSize - var buttonsTitleAlpha: CGFloat - let effectiveDisplayMode = self.displayMode - - let hasCameraButton = self.cameraButton.isUserInteractionEnabled - let hasVideo = self.call.hasVideo - switch effectiveDisplayMode { - case .modal: - audioButtonSize = hasCameraButton ? smallButtonSize : sideButtonSize - buttonsTitleAlpha = 1.0 - case .fullscreen: - if case .regular = layout.metrics.widthClass { - audioButtonSize = hasCameraButton ? smallButtonSize : sideButtonSize - } else { - audioButtonSize = sideButtonSize - } - if case .regular = layout.metrics.widthClass { - buttonsTitleAlpha = 1.0 - } else { - buttonsTitleAlpha = 0.0 - } - } - - self.cameraButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: hasVideo ? activeButtonAppearance : normalButtonAppearance, image: hasVideo ? .cameraOn : .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition) - - self.switchCameraButton.update(size: audioButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .flipCamera), text: "", transition: transition) - - transition.updateAlpha(node: self.switchCameraButton, alpha: hasCameraButton && hasVideo ? 1.0 : 0.0) - transition.updateTransformScale(node: self.switchCameraButton, scale: hasCameraButton && hasVideo ? 1.0 : 0.0) - - transition.updateTransformScale(node: self.cameraButton, scale: hasCameraButton ? 1.0 : 0.0) - - let hasAudioButton = !self.isScheduling - transition.updateAlpha(node: self.audioButton, alpha: hasCameraButton || !hasAudioButton ? 0.0 : 1.0) - transition.updateTransformScale(node: self.audioButton, scale: hasCameraButton || !hasAudioButton ? 0.0 : 1.0) - - self.audioButton.update(size: audioButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage, isEnabled: isSoundEnabled), text: soundTitle, transition: transition) - self.audioButton.isUserInteractionEnabled = isSoundEnabled - - self.leaveButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0xff3b30, 0.3)), image: .cancel), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate) - - transition.updateAlpha(node: self.cameraButton.textNode, alpha: buttonsTitleAlpha) - transition.updateAlpha(node: self.switchCameraButton.textNode, alpha: buttonsTitleAlpha) - transition.updateAlpha(node: self.audioButton.textNode, alpha: buttonsTitleAlpha) - transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - guard !self.ignoreLayout else { - return - } - let isFirstTime = self.validLayout == nil - let previousLayout = self.validLayout?.0 - self.validLayout = (layout, navigationHeight) - - let size = layout.size - let contentWidth: CGFloat - let headerWidth: CGFloat - let contentLeftInset: CGFloat - if case .regular = layout.metrics.widthClass { - contentWidth = max(320.0, min(375.0, floor(size.width * 0.3))) - headerWidth = size.width - if self.peerIdToEndpointId.isEmpty { - contentLeftInset = 0.0 - } else { - contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth - } - } else { - contentWidth = self.isLandscape ? min(530.0, size.width - 210.0) : size.width - headerWidth = contentWidth - contentLeftInset = 0.0 - } - - var previousIsLandscape = false - if let previousLayout = previousLayout, case .compact = previousLayout.metrics.widthClass, previousLayout.size.width > previousLayout.size.height { - previousIsLandscape = true - } - var shouldSwitchToExpanded = false - if case let .modal(isExpanded, _) = self.displayMode { - if previousIsLandscape != self.isLandscape && !isExpanded { - shouldSwitchToExpanded = true - } else if case .regular = layout.metrics.widthClass, !isExpanded { - shouldSwitchToExpanded = true - } - } - if shouldSwitchToExpanded { - self.displayMode = .modal(isExpanded: true, isFilled: true) - self.updateDecorationsColors() - self.updateDecorationsLayout(transition: transition) - self.updateMembers() - } else if case .fullscreen = self.displayMode, previousIsLandscape != self.isLandscape { - self.updateMembers() - } - - let effectiveDisplayMode = self.displayMode - - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerWidth) / 2.0), y: 10.0), size: CGSize(width: headerWidth, height: 44.0))) - self.updateTitle(transition: transition) - - transition.updateFrame(node: self.optionsButton, frame: CGRect(origin: CGPoint(x: 20.0 + floorToScreenPixels((size.width - headerWidth) / 2.0), y: 18.0), size: CGSize(width: 28.0, height: 28.0))) - transition.updateFrame(node: self.panelButton, frame: CGRect(origin: CGPoint(x: size.width - floorToScreenPixels((size.width - headerWidth) / 2.0) - 20.0 - 28.0 - 38.0 - 24.0, y: 18.0), size: CGSize(width: 38.0, height: 28.0))) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - floorToScreenPixels((size.width - headerWidth) / 2.0) - 20.0 - 28.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0))) - - transition.updateAlpha(node: self.optionsButton, alpha: self.optionsButton.isUserInteractionEnabled ? 1.0 : 0.0) - transition.updateAlpha(node: self.panelButton, alpha: self.panelButton.isUserInteractionEnabled ? 1.0 : 0.0) - - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: 0.0), size: size)) - - let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) - let sideInset: CGFloat = 14.0 - - var listInsets = UIEdgeInsets() - listInsets.left = sideInset + (self.isLandscape ? 0.0 : layout.safeInsets.left) - listInsets.right = sideInset + (self.isLandscape ? 0.0 : layout.safeInsets.right) - - let topEdgeOffset: CGFloat - if let statusBarHeight = layout.statusBarHeight { - topEdgeOffset = statusBarHeight - } else { - topEdgeOffset = 44.0 - } - - if self.isLandscape { - transition.updateFrame(node: self.topPanelEdgeNode, frame: CGRect(x: 0.0, y: -topEdgeOffset, width: size.width, height: topPanelHeight + topEdgeOffset)) - } else if let _ = self.panGestureArguments { - } else { - let topEdgeFrame: CGRect - if self.isFullscreen { - topEdgeFrame = CGRect(x: 0.0, y: -topEdgeOffset, width: size.width, height: topPanelHeight + topEdgeOffset) - } else { - topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight) - } - transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame) - } - - let bottomPanelHeight = self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom - var listTopInset = layoutTopInset + topPanelHeight - if self.isLandscape { - listTopInset = topPanelHeight - } - - let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - (self.isLandscape ? layout.intrinsicInsets.bottom : bottomPanelHeight) + bottomGradientHeight) - let topInset: CGFloat - if let (panInitialTopInset, panOffset) = self.panGestureArguments { - if self.isExpanded { - topInset = min(self.topInset ?? listSize.height, panInitialTopInset + max(0.0, panOffset)) - } else { - topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) - } - } else if case .regular = layout.metrics.widthClass { - topInset = 0.0 - } else if let currentTopInset = self.topInset { - topInset = self.isExpanded ? 0.0 : currentTopInset - } else { - topInset = listSize.height - 46.0 - floor(56.0 * 3.5) - bottomGradientHeight - } - - transition.updateFrameAsPositionAndBounds(node: self.listContainer, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: contentLeftInset.isZero ? floorToScreenPixels((size.width - contentWidth) / 2.0) : contentLeftInset, y: listTopInset + topInset), size: listSize)) - - let tileGridSize = CGSize(width: max(0.0, contentLeftInset - sideInset), height: size.height - layout.intrinsicInsets.bottom - listTopInset - topInset) - - if contentLeftInset > 0.0 { - self.tileGridNode.isHidden = false - } - if !self.tileGridNode.isHidden { - let _ = self.tileGridNode.update(size: tileGridSize, layoutMode: .grid, items: self.currentTileItems, transition: transition, completion: { [weak self] in - if contentLeftInset.isZero && transition.isAnimated { - self?.tileGridNode.isHidden = true - } - }) - } - transition.updateFrame(node: self.tileGridNode, frame: CGRect(origin: CGPoint(x: sideInset, y: listTopInset + topInset), size: tileGridSize)) - self.tileGridNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: tileGridSize), within: tileGridSize) - - listInsets.bottom = bottomGradientHeight - - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: listSize, insets: listInsets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - let fullscreenListWidth: CGFloat - let fullscreenListHeight: CGFloat = 84.0 - let fullscreenListTransform: CATransform3D - let fullscreenListInset: CGFloat = 14.0 - let fullscreenListUpdateSizeAndInsets: ListViewUpdateSizeAndInsets - let fullscreenListContainerFrame: CGRect - if self.isLandscape { - fullscreenListWidth = layout.size.height - fullscreenListTransform = CATransform3DIdentity - fullscreenListUpdateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: fullscreenListHeight, height: layout.size.height), insets: UIEdgeInsets(top: fullscreenListInset, left: 0.0, bottom: fullscreenListInset, right: 0.0), duration: duration, curve: curve) - fullscreenListContainerFrame = CGRect(x: layout.size.width - min(self.effectiveBottomAreaHeight, fullscreenBottomAreaHeight) - layout.safeInsets.right - fullscreenListHeight - 4.0, y: 0.0, width: fullscreenListHeight, height: layout.size.height) - } else { - fullscreenListWidth = layout.size.width - fullscreenListTransform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0) - fullscreenListUpdateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: fullscreenListHeight, height: layout.size.width), insets: UIEdgeInsets(top: fullscreenListInset + layout.safeInsets.left, left: 0.0, bottom: fullscreenListInset + layout.safeInsets.left, right: 0.0), duration: duration, curve: curve) - fullscreenListContainerFrame = CGRect(x: 0.0, y: layout.size.height - min(bottomPanelHeight, fullscreenBottomAreaHeight + layout.intrinsicInsets.bottom) - fullscreenListHeight - 4.0, width: layout.size.width, height: fullscreenListHeight) - } - - transition.updateFrame(node: self.fullscreenListContainer, frame: fullscreenListContainerFrame) - - self.fullscreenListNode.bounds = CGRect(x: 0.0, y: 0.0, width: fullscreenListHeight, height: fullscreenListWidth) - transition.updatePosition(node: self.fullscreenListNode, position: CGPoint(x: fullscreenListContainerFrame.width / 2.0, y: fullscreenListContainerFrame.height / 2.0)) - - self.fullscreenListNode.transform = fullscreenListTransform - self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: fullscreenListUpdateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - if case .regular = layout.metrics.widthClass { - self.transitionContainerNode.view.mask = nil - } else { - self.transitionContainerNode.view.mask = self.transitionMaskView - } - - var childrenLayout = layout - var childrenInsets = childrenLayout.intrinsicInsets - var childrenSafeInsets = childrenLayout.safeInsets - if case .regular = layout.metrics.widthClass { - let childrenLayoutWidth: CGFloat = 375.0 - if contentLeftInset.isZero { - childrenSafeInsets.left = floorToScreenPixels((size.width - childrenLayoutWidth) / 2.0) - childrenSafeInsets.right = floorToScreenPixels((size.width - childrenLayoutWidth) / 2.0) - } else { - childrenSafeInsets.left = floorToScreenPixels((contentLeftInset - childrenLayoutWidth) / 2.0) - childrenSafeInsets.right = childrenSafeInsets.left + (size.width - contentLeftInset) - } - } else if !self.isLandscape, case .fullscreen = effectiveDisplayMode { - childrenInsets.bottom += self.effectiveBottomAreaHeight + fullscreenListHeight + 36.0 - } - childrenLayout.safeInsets = childrenSafeInsets - childrenLayout.intrinsicInsets = childrenInsets - self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition) - - var bottomPanelLeftInset = contentLeftInset - var bottomPanelWidth = size.width - contentLeftInset - if case .regular = layout.metrics.widthClass, bottomPanelLeftInset.isZero { - bottomPanelLeftInset = floorToScreenPixels((layout.size.width - contentWidth) / 2.0) - bottomPanelWidth = contentWidth - } - - var bottomPanelFrame = CGRect(origin: CGPoint(x: bottomPanelLeftInset, y: layout.size.height - bottomPanelHeight), size: CGSize(width: bottomPanelWidth, height: bottomPanelHeight)) - let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom - if self.isLandscape { - bottomPanelFrame = CGRect(origin: CGPoint(x: layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right, y: 0.0), size: CGSize(width: fullscreenBottomAreaHeight + layout.safeInsets.right, height: layout.size.height)) - } - let bottomGradientFrame = CGRect(origin: CGPoint(x: bottomPanelLeftInset, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: bottomPanelWidth, height: bottomGradientHeight)) - transition.updateFrame(node: self.bottomGradientNode, frame: bottomGradientFrame) - transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame) - - if let pickerView = self.pickerView { - transition.updateFrame(view: pickerView, frame: CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0)) - } - - let timerFrame = CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0) - transition.updateFrame(node: self.timerNode, frame: timerFrame) - self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate) - - let scheduleTextSize = self.scheduleTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - self.scheduleTextNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scheduleTextSize.width) / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - scheduleTextSize.height - 145.0), size: scheduleTextSize) - - let centralButtonSide = min(contentWidth, size.height) - 32.0 - let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide) - let cameraButtonSize = smallButtonSize - let sideButtonMinimalInset: CGFloat = 16.0 - let sideButtonOffset = min(42.0, floor((((contentWidth - 112.0) / 2.0) - sideButtonSize.width) / 2.0)) - let sideButtonOrigin = max(sideButtonMinimalInset, floor((contentWidth - 112.0) / 2.0) - sideButtonOffset - sideButtonSize.width) - - let smallButtons: Bool - if case .regular = layout.metrics.widthClass { - smallButtons = false - } else { - switch effectiveDisplayMode { - case .modal: - smallButtons = self.isLandscape - case .fullscreen: - smallButtons = true - } - } - let actionButtonState: VoiceChatActionButton.State - let actionButtonTitle: String - let actionButtonSubtitle: String - var actionButtonEnabled = true - if let callState = self.callState, !self.isScheduling { - if callState.scheduleTimestamp != nil { - self.ignoreConnecting = true - if callState.canManageCall { - actionButtonState = .scheduled(state: .start) - actionButtonTitle = self.presentationData.strings.VoiceChat_StartNow - actionButtonSubtitle = "" - } else { - if callState.subscribedToScheduled { - actionButtonState = .scheduled(state: .unsubscribe) - actionButtonTitle = self.presentationData.strings.VoiceChat_CancelReminder - } else { - actionButtonState = .scheduled(state: .subscribe) - actionButtonTitle = self.presentationData.strings.VoiceChat_SetReminder - } - actionButtonSubtitle = "" - } - } else { - let connected = self.ignoreConnecting || callState.networkState == .connected - if case .connected = callState.networkState { - self.ignoreConnecting = false - self.ignoreConnectingTimer?.invalidate() - self.ignoreConnectingTimer = nil - } else if self.ignoreConnecting { - if self.ignoreConnectingTimer == nil { - let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in - if let strongSelf = self { - strongSelf.ignoreConnecting = false - strongSelf.ignoreConnectingTimer?.invalidate() - strongSelf.ignoreConnectingTimer = nil - - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - } - } - }, queue: Queue.mainQueue()) - self.ignoreConnectingTimer = timer - timer.start() - } - } - - if connected { - if let muteState = callState.muteState, !self.pushingToTalk && !self.temporaryPushingToTalk { - if muteState.canUnmute { - actionButtonState = .active(state: .muted) - - actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute - actionButtonSubtitle = "" - } else { - actionButtonState = .active(state: .cantSpeak) - - if callState.raisedHand { - actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak - actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp - } else { - actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin - actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp - } - } - } else { - actionButtonState = .active(state: .on) - - actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute - actionButtonSubtitle = "" - } - } else { - actionButtonState = .connecting - actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting - actionButtonSubtitle = "" - actionButtonEnabled = false - } - } - } else { - if self.isScheduling { - actionButtonState = .button(text: self.scheduleButtonTitle) - actionButtonTitle = "" - actionButtonSubtitle = "" - actionButtonEnabled = true - } else { - actionButtonState = .connecting - actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting - actionButtonSubtitle = "" - actionButtonEnabled = false - } - } - - self.actionButton.isDisabled = !actionButtonEnabled - self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true) - - let isVideoEnabled = self.callState?.isVideoEnabled ?? false - var hasCameraButton = isVideoEnabled - if let joinedVideo = self.joinedVideo { - hasCameraButton = joinedVideo - } - if !isVideoEnabled { - hasCameraButton = false - } - switch actionButtonState { - case let .active(state): - switch state { - case .cantSpeak: - hasCameraButton = false - case .on, .muted: - break - } - case .connecting: - if !self.connectedOnce { - hasCameraButton = false - } - case .scheduled, .button: - hasCameraButton = false - } - let hasVideo = hasCameraButton && self.call.hasVideo - - let upperButtonDistance: CGFloat = 12.0 - let firstButtonFrame: CGRect - let secondButtonFrame: CGRect - let thirdButtonFrame: CGRect - let forthButtonFrame: CGRect - - let leftButtonFrame: CGRect - if self.isScheduled || !hasVideo { - leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) - } else { - leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height - upperButtonDistance - cameraButtonSize.height) / 2.0) + upperButtonDistance + cameraButtonSize.height), size: sideButtonSize) - } - let rightButtonFrame = CGRect(origin: CGPoint(x: contentWidth - sideButtonOrigin - sideButtonSize.width, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) - var centerButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) - - if case .regular = layout.metrics.widthClass { - centerButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentWidth - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) - - if hasCameraButton { - firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize) - } else { - firstButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.center.x - cameraButtonSize.width / 2.0, y: leftButtonFrame.center.y - cameraButtonSize.height / 2.0), size: cameraButtonSize) - } - secondButtonFrame = leftButtonFrame - thirdButtonFrame = centerButtonFrame - forthButtonFrame = rightButtonFrame - } else { - switch effectiveDisplayMode { - case .modal: - if self.isLandscape { - let sideInset: CGFloat - let buttonsCount: Int - if hasVideo { - sideInset = 26.0 - buttonsCount = 4 - } else { - sideInset = 42.0 - buttonsCount = 3 - } - let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) - let x = floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) - forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize) - let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) - thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) - secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize) - if hasCameraButton { - firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize) - } else { - firstButtonFrame = secondButtonFrame - } - } else { - if hasCameraButton { - firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize) - } else { - firstButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.center.x - cameraButtonSize.width / 2.0, y: leftButtonFrame.center.y - cameraButtonSize.height / 2.0), size: cameraButtonSize) - } - secondButtonFrame = leftButtonFrame - thirdButtonFrame = centerButtonFrame - forthButtonFrame = rightButtonFrame - } - case let .fullscreen(controlsHidden): - if self.isLandscape { - let sideInset: CGFloat - let buttonsCount: Int - if hasVideo { - sideInset = 26.0 - buttonsCount = 4 - } else { - sideInset = 42.0 - buttonsCount = 3 - } - let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) - let x = controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) - forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize) - let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) - thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) - secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize) - if hasVideo { - firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize) - } else { - firstButtonFrame = secondButtonFrame - } - } else { - let sideInset: CGFloat - let buttonsCount: Int - if hasVideo { - sideInset = 26.0 - buttonsCount = 4 - } else { - sideInset = 42.0 - buttonsCount = 3 - } - let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) - let y = controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0) - if hasVideo { - firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) - secondButtonFrame = CGRect(origin: CGPoint(x: firstButtonFrame.maxX + spacing, y: y), size: sideButtonSize) - } else { - firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) - secondButtonFrame = firstButtonFrame - } - let thirdButtonPreFrame = CGRect(origin: CGPoint(x: secondButtonFrame.maxX + spacing, y: y), size: sideButtonSize) - thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) - forthButtonFrame = CGRect(origin: CGPoint(x: thirdButtonPreFrame.maxX + spacing, y: y), size: sideButtonSize) - } - } - } - - let buttonHeight = self.scheduleCancelButton.updateLayout(width: size.width - 32.0, transition: .immediate) - self.scheduleCancelButton.frame = CGRect(x: 16.0, y: 137.0, width: size.width - 32.0, height: buttonHeight) - - if self.actionButton.supernode === self.bottomPanelNode { - transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame, completion: transition.isAnimated ? { [weak self] _ in - self?.animatingExpansion = false - } : nil) - } - - self.cameraButton.isUserInteractionEnabled = hasCameraButton - - var buttonsTransition: ContainedViewLayoutTransition = .immediate - if !isFirstTime { - if case .animated(_, .spring) = transition { - buttonsTransition = transition - } else { - buttonsTransition = .animated(duration: 0.3, curve: .linear) - } - } - self.updateButtons(transition: buttonsTransition) - - if self.audioButton.supernode === self.bottomPanelNode { - transition.updateAlpha(node: self.cameraButton, alpha: hasCameraButton ? 1.0 : 0.0) - transition.updateFrameAsPositionAndBounds(node: self.switchCameraButton, frame: firstButtonFrame) - - if !self.animatingButtonsSwap || transition.isAnimated { - transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: secondButtonFrame, completion: { [weak self] _ in - self?.animatingButtonsSwap = false - }) - transition.updateFrameAsPositionAndBounds(node: self.cameraButton, frame: secondButtonFrame) - } - transition.updateFrameAsPositionAndBounds(node: self.leaveButton, frame: forthButtonFrame) - } - if isFirstTime { - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - while !self.enqueuedFullscreenTransitions.isEmpty { - self.dequeueFullscreenTransition() - } - } - } - - private var appIsActive = true { - didSet { - if self.appIsActive != oldValue { - self.updateVisibility() - self.updateRequestedVideoChannels() - } - } - } - private var visibility = false { - didSet { - if self.visibility != oldValue { - self.updateVisibility() - self.updateRequestedVideoChannels() - } - } - } - - private func updateVisibility() { - let visible = self.appIsActive && self.visibility - if self.tileGridNode.isHidden { - self.tileGridNode.visibility = false - } else { - self.tileGridNode.visibility = visible - } - self.mainStageNode.visibility = visible - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatTilesGridItemNode { - itemNode.gridVisibility = visible - } - } - self.fullscreenListNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode { - itemNode.gridVisibility = visible - } - } - - self.videoRenderingContext.updateVisibility(isVisible: visible) - } - - func animateIn() { - guard let (layout, navigationHeight) = self.validLayout else { - return - } - - self.visibility = true - - self.updateDecorationsLayout(transition: .immediate) - - self.animatingAppearance = true - - let initialBounds = self.contentContainer.bounds - let topPanelFrame = self.topPanelNode.view.convert(self.topPanelNode.bounds, to: self.view) - self.contentContainer.bounds = initialBounds.offsetBy(dx: 0.0, dy: -(layout.size.height - topPanelFrame.minY)) - self.contentContainer.isHidden = false - - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - transition.animateView({ - self.contentContainer.view.bounds = initialBounds - }, completion: { _ in - self.animatingAppearance = false - if self.actionButton.supernode !== self.bottomPanelNode { - self.actionButton.ignoreHierarchyChanges = true - self.audioButton.isHidden = false - self.cameraButton.isHidden = false - self.leaveButton.isHidden = false - self.audioButton.layer.removeAllAnimations() - self.cameraButton.layer.removeAllAnimations() - self.leaveButton.layer.removeAllAnimations() - self.bottomPanelNode.addSubnode(self.cameraButton) - self.bottomPanelNode.addSubnode(self.audioButton) - self.bottomPanelNode.addSubnode(self.leaveButton) - self.bottomPanelNode.addSubnode(self.actionButton) - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - self.actionButton.ignoreHierarchyChanges = false - } - - self.controller?.currentOverlayController?.dismiss() - self.controller?.currentOverlayController = nil - }) - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } - - func animateOut(completion: (() -> Void)?) { - guard let (layout, _) = self.validLayout else { - return - } - var offsetCompleted = false - let internalCompletion: () -> Void = { [weak self] in - if offsetCompleted { - if let strongSelf = self { - strongSelf.contentContainer.layer.removeAllAnimations() - strongSelf.dimNode.layer.removeAllAnimations() - - var bounds = strongSelf.contentContainer.bounds - bounds.origin.y = 0.0 - strongSelf.contentContainer.bounds = bounds - - strongSelf.visibility = false - } - completion?() - } - } - - let topPanelFrame = self.topPanelNode.view.convert(self.topPanelNode.bounds, to: self.view) - self.contentContainer.layer.animateBoundsOriginYAdditive(from: self.contentContainer.bounds.origin.y, to: -(layout.size.height - topPanelFrame.minY) - 44.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - offsetCompleted = true - internalCompletion() - }) - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - } - - private func enqueueTransition(_ transition: ListTransition) { - self.enqueuedTransitions.append(transition) - - if let _ = self.validLayout { - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - } - - private func enqueueFullscreenTransition(_ transition: ListTransition) { - self.enqueuedFullscreenTransitions.append(transition) - - if let _ = self.validLayout { - while !self.enqueuedFullscreenTransitions.isEmpty { - self.dequeueFullscreenTransition() - } - } - } - - private func dequeueTransition() { - guard let (layout, _) = self.validLayout, let transition = self.enqueuedTransitions.first else { - return - } - self.enqueuedTransitions.remove(at: 0) - - if let callState = self.callState { - if callState.scheduleTimestamp != nil && self.listNode.alpha > 0.0 { - self.timerNode.isHidden = false - self.cameraButton.alpha = 0.0 - self.cameraButton.isUserInteractionEnabled = false - self.listNode.alpha = 0.0 - self.listNode.isUserInteractionEnabled = false - self.backgroundNode.backgroundColor = panelBackgroundColor - self.updateDecorationsColors() - } else if callState.scheduleTimestamp == nil && !self.isScheduling && self.listNode.alpha == 0.0 { - self.transitionToCall() - } - } - - var options = ListViewDeleteAndInsertOptions() - let isFirstTime = self.isFirstTime - if isFirstTime { - self.isFirstTime = false - } else { - if transition.crossFade { - options.insert(.AnimateCrossfade) - } - if transition.animated { - options.insert(.AnimateInsertion) - } - } - options.insert(.LowLatency) - options.insert(.PreferSynchronousResourceLoading) - - var size = layout.size - if case .regular = layout.metrics.widthClass { - size.width = floor(min(size.width, size.height) * 0.5) - } - - let bottomPanelHeight = self.isLandscape ? layout.intrinsicInsets.bottom : bottomAreaHeight + layout.intrinsicInsets.bottom - let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) - let listTopInset = layoutTopInset + topPanelHeight - let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight + bottomGradientHeight) - - self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5) - bottomGradientHeight - - if transition.animated { - self.animatingInsertion = true - } - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in - guard let strongSelf = self else { - return - } - if isFirstTime { - strongSelf.updateDecorationsLayout(transition: .immediate) - } else if strongSelf.animatingInsertion { - strongSelf.updateDecorationsLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) - } - strongSelf.animatingInsertion = false - if !strongSelf.didSetContentsReady { - strongSelf.didSetContentsReady = true - strongSelf.controller?.contentsReady.set(true) - } - strongSelf.updateVisibility() - }) - } - - private func dequeueFullscreenTransition() { - guard let _ = self.validLayout, let transition = self.enqueuedFullscreenTransitions.first else { - return - } - self.enqueuedFullscreenTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - let isFirstTime = self.isFirstTime - if !isFirstTime { - if transition.animated { - options.insert(.AnimateInsertion) - } - } - - self.fullscreenListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - - private func updateMembers(maybeUpdateVideo: Bool = true, force: Bool = false) { - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), maybeUpdateVideo: maybeUpdateVideo, force: force) - } - - private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [EnginePeer], speakingPeers: Set, maybeUpdateVideo: Bool = true, force: Bool = false) { - var disableAnimation = false - if self.currentCallMembers?.1 != callMembers.1 { - disableAnimation = true - } - - let speakingPeersUpdated = self.currentSpeakingPeers != speakingPeers - self.currentCallMembers = callMembers - self.currentInvitedPeers = invitedPeers - - var entries: [ListEntry] = [] - var fullscreenEntries: [ListEntry] = [] - var index: Int32 = 0 - var fullscreenIndex: Int32 = 0 - var processedPeerIds = Set() - var processedFullscreenPeerIds = Set() - - var peerIdToCameraEndpointId: [PeerId: String] = [:] - var peerIdToEndpointId: [PeerId: String] = [:] - - var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = [] - var gridTileItems: [VoiceChatTileItem] = [] - var tileItems: [VoiceChatTileItem] = [] - var gridTileByVideoEndpoint: [String: VoiceChatTileItem] = [:] - var tileByVideoEndpoint: [String: VoiceChatTileItem] = [:] - var entryByPeerId: [PeerId: VoiceChatPeerEntry] = [:] - var latestWideVideo: String? = nil - - var isTablet = false - var displayPanelVideos = false - if let (layout, _) = self.validLayout, case .regular = layout.metrics.widthClass { - isTablet = true - displayPanelVideos = self.displayPanelVideos - } - -// let isLivestream: Bool -// if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { -// isLivestream = true -// } else { -// isLivestream = false -// } - - let canManageCall = self.callState?.canManageCall ?? false - - var joinedVideo = self.joinedVideo ?? true - - var myEntry: VoiceChatPeerEntry? - var mainEntry: VoiceChatPeerEntry? - for member in callMembers.0 { - if processedPeerIds.contains(member.peer.id) { - continue - } - processedPeerIds.insert(member.peer.id) - - let memberState: VoiceChatPeerEntry.State - var memberMuteState: GroupCallParticipantsContext.Participant.MuteState? - if member.hasRaiseHand && !(member.muteState?.canUnmute ?? true) { -// if isLivestream && !canManageCall { -// continue -// } - memberState = .raisedHand - memberMuteState = member.muteState - - if self.raisedHandDisplayDisposables[member.peer.id] == nil { - var displayedRaisedHands = self.displayedRaisedHands - displayedRaisedHands.insert(member.peer.id) - self.displayedRaisedHands = displayedRaisedHands - - let signal: Signal = Signal.complete() |> delay(3.0, queue: Queue.mainQueue()) - self.raisedHandDisplayDisposables[member.peer.id] = signal.start(completed: { [weak self] in - if let strongSelf = self { - var displayedRaisedHands = strongSelf.displayedRaisedHands - displayedRaisedHands.remove(member.peer.id) - strongSelf.displayedRaisedHands = displayedRaisedHands - strongSelf.updateMembers() - } - }) - } - } else { - if member.peer.id == self.callState?.myPeerId { - if muteState == nil { - memberState = speakingPeers.contains(member.peer.id) ? .speaking : .listening - } else { - memberState = .listening - memberMuteState = member.muteState - } - } else { - memberState = speakingPeers.contains(member.peer.id) ? .speaking : .listening - memberMuteState = member.muteState - } - - if let disposable = self.raisedHandDisplayDisposables[member.peer.id] { - disposable.dispose() - self.raisedHandDisplayDisposables[member.peer.id] = nil - } - -// if isLivestream && !(memberMuteState?.canUnmute ?? true) { -// continue -// } - } - - var memberPeer = member.peer - if member.peer.id == self.callState?.myPeerId { - joinedVideo = member.joinedVideo - if let user = memberPeer as? TelegramUser, let photo = self.currentUpdatingAvatar { - memberPeer = user.withUpdatedPhoto([photo]) - } - } - - joinedVideo = true - - if let videoEndpointId = member.videoEndpointId { - peerIdToCameraEndpointId[member.peer.id] = videoEndpointId - } - if let anyEndpointId = member.presentationEndpointId ?? member.videoEndpointId { - peerIdToEndpointId[member.peer.id] = anyEndpointId - } - - let peerEntry = VoiceChatPeerEntry( - peer: memberPeer, - about: member.about, - isMyPeer: self.callState?.myPeerId == member.peer.id, - videoEndpointId: member.videoEndpointId, - videoPaused: member.videoDescription?.isPaused ?? false, - presentationEndpointId: member.presentationEndpointId, - presentationPaused: member.presentationDescription?.isPaused ?? false, - effectiveSpeakerVideoEndpointId: self.effectiveSpeaker?.1, - state: memberState, - muteState: memberMuteState, - canManageCall: canManageCall, - volume: member.volume, - raisedHand: member.hasRaiseHand, - displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id), - active: memberPeer.id == self.effectiveSpeaker?.0, - isLandscape: self.isLandscape - ) - if peerEntry.isMyPeer { - myEntry = peerEntry - } - if peerEntry.active { - mainEntry = peerEntry - } - entryByPeerId[peerEntry.peer.id] = peerEntry - - var isTile = false - if let interaction = self.itemInteraction { - if let videoEndpointId = member.presentationEndpointId { - if !self.videoOrder.contains(videoEndpointId) { - if peerEntry.isMyPeer { - self.videoOrder.insert(videoEndpointId, at: 0) - } else { - self.videoOrder.append(videoEndpointId) - } - } - if isTablet { - if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.presentationDescription?.isPaused ?? false, showAsPresentation: peerIdToCameraEndpointId[peerEntry.peer.id] != nil, secondary: false) { - isTile = true - gridTileByVideoEndpoint[videoEndpointId] = tileItem - } - } - if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.presentationDescription?.isPaused ?? false, showAsPresentation: peerIdToCameraEndpointId[peerEntry.peer.id] != nil, secondary: displayPanelVideos) { - isTile = true - tileByVideoEndpoint[videoEndpointId] = tileItem - } - if self.wideVideoNodes.contains(videoEndpointId) { - latestWideVideo = videoEndpointId - } - } - if let videoEndpointId = member.videoEndpointId { - if !self.videoOrder.contains(videoEndpointId) { - if peerEntry.isMyPeer { - self.videoOrder.insert(videoEndpointId, at: 0) - } else { - self.videoOrder.append(videoEndpointId) - } - } - if isTablet { - if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.videoDescription?.isPaused ?? false, showAsPresentation: false, secondary: false) { - isTile = true - gridTileByVideoEndpoint[videoEndpointId] = tileItem - } - } - if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.videoDescription?.isPaused ?? false, showAsPresentation: false, secondary: displayPanelVideos) { - isTile = true - tileByVideoEndpoint[videoEndpointId] = tileItem - } - if self.wideVideoNodes.contains(videoEndpointId) { - latestWideVideo = videoEndpointId - } - } - } - - if !isTile || isTablet || !joinedVideo { - entries.append(.peer(peerEntry, index)) - } - - index += 1 - - if self.callState?.networkState == .connecting { - } else { - if var videoChannel = member.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) { - if self.effectiveSpeaker?.1 == videoChannel.endpointId { - videoChannel.maxQuality = .full - } - requestedVideoChannels.append(videoChannel) - } - if member.peer.id != self.callState?.myPeerId { - if var presentationChannel = member.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .thumbnail) { - if self.effectiveSpeaker?.1 == presentationChannel.endpointId { - presentationChannel.minQuality = .full - presentationChannel.maxQuality = .full - } - requestedVideoChannels.append(presentationChannel) - } - } - } - } - - var temporaryList: [String] = [] - for tileVideoEndpoint in self.videoOrder { - if let _ = tileByVideoEndpoint[tileVideoEndpoint] { - temporaryList.append(tileVideoEndpoint) - } - } - - if (tileByVideoEndpoint.count % 2) != 0, let last = temporaryList.last, !self.wideVideoNodes.contains(last), let latestWide = latestWideVideo { - self.videoOrder.removeAll(where: { $0 == latestWide }) - self.videoOrder.append(latestWide) - } - - for tileVideoEndpoint in self.videoOrder { - if let tileItem = gridTileByVideoEndpoint[tileVideoEndpoint] { - gridTileItems.append(tileItem) - } - if let tileItem = tileByVideoEndpoint[tileVideoEndpoint] { - if displayPanelVideos && tileItem.peer.id == self.effectiveSpeaker?.0 { - } else { - tileItems.append(tileItem) - } - if let fullscreenEntry = entryByPeerId[tileItem.peer.id] { - if processedFullscreenPeerIds.contains(tileItem.peer.id) { - continue - } - fullscreenEntries.append(.peer(fullscreenEntry, fullscreenIndex)) - processedFullscreenPeerIds.insert(fullscreenEntry.peer.id) - fullscreenIndex += 1 - } - } - } - - self.joinedVideo = joinedVideo - - let configuration = self.configuration ?? VoiceChatConfiguration.defaultValue - var reachedLimit = false - - if !joinedVideo && (!tileItems.isEmpty || !gridTileItems.isEmpty), let peer = self.peer { - tileItems.removeAll() - gridTileItems.removeAll() - - tileItems.append(VoiceChatTileItem(account: self.context.account, peer: peer, videoEndpointId: "", videoReady: false, videoTimeouted: true, isVideoLimit: true, videoLimit: configuration.videoParticipantsMaxCount, isPaused: false, isOwnScreencast: false, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, speaking: false, secondary: false, isTablet: false, icon: .none, text: .none, additionalText: nil, action: {}, contextAction: nil, getVideo: { _ in return nil }, getAudioLevel: nil)) - } else if let callState = self.callState, !tileItems.isEmpty && callState.isVideoWatchersLimitReached && self.connectedOnce && (callState.canManageCall || callState.adminIds.contains(self.context.account.peerId)) { - reachedLimit = true - } - - for member in callMembers.0 { - if processedFullscreenPeerIds.contains(member.peer.id) { - continue - } - processedFullscreenPeerIds.insert(member.peer.id) - if let peerEntry = entryByPeerId[member.peer.id] { - fullscreenEntries.append(.peer(peerEntry, fullscreenIndex)) - fullscreenIndex += 1 - } - } - - for peer in invitedPeers { - if processedPeerIds.contains(peer.id) { - continue - } - processedPeerIds.insert(peer.id) - - entries.append(.peer(VoiceChatPeerEntry( - peer: peer._asPeer(), - about: nil, - isMyPeer: false, - videoEndpointId: nil, - videoPaused: false, - presentationEndpointId: nil, - presentationPaused: false, - effectiveSpeakerVideoEndpointId: nil, - state: .invited, - muteState: nil, - canManageCall: false, - volume: nil, - raisedHand: false, - displayRaisedHandStatus: false, - active: false, - isLandscape: false - ), index)) - index += 1 - } - - self.requestedVideoChannels = requestedVideoChannels - - var myVideoUpdated = false - if let previousMyEntry = self.myEntry, let myEntry = myEntry, previousMyEntry.effectiveVideoEndpointId == nil && myEntry.effectiveVideoEndpointId != nil && self.currentForcedSpeaker == nil { - self.currentDominantSpeaker = (myEntry.peer.id, myEntry.effectiveVideoEndpointId, CACurrentMediaTime()) - myVideoUpdated = true - } - self.myEntry = myEntry - - guard self.didSetDataReady && (force || (!self.isPanning && !self.animatingExpansion && !self.animatingMainStage)) else { - return - } - - let previousMainEntry = self.mainEntry - self.mainEntry = mainEntry - if let mainEntry = mainEntry { - self.mainStageNode.update(peerEntry: mainEntry, pinned: self.currentForcedSpeaker != nil) - - if let previousMainEntry = previousMainEntry, maybeUpdateVideo { - if previousMainEntry.effectiveVideoEndpointId != mainEntry.effectiveVideoEndpointId || previousMainEntry.videoPaused != mainEntry.videoPaused || myVideoUpdated { - self.updateMainVideo(waitForFullSize: true, entries: fullscreenEntries, force: true) - return - } - } - } else if self.effectiveSpeaker != nil, !fullscreenEntries.isEmpty { - self.updateMainVideo(waitForFullSize: true, entries: fullscreenEntries, force: true) - return - } - - self.updateRequestedVideoChannels() - - self.currentSpeakingPeers = speakingPeers - self.peerIdToEndpointId = peerIdToEndpointId - - var updateLayout = false - var animatingLayout = false - if self.currentTileItems.isEmpty != gridTileItems.isEmpty { - animatingLayout = true - updateLayout = true - } - if isTablet { - updateLayout = true - self.currentTileItems = gridTileItems - if displayPanelVideos && !tileItems.isEmpty { - entries.insert(.tiles(tileItems, .pairs, configuration.videoParticipantsMaxCount, reachedLimit), at: 0) - } - } else { - if !tileItems.isEmpty { - entries.insert(.tiles(tileItems, .pairs, configuration.videoParticipantsMaxCount, reachedLimit), at: 0) - } - } - - var canInvite = true - var inviteIsLink = false - if let peer = self.peer as? TelegramChannel { - if peer.flags.contains(.isGigagroup) { - if peer.flags.contains(.isCreator) || peer.adminRights != nil { - } else { - canInvite = false - } - } - if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) { - inviteIsLink = true - } - } - if canInvite { - entries.append(.invite(self.presentationData.theme, self.presentationData.strings, inviteIsLink ? self.presentationData.strings.VoiceChat_Share : self.presentationData.strings.VoiceChat_InviteMember, inviteIsLink)) - } - - let previousEntries = self.currentEntries - let previousFullscreenEntries = self.currentFullscreenEntries - self.currentEntries = entries - self.currentFullscreenEntries = fullscreenEntries - - if previousEntries.count == entries.count { - var allEqual = true - for i in 0 ..< previousEntries.count { - if previousEntries[i].stableId != entries[i].stableId { - if case let .peer(lhsPeer, _) = previousEntries[i], case let .peer(rhsPeer, _) = entries[i] { - if lhsPeer.isMyPeer != rhsPeer.isMyPeer { - allEqual = false - break - } - } else { - allEqual = false - break - } - } - } - if allEqual { - disableAnimation = true - } - } else if abs(previousEntries.count - entries.count) > 10 { - disableAnimation = true - } - - let presentationData = self.presentationData.withUpdated(theme: self.darkTheme) - let transition = self.preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: !disableAnimation, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!) - self.enqueueTransition(transition) - - let fullscreenTransition = self.preparedFullscreenTransition(from: previousFullscreenEntries, to: fullscreenEntries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: true, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!) - if !isTablet { - self.enqueueFullscreenTransition(fullscreenTransition) - } - - if speakingPeersUpdated { - var speakingPeers = speakingPeers - var updatedSpeakers: [PeerId] = [] - for peerId in self.currentSpeakers { - if speakingPeers.contains(peerId) { - updatedSpeakers.append(peerId) - speakingPeers.remove(peerId) - } - } - - var currentSpeakingSubtitle = "" - for peerId in Array(speakingPeers) { - updatedSpeakers.append(peerId) - if let peer = entryByPeerId[peerId]?.peer { - let displayName = speakingPeers.count == 1 ? EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) : EnginePeer(peer).compactDisplayTitle - if currentSpeakingSubtitle.isEmpty { - currentSpeakingSubtitle.append(displayName) - } else { - currentSpeakingSubtitle.append(", \(displayName)") - } - } - } - self.currentSpeakers = updatedSpeakers - self.currentSpeakingSubtitle = currentSpeakingSubtitle.isEmpty ? nil : currentSpeakingSubtitle - self.updateTitle(transition: .immediate) - } - - if case .fullscreen = self.displayMode, !self.mainStageNode.animating { - if speakingPeersUpdated { - self.mainStageNode.update(speakingPeerId: self.currentSpeakers.first) - } - } else { - self.mainStageNode.update(speakingPeerId: nil) - } - - if updateLayout, let (layout, navigationHeight) = self.validLayout { - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .easeInOut) - if animatingLayout { - self.animatingExpansion = true - } - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) - self.updateDecorationsLayout(transition: transition) - } - } - - private func callStateDidReset() { - self.requestedVideoSources.removeAll() - self.filterRequestedVideoChannels(channels: []) - self.updateRequestedVideoChannels() - } - - private func filterRequestedVideoChannels(channels: [PresentationGroupCallRequestedVideo]) { - var validSources = Set() - for channel in channels { - validSources.insert(channel.endpointId) - - if !self.requestedVideoSources.contains(channel.endpointId) { - self.requestedVideoSources.insert(channel.endpointId) - - let input = (self.call as! PresentationGroupCallImpl).video(endpointId: channel.endpointId) - if let input = input, let videoView = self.videoRenderingContext.makeView(input: input, blur: false) { - let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: self.videoRenderingContext.makeView(input: input, blur: true)) - - self.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue()))) - |> deliverOnMainQueue - ).start(next: { [weak self, weak videoNode] ready, timeouted in - if let strongSelf = self, let videoNode = videoNode { - Queue.mainQueue().after(0.1) { - if timeouted && !ready { - strongSelf.timeoutedEndpointIds.insert(channel.endpointId) - strongSelf.readyVideoEndpointIds.remove(channel.endpointId) - strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) - strongSelf.wideVideoNodes.remove(channel.endpointId) - - strongSelf.updateMembers() - } else if ready { - strongSelf.readyVideoEndpointIds.insert(channel.endpointId) - strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) - strongSelf.timeoutedEndpointIds.remove(channel.endpointId) - if videoNode.aspectRatio <= 0.77 { - strongSelf.wideVideoNodes.insert(channel.endpointId) - } else { - strongSelf.wideVideoNodes.remove(channel.endpointId) - } - strongSelf.updateMembers() - - if let (layout, _) = strongSelf.validLayout, case .compact = layout.metrics.widthClass { - if let interaction = strongSelf.itemInteraction { - loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count { - let entry = strongSelf.currentFullscreenEntries[i] - switch entry { - case let .peer(peerEntry, _): - if peerEntry.effectiveVideoEndpointId == channel.endpointId { - let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme) - strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) - break loop - } - default: - break - } - } - } - } - } - } - } - }), forKey: channel.endpointId) - self.videoNodes[channel.endpointId] = videoNode - - if let _ = self.validLayout { - self.updateMembers() - } - } - - /*self.call.makeIncomingVideoView(endpointId: channel.endpointId, requestClone: GroupVideoNode.useBlurTransparency, completion: { [weak self] videoView, backdropVideoView in - Queue.mainQueue().async { - guard let strongSelf = self, let videoView = videoView else { - return - } - let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView) - - strongSelf.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue()))) - |> deliverOnMainQueue - ).start(next: { [weak self, weak videoNode] ready, timeouted in - if let strongSelf = self, let videoNode = videoNode { - Queue.mainQueue().after(0.1) { - if timeouted && !ready { - strongSelf.timeoutedEndpointIds.insert(channel.endpointId) - strongSelf.readyVideoEndpointIds.remove(channel.endpointId) - strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) - strongSelf.wideVideoNodes.remove(channel.endpointId) - - strongSelf.updateMembers() - } else if ready { - strongSelf.readyVideoEndpointIds.insert(channel.endpointId) - strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) - strongSelf.timeoutedEndpointIds.remove(channel.endpointId) - if videoNode.aspectRatio <= 0.77 { - strongSelf.wideVideoNodes.insert(channel.endpointId) - } else { - strongSelf.wideVideoNodes.remove(channel.endpointId) - } - strongSelf.updateMembers() - - if let (layout, _) = strongSelf.validLayout, case .compact = layout.metrics.widthClass { - if let interaction = strongSelf.itemInteraction { - loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count { - let entry = strongSelf.currentFullscreenEntries[i] - switch entry { - case let .peer(peerEntry, _): - if peerEntry.effectiveVideoEndpointId == channel.endpointId { - let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme) - strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) - break loop - } - default: - break - } - } - } - } - } - } - } - }), forKey: channel.endpointId) - strongSelf.videoNodes[channel.endpointId] = videoNode - - if let _ = strongSelf.validLayout { - strongSelf.updateMembers() - } - } - })*/ - } - } - - var removeRequestedVideoSources: [String] = [] - for source in self.requestedVideoSources { - if !validSources.contains(source) { - removeRequestedVideoSources.append(source) - } - } - for source in removeRequestedVideoSources { - self.requestedVideoSources.remove(source) - } - - for (videoEndpointId, _) in self.videoNodes { - if !validSources.contains(videoEndpointId) { - self.videoNodes[videoEndpointId] = nil - self.videoOrder.removeAll(where: { $0 == videoEndpointId }) - self.readyVideoEndpointIds.remove(videoEndpointId) - self.readyVideoEndpointIdsPromise.set(self.readyVideoEndpointIds) - self.readyVideoDisposables.set(nil, forKey: videoEndpointId) - } - } - } - - private func updateMainVideo(waitForFullSize: Bool, entries: [ListEntry]? = nil, updateMembers: Bool = true, force: Bool = false, completion: (() -> Void)? = nil) { - let effectiveMainSpeaker = self.currentForcedSpeaker ?? self.currentDominantSpeaker.flatMap { ($0.0, $0.1) } - guard effectiveMainSpeaker?.0 != self.effectiveSpeaker?.0 || effectiveMainSpeaker?.1 != self.effectiveSpeaker?.1 || force else { - return - } - - let currentEntries = entries ?? self.currentFullscreenEntries - var effectiveSpeaker: (PeerId, String?, Bool, Bool, Bool)? = nil - var anySpeakerWithVideo: (PeerId, String?, Bool, Bool, Bool)? = nil - var anySpeaker: (PeerId, Bool)? = nil - if let (peerId, preferredVideoEndpointId) = effectiveMainSpeaker { - for entry in currentEntries { - switch entry { - case let .peer(peer, _): - if peer.peer.id == peerId { - if let preferredVideoEndpointId = preferredVideoEndpointId, peer.videoEndpointId == preferredVideoEndpointId || peer.presentationEndpointId == preferredVideoEndpointId { - var isPaused = false - if peer.presentationEndpointId != nil && preferredVideoEndpointId == peer.presentationEndpointId { - isPaused = peer.presentationPaused - } else if peer.videoEndpointId != nil && preferredVideoEndpointId == peer.videoEndpointId { - isPaused = peer.videoPaused - } - effectiveSpeaker = (peerId, preferredVideoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && preferredVideoEndpointId == peer.presentationEndpointId, isPaused) - } else { - var isPaused = false - if peer.effectiveVideoEndpointId != nil && peer.effectiveVideoEndpointId == peer.presentationEndpointId { - isPaused = peer.presentationPaused - } else if peer.effectiveVideoEndpointId != nil && peer.effectiveVideoEndpointId == peer.videoEndpointId { - isPaused = peer.videoPaused - } - effectiveSpeaker = (peerId, peer.effectiveVideoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && peer.effectiveVideoEndpointId == peer.presentationEndpointId, isPaused) - } - } else if anySpeakerWithVideo == nil, let videoEndpointId = peer.effectiveVideoEndpointId { - var isPaused = false - if videoEndpointId == peer.presentationEndpointId { - isPaused = peer.presentationPaused - } else if videoEndpointId == peer.videoEndpointId { - isPaused = peer.videoPaused - } - anySpeakerWithVideo = (peer.peer.id, videoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && videoEndpointId == peer.presentationEndpointId, isPaused) - } else if anySpeaker == nil { - anySpeaker = (peer.peer.id, peer.isMyPeer) - } - default: - break - } - } - } - - if effectiveSpeaker == nil { - self.currentForcedSpeaker = nil - effectiveSpeaker = anySpeakerWithVideo ?? anySpeaker.flatMap { ($0.0, nil, $0.1, false, false) } - if let (peerId, videoEndpointId, _, _, _) = effectiveSpeaker { - self.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) - } else { - self.currentDominantSpeaker = nil - } - } - - self.effectiveSpeaker = effectiveSpeaker - if updateMembers { - self.updateMembers(maybeUpdateVideo: false, force: force) - } - - var waitForFullSize = waitForFullSize - var isReady = false - if let (_, maybeVideoEndpointId, _, _, _) = effectiveSpeaker, let videoEndpointId = maybeVideoEndpointId { - isReady = true - if !self.readyVideoEndpointIds.contains(videoEndpointId) { - isReady = false - if entries == nil { - waitForFullSize = false - } - } - } - - self.mainStageNode.update(peer: effectiveSpeaker, isReady: isReady, waitForFullSize: waitForFullSize, completion: { - completion?() - }) - } - - private func updateRequestedVideoChannels() { - Queue.mainQueue().after(0.3) { - let enableVideo = self.appIsActive && self.visibility - - self.call.setRequestedVideoList(items: enableVideo ? self.requestedVideoChannels : []) - self.filterRequestedVideoChannels(channels: self.requestedVideoChannels) - } - } - - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer is UILongPressGestureRecognizer { - return !self.isScheduling - } else if gestureRecognizer is DirectionalPanGestureRecognizer { - if self.mainStageNode.animating || self.animatingMainStage { - return false - } - - let bottomPanelLocation = gestureRecognizer.location(in: self.bottomPanelNode.view) - let containerLocation = gestureRecognizer.location(in: self.contentContainer.view) - let mainStageLocation = gestureRecognizer.location(in: self.mainStageNode.view) - - if self.isLandscape && self.mainStageContainerNode.isUserInteractionEnabled && mainStageLocation.x > self.mainStageNode.frame.width - 80.0 { - return false - } - - if self.audioButton.frame.contains(bottomPanelLocation) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(bottomPanelLocation)) || self.leaveButton.frame.contains(bottomPanelLocation) || self.pickerView?.frame.contains(containerLocation) == true || (self.mainStageContainerNode.isUserInteractionEnabled && (mainStageLocation.y < 44.0 || mainStageLocation.y > self.mainStageNode.frame.height - 100.0)) { - return false - } - } - return true - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { - return true - } - return false - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - guard let (layout, _) = self.validLayout else { - return - } - let contentOffset = self.listNode.visibleContentOffset() - switch recognizer.state { - case .began: - let topInset: CGFloat - if case .regular = layout.metrics.widthClass { - topInset = 0.0 - } else if self.isExpanded { - topInset = 0.0 - } else if let currentTopInset = self.topInset { - topInset = currentTopInset - } else { - topInset = self.listNode.frame.height - } - self.panGestureArguments = (topInset, 0.0) - - self.controller?.dismissAllTooltips() - - if case .fullscreen = self.displayMode, case .compact = layout.metrics.widthClass { - self.isPanning = true - - self.mainStageBackgroundNode.alpha = 0.0 - self.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) - self.mainStageNode.setControlsHidden(true, animated: true) - - self.fullscreenListNode.alpha = 0.0 - self.fullscreenListNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self] finished in - self?.attachTileVideos() - - self?.fullscreenListContainer.subnodeTransform = CATransform3DIdentity - }) - - self.listContainer.transform = CATransform3DMakeScale(0.86, 0.86, 1.0) - - self.contentContainer.insertSubnode(self.mainStageContainerNode, aboveSubnode: self.bottomPanelNode) - } - case .changed: - var translation = recognizer.translation(in: self.contentContainer.view).y - if self.isScheduled && translation < 0.0 { - return - } - - let translateBounds: Bool - if case .regular = layout.metrics.widthClass { - translateBounds = true - } else { - switch self.displayMode { - case let .modal(isExpanded, previousIsFilled): - var topInset: CGFloat = 0.0 - if let (currentTopInset, currentPanOffset) = self.panGestureArguments { - topInset = currentTopInset - - if case let .known(value) = contentOffset, value <= 0.5 { - } else { - translation = currentPanOffset - if self.isExpanded { - recognizer.setTranslation(CGPoint(), in: self.contentContainer.view) - } - } - - self.panGestureArguments = (currentTopInset, translation) - } - - let currentOffset = topInset + translation - - var isFilled = previousIsFilled - if currentOffset < 20.0 { - isFilled = true - } else if currentOffset > 40.0 { - isFilled = false - } - if isFilled != previousIsFilled { - self.displayMode = .modal(isExpanded: isExpanded, isFilled: isFilled) - self.updateDecorationsColors() - } - - if self.isExpanded { - } else { - if currentOffset > 0.0 { - self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller) - } - } - case .fullscreen: - if abs(translation) > 32.0 { - if self.fullscreenListNode.layer.animationKeys()?.contains("opacity") == true { - self.fullscreenListNode.layer.removeAllAnimations() - } - } - var bounds = self.mainStageContainerNode.bounds - bounds.origin.y = -translation - self.mainStageContainerNode.bounds = bounds - - var backgroundFrame = self.mainStageNode.frame - backgroundFrame.origin.y += -translation - self.mainStageBackgroundNode.frame = backgroundFrame - - self.fullscreenListContainer.subnodeTransform = CATransform3DMakeTranslation(0.0, translation, 0.0) - } - - translateBounds = !self.isExpanded - } - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - self.updateDecorationsLayout(transition: .immediate) - } - - if translateBounds { - var bounds = self.contentContainer.bounds - bounds.origin.y = -translation - bounds.origin.y = min(0.0, bounds.origin.y) - self.contentContainer.bounds = bounds - } - case .ended: - let translation = recognizer.translation(in: self.contentContainer.view) - var velocity = recognizer.velocity(in: self.contentContainer.view) - - if self.isScheduled && (translation.y < 0.0 || velocity.y < 0.0) { - return - } - - if case let .known(value) = contentOffset, value > 0.0 { - velocity = CGPoint() - } else if case .unknown = contentOffset { - velocity = CGPoint() - } - - var bounds = self.contentContainer.bounds - bounds.origin.y = -translation.y - bounds.origin.y = min(0.0, bounds.origin.y) - - let offset: CGFloat - if let (inset, panOffset) = self.panGestureArguments { - offset = inset + panOffset - } else { - offset = 0.0 - } - - let topInset: CGFloat - if let currentTopInset = self.topInset { - topInset = currentTopInset - } else { - topInset = self.listNode.frame.height - } - - if case .fullscreen = self.displayMode, case .compact = layout.metrics.widthClass { - self.panGestureArguments = nil - self.fullscreenListContainer.subnodeTransform = CATransform3DIdentity - if abs(translation.y) > 100.0 || abs(velocity.y) > 300.0 { - self.mainStageBackgroundNode.layer.removeAllAnimations() - self.currentForcedSpeaker = nil - self.updateDisplayMode(.modal(isExpanded: true, isFilled: true), fromPan: true) - self.effectiveSpeaker = nil - } else { - self.isPanning = false - self.mainStageBackgroundNode.alpha = 1.0 - self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in - self?.attachFullscreenVideos() - }) - self.mainStageNode.setControlsHidden(false, animated: true, delay: 0.15) - - self.fullscreenListNode.alpha = 1.0 - self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.15) - - var bounds = self.mainStageContainerNode.bounds - let previousBounds = bounds - bounds.origin.y = 0.0 - self.mainStageContainerNode.bounds = bounds - self.mainStageContainerNode.layer.animateBounds(from: previousBounds, to: self.mainStageContainerNode.bounds, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.listContainer.transform = CATransform3DIdentity - strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode) - strongSelf.updateMembers() - } - }) - } - } else if case .modal(true, _) = self.displayMode, case .compact = layout.metrics.widthClass { - self.panGestureArguments = nil - if velocity.y > 300.0 || offset > topInset / 2.0 { - self.displayMode = .modal(isExpanded: false, isFilled: false) - self.updateDecorationsColors() - self.animatingExpansion = true - self.listNode.scroller.setContentOffset(CGPoint(), animated: false) - - let distance: CGFloat - if let topInset = self.topInset { - distance = topInset - offset - } else { - distance = 0.0 - } - let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) - let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) - } - self.updateDecorationsLayout(transition: transition, completion: { - self.animatingExpansion = false - }) - } else { - self.displayMode = .modal(isExpanded: true, isFilled: true) - self.updateDecorationsColors() - self.animatingExpansion = true - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: { - self.animatingExpansion = false - }) - } - } else { - self.panGestureArguments = nil - var dismissing = false - if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) { - if self.isScheduling { - self.dismissScheduled() - dismissing = true - } else if case .regular = layout.metrics.widthClass { - self.controller?.dismiss(closing: false, manual: true) - dismissing = true - } else { - if case .fullscreen = self.displayMode { - } else { - self.controller?.dismiss(closing: false, manual: true) - dismissing = true - } - } - } else if !self.isScheduling && (velocity.y < -300.0 || offset < topInset / 2.0) { - if velocity.y > -2200.0 && !self.isFullscreen { - DispatchQueue.main.async { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } - } - - let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) - let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - if case .modal = self.displayMode { - self.displayMode = .modal(isExpanded: true, isFilled: true) - } - self.updateDecorationsColors() - self.animatingExpansion = true - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) - } - self.updateDecorationsLayout(transition: transition, completion: { - self.animatingExpansion = false - }) - } else if !self.isScheduling { - self.updateDecorationsColors() - self.animatingExpansion = true - self.listNode.scroller.setContentOffset(CGPoint(), animated: false) - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: { - self.animatingExpansion = false - }) - } - if !dismissing { - var bounds = self.contentContainer.bounds - let previousBounds = bounds - bounds.origin.y = 0.0 - self.contentContainer.bounds = bounds - self.contentContainer.layer.animateBounds(from: previousBounds, to: self.contentContainer.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - } - } - case .cancelled: - self.panGestureArguments = nil - - let previousBounds = self.contentContainer.bounds - var bounds = self.contentContainer.bounds - bounds.origin.y = 0.0 - self.contentContainer.bounds = bounds - self.contentContainer.layer.animateBounds(from: previousBounds, to: self.contentContainer.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: { - self.animatingExpansion = false - }) - - if case .fullscreen = self.displayMode, case .regular = layout.metrics.widthClass { - self.fullscreenListContainer.subnodeTransform = CATransform3DIdentity - self.isPanning = false - self.mainStageBackgroundNode.alpha = 1.0 - self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in - self?.attachFullscreenVideos() - }) - self.mainStageNode.setControlsHidden(false, animated: true, delay: 0.15) - - self.fullscreenListNode.alpha = 1.0 - self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.15) - - var bounds = self.mainStageContainerNode.bounds - let previousBounds = bounds - bounds.origin.y = 0.0 - self.mainStageContainerNode.bounds = bounds - self.mainStageContainerNode.layer.animateBounds(from: previousBounds, to: self.mainStageContainerNode.bounds, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode) - strongSelf.updateMembers() - - strongSelf.listContainer.transform = CATransform3DIdentity - } - }) - } - default: - break - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - if result === self.topPanelNode.view { - return self.view - } - if result === self.bottomPanelNode.view { - return self.view - } - if !self.bounds.contains(point) { - return nil - } - if point.y < self.topPanelNode.frame.minY { - return self.dimNode.view - } - return result - } - - fileprivate func scrollToTop() { - if self.isExpanded { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } - } - - private func openTitleEditing() { - let _ = (self.context.account.postbox.loadedPeerWithId(self.call.peerId) - |> deliverOnMainQueue).start(next: { [weak self] chatPeer in - guard let strongSelf = self else { - return - } - - let initialTitle = strongSelf.callState?.title ?? "" - - let title: String - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - title = strongSelf.presentationData.strings.LiveStream_EditTitle - text = strongSelf.presentationData.strings.LiveStream_EditTitleText - } else { - title = strongSelf.presentationData.strings.VoiceChat_EditTitle - text = strongSelf.presentationData.strings.VoiceChat_EditTitleText - } - - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { title in - if let strongSelf = self, let title = title, title != initialTitle { - strongSelf.call.updateTitle(title) - - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = title.isEmpty ? strongSelf.presentationData.strings.LiveStream_EditTitleRemoveSuccess : strongSelf.presentationData.strings.LiveStream_EditTitleSuccess(title).string - } else { - text = title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).string - } - - strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false }) - } - }) - strongSelf.controller?.present(controller, in: .window(.root)) - }) - } - - private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { - guard let peerId = self.callState?.myPeerId else { - return - } - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Configuration.SearchBots() - ) - |> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in - guard let strongSelf = self, let peer = peer else { - return - } - - let presentationData = strongSelf.presentationData - - let legacyController = LegacyController(presentation: .custom, theme: strongSelf.darkTheme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - strongSelf.view.endEditing(true) - strongSelf.controller?.present(legacyController, in: .window(.root)) - - var hasPhotos = false - if !peer.profileImageRepresentations.isEmpty { - hasPhotos = true - } - - let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context) -// paintStickersContext.presentStickersController = { completion in -// let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in -// let coder = PostboxEncoder() -// coder.encodeRootObject(fileReference.media) -// completion?(coder.makeData(), fileReference.media.isAnimatedSticker, node.view, rect) -// return true -// }) -// strongSelf.controller?.present(controller, in: .window(.root)) -// return controller -// } - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! - mixin.forceDark = true - mixin.stickersContext = paintStickersContext - let _ = strongSelf.currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { [weak self] assetsController in - guard let strongSelf = self else { - return - } - let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in - assetsController?.dismiss() - self?.updateProfilePhoto(result) - })) - controller.navigationPresentation = .modal - strongSelf.controller?.push(controller) - - if fromGallery { - completion() - } - } - mixin.didFinishWithImage = { [weak self] image in - if let image = image { - completion() - self?.updateProfilePhoto(image) - } - } - mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in - if let image = image, let asset = asset { - completion() - self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) - } - } - mixin.didFinishWithDelete = { - guard let strongSelf = self else { - return - } - - let proceed = { - let _ = strongSelf.currentAvatarMixin.swap(nil) - let postbox = strongSelf.context.account.postbox - strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) - |> deliverOnMainQueue).start()) - } - - let actionSheet = ActionSheetController(presentationData: presentationData.withUpdated(theme: strongSelf.darkTheme)) - let items: [ActionSheetItem] = [ - ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - proceed() - }) - ] - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - strongSelf.controller?.present(actionSheet, in: .window(.root)) - } - mixin.didDismiss = { [weak legacyController] in - guard let strongSelf = self else { - return - } - let _ = strongSelf.currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } - } - }) - } - - private func updateProfilePhoto(_ image: UIImage) { - guard let data = image.jpegData(compressionQuality: 0.6), let peerId = self.callState?.myPeerId else { - return - } - - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) - - self.currentUpdatingAvatar = representation - self.updateAvatarPromise.set(.single((representation, 0.0))) - - let postbox = self.call.account.postbox - let signal = peerId.namespace == Namespaces.Peer.CloudUser ? self.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) : self.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: self.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) - - self.updateAvatarDisposable.set((signal - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - switch result { - case .complete: - strongSelf.updateAvatarPromise.set(.single(nil)) - case let .progress(value): - strongSelf.updateAvatarPromise.set(.single((representation, value))) - } - })) - - self.updateMembers() - } - - private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { - guard let data = image.jpegData(compressionQuality: 0.6), let peerId = self.callState?.myPeerId else { - return - } - - let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) - - self.currentUpdatingAvatar = representation - self.updateAvatarPromise.set(.single((representation, 0.0))) - - var videoStartTimestamp: Double? = nil - if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { - videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue - } - - let context = self.context - let account = self.context.account - let signal = Signal { [weak self] subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(account: account, adjustments: adjustments) - } else { - return nil - } - } - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else if let url = asset as? URL, let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, watcher: nil, entityRenderer: entityRenderer)! - } else { - return SSignal.single(nil) - } - }) - - } else { - signal = SSignal.complete() - } - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - } - - if let timestamp = videoStartTimestamp { - videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - } - account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - subscriber.putNext(resource) - } - } - subscriber.putCompletion() - } else if let strongSelf = self, let progress = next as? NSNumber { - Queue.mainQueue().async { - strongSelf.updateAvatarPromise.set(.single((representation, Float(truncating: progress) * 0.25))) - } - } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() - } - } - - self.updateAvatarDisposable.set((signal - |> mapToSignal { videoResource -> Signal in - if peerId.namespace == Namespaces.Peer.CloudUser { - return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) - }) - } else { - return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) - }) - } - } - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - switch result { - case .complete: - strongSelf.updateAvatarPromise.set(.single(nil)) - case let .progress(value): - strongSelf.updateAvatarPromise.set(.single((representation, 0.25 + value * 0.75))) - } - })) - } - - private func displayUnmuteTooltip() { - guard let (layout, _) = self.validLayout else { - return - } - let location = self.actionButton.view.convert(self.actionButton.bounds, to: self.view).center - var point = CGRect(origin: CGPoint(x: location.x - 5.0, y: location.y - 5.0 - 68.0), size: CGSize(width: 10.0, height: 10.0)) - var position: TooltipScreen.ArrowPosition = .bottom - if case .compact = layout.metrics.widthClass { - if self.isLandscape { - point.origin.x = location.x - 5.0 - 36.0 - point.origin.y = location.y - 5.0 - position = .right - } else if case .fullscreen = self.displayMode { - point.origin.y += 32.0 - } - } - self.controller?.present(TooltipScreen(account: self.context.account, text: self.presentationData.strings.VoiceChat_UnmuteSuggestion, style: .gradient(UIColor(rgb: 0x1d446c), UIColor(rgb: 0x193e63)), icon: nil, location: .point(point, position), displayDuration: .custom(8.0), shouldDismissOnTouch: { _ in - return .dismiss(consume: false) - }), in: .window(.root)) - } - - private var isScheduled: Bool { - return self.isScheduling || self.callState?.scheduleTimestamp != nil - } - - private func attachFullscreenVideos() { - guard let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass else { - return - } - var verticalItemNodes: [String: ASDisplayNode] = [:] - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatTilesGridItemNode { - for tileNode in itemNode.tileNodes { - if let item = tileNode.item { - verticalItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] = tileNode - } - - if tileNode.item?.peer.id == self.effectiveSpeaker?.0 && tileNode.item?.videoEndpointId == self.effectiveSpeaker?.1 { - tileNode.isHidden = false - } - } - } - } - - self.fullscreenListNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { - let otherItemNode = verticalItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] - itemNode.transitionIn(from: otherItemNode) - } - } - } - - private func attachTileVideos() { - var fullscreenItemNodes: [String: VoiceChatFullscreenParticipantItemNode] = [:] - var tileNodes: [VoiceChatTileItemNode] = [] - if !self.tileGridNode.isHidden { - tileNodes = self.tileGridNode.tileNodes - } else { - self.fullscreenListNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { - fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] = itemNode - } - } - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatTilesGridItemNode { - tileNodes = itemNode.tileNodes - } - } - } - - for tileNode in tileNodes { - if let item = tileNode.item { - let otherItemNode = fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] - tileNode.transitionIn(from: otherItemNode) - - if tileNode.item?.peer.id == self.effectiveSpeaker?.0 && tileNode.item?.videoEndpointId == self.effectiveSpeaker?.1 { - tileNode.isHidden = true - } - } - } - } - - private func updateDisplayMode(_ displayMode: DisplayMode, fromPan: Bool = false) { - guard !self.animatingExpansion && !self.animatingMainStage && !self.mainStageNode.animating else { - return - } - self.updateMembers() - - let previousDisplayMode = self.displayMode - var isFullscreen = false - if case .fullscreen = displayMode { - isFullscreen = true - } - - if case .fullscreen = previousDisplayMode, case .fullscreen = displayMode { - self.animatingExpansion = true - } else { - self.animatingMainStage = true - } - - var hasFullscreenList = false - if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass { - hasFullscreenList = true - } - - let completion = { - self.displayMode = displayMode - self.updateDecorationsColors() - - self.mainStageContainerNode.isHidden = false - self.mainStageContainerNode.isUserInteractionEnabled = isFullscreen - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.55, curve: .spring) - if case .modal = previousDisplayMode, case .fullscreen = self.displayMode { - self.mainStageNode.alpha = 0.0 - self.updateDecorationsLayout(transition: .immediate) - - var verticalItemNodes: [String: ASDisplayNode] = [:] - - var tileNodes: [VoiceChatTileItemNode] = [] - if !self.tileGridNode.isHidden { - tileNodes = self.tileGridNode.tileNodes - } else { - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatTilesGridItemNode { - tileNodes = itemNode.tileNodes - } - } - } - for tileNode in tileNodes { - if let item = tileNode.item { - verticalItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] = tileNode - } - } - - let completion = { - let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 - - if hasFullscreenList { - self.fullscreenListContainer.isHidden = false - self.fullscreenListNode.alpha = 0.0 - } - - var gridSnapshotView: UIView? - if !hasFullscreenList, let snapshotView = self.tileGridNode.view.snapshotView(afterScreenUpdates: false) { - gridSnapshotView = snapshotView - self.tileGridNode.view.addSubview(snapshotView) - self.displayPanelVideos = true - self.updateMembers(maybeUpdateVideo: false, force: true) - } - - let completion = { - if hasFullscreenList { - self.attachFullscreenVideos() - - self.fullscreenListNode.alpha = 1.0 - self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - } - if let effectiveSpeakerPeerId = effectiveSpeakerPeerId, let otherItemNode = verticalItemNodes[String(effectiveSpeakerPeerId.toInt64()) + "_" + (self.effectiveSpeaker?.1 ?? "")] { - - if hasFullscreenList { - let transitionStartPosition = otherItemNode.view.convert(CGPoint(x: otherItemNode.frame.width / 2.0, y: otherItemNode.frame.height), to: self.fullscreenListContainer.view.superview) - self.fullscreenListContainer.layer.animatePosition(from: transitionStartPosition, to: self.fullscreenListContainer.position, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) - } - - self.mainStageNode.animateTransitionIn(from: otherItemNode, transition: transition, completion: { [weak self] in - self?.animatingMainStage = false - }) - self.mainStageNode.alpha = 1.0 - - self.mainStageBackgroundNode.alpha = 1.0 - self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: hasFullscreenList ? 0.13 : 0.3, completion: { [weak otherItemNode] _ in - otherItemNode?.alpha = 0.0 - gridSnapshotView?.removeFromSuperview() - completion() - }) - } else { - completion() - } - - if hasFullscreenList { - self.listContainer.layer.animateScale(from: 1.0, to: 0.86, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) - } - - if self.isLandscape { - self.transitionMaskTopFillLayer.opacity = 1.0 - } - self.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in - Queue.mainQueue().after(0.3) { - self?.transitionMaskTopFillLayer.opacity = 0.0 - self?.transitionMaskBottomFillLayer.removeAllAnimations() - } - }) - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) - self.updateDecorationsLayout(transition: transition) - } - } - let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 - var index = 0 - for item in self.currentFullscreenEntries { - if case let .peer(entry, _) = item, entry.peer.id == effectiveSpeakerPeerId { - break - } else { - index += 1 - } - } - let position: ListViewScrollPosition - if index > self.currentFullscreenEntries.count - 3 { - index = self.currentFullscreenEntries.count - 1 - position = .bottom(0.0) - } else { - position = .center(.bottom) - } - self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: position, animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in - completion() - }) - } else if case .fullscreen = previousDisplayMode, case .modal = self.displayMode { - var minimalVisiblePeerid: (PeerId, CGFloat)? - var fullscreenItemNodes: [String: VoiceChatFullscreenParticipantItemNode] = [:] - self.fullscreenListNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { - let convertedFrame = itemNode.view.convert(itemNode.bounds, to: self.transitionContainerNode.view) - if let (_, x) = minimalVisiblePeerid { - if convertedFrame.minX >= 0.0 && convertedFrame.minX < x { - minimalVisiblePeerid = (item.peer.id, convertedFrame.minX) - } - } else if convertedFrame.minX >= 0.0 { - minimalVisiblePeerid = (item.peer.id, convertedFrame.minX) - } - fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] = itemNode - } - } - - let completion = { - let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 - var targetTileNode: VoiceChatTileItemNode? - - self.transitionContainerNode.addSubnode(self.mainStageNode) - - self.listContainer.transform = CATransform3DIdentity - - var tileNodes: [VoiceChatTileItemNode] = [] - if !self.tileGridNode.isHidden { - tileNodes = self.tileGridNode.tileNodes - } else { - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatTilesGridItemNode { - tileNodes = itemNode.tileNodes - } - } - } - for tileNode in tileNodes { - if let item = tileNode.item { - if item.peer.id == effectiveSpeakerPeerId, item.videoEndpointId == self.effectiveSpeaker?.1 { - targetTileNode = tileNode - } - } - } - - var transitionOffset = -self.mainStageContainerNode.bounds.minY - if transitionOffset.isZero, let (layout, _) = self.validLayout { - if case .regular = layout.metrics.widthClass { - transitionOffset += 87.0 - } - if let targetTileNode = targetTileNode { - let transitionTargetPosition = targetTileNode.view.convert(CGPoint(x: targetTileNode.frame.width / 2.0, y: targetTileNode.frame.height), to: self.fullscreenListContainer.view.superview) - self.fullscreenListContainer.layer.animatePosition(from: self.fullscreenListContainer.position, to: transitionTargetPosition, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) - } - - if !hasFullscreenList { - self.displayPanelVideos = false - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatTilesGridItemNode { - itemNode.snapshotForDismissal() - } - } - self.updateMembers(maybeUpdateVideo: false, force: true) - self.attachTileVideos() - - self.mainStageBackgroundNode.alpha = 0.0 - self.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) - } else { - self.fullscreenListNode.alpha = 0.0 - self.mainStageBackgroundNode.alpha = 1.0 - self.fullscreenListNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.fullscreenListContainer.isHidden = true - strongSelf.fullscreenListNode.alpha = 1.0 - strongSelf.attachTileVideos() - - strongSelf.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) - strongSelf.mainStageBackgroundNode.alpha = 0.0 - } - }) - } - } - self.mainStageNode.animateTransitionOut(to: targetTileNode, offset: transitionOffset, transition: transition, completion: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.effectiveSpeaker = nil - strongSelf.mainStageNode.update(peer: nil, waitForFullSize: false) - strongSelf.mainStageNode.setControlsHidden(false, animated: false) - strongSelf.fullscreenListContainer.isHidden = true - strongSelf.mainStageContainerNode.isHidden = true - strongSelf.mainStageContainerNode.addSubnode(strongSelf.mainStageNode) - - var bounds = strongSelf.mainStageContainerNode.bounds - bounds.origin.y = 0.0 - strongSelf.mainStageContainerNode.bounds = bounds - - strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode) - - strongSelf.isPanning = false - strongSelf.animatingMainStage = false - }) - - if hasFullscreenList { - self.listContainer.layer.animateScale(from: 0.86, to: 1.0, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) - } - - self.transitionMaskTopFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - if !transitionOffset.isZero { - self.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) - self.updateDecorationsLayout(transition: transition) - } - } - if false, let (peerId, _) = minimalVisiblePeerid { - var index = 0 - for item in self.currentEntries { - if case let .peer(entry, _) = item, entry.peer.id == peerId { - break - } else { - index += 1 - } - } - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in - completion() - }) - } else { - completion() - } - } else if case .fullscreen = self.displayMode { - if let (layout, navigationHeight) = self.validLayout { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) - self.updateDecorationsLayout(transition: transition) - } - } - } - - if case .fullscreen(false) = displayMode, case .modal = previousDisplayMode { - self.updateMainVideo(waitForFullSize: true, updateMembers: true, force: true, completion: { - completion() - }) - } else { - completion() - } - } - - fileprivate var actionButtonPosition: CGPoint { - guard let (layout, _) = self.validLayout else { - return CGPoint() - } - let size = layout.size - let hasCameraButton = self.cameraButton.isUserInteractionEnabled - let centralButtonSide = min(size.width, size.height) - 32.0 - let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide) - - if case .regular = layout.metrics.widthClass { - let contentWidth: CGFloat = max(320.0, min(375.0, floor(size.width * 0.3))) - let contentLeftInset: CGFloat - if self.peerIdToEndpointId.isEmpty { - contentLeftInset = floorToScreenPixels((layout.size.width - contentWidth) / 2.0) - } else { - contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth - } - return CGPoint(x: contentLeftInset + floorToScreenPixels(contentWidth / 2.0), y: layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + floor(self.effectiveBottomAreaHeight / 2.0) - 3.0) - } else { - switch self.displayMode { - case .modal: - if self.isLandscape { - let sideInset: CGFloat - let buttonsCount: Int - if hasCameraButton { - sideInset = 26.0 - buttonsCount = 4 - } else { - sideInset = 42.0 - buttonsCount = 3 - } - let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) - let x = layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right + floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) - let actionButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) - return actionButtonFrame.center - } else { - let actionButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) - return actionButtonFrame.center - } - case let .fullscreen(controlsHidden): - if self.isLandscape { - let sideInset: CGFloat - let buttonsCount: Int - if hasCameraButton { - sideInset = 26.0 - buttonsCount = 4 - } else { - sideInset = 42.0 - buttonsCount = 3 - } - let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) - let x = layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right + (controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0)) - let actionButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) - return actionButtonFrame.center - } else { - let sideInset: CGFloat - let buttonsCount: Int - if hasCameraButton { - sideInset = 26.0 - buttonsCount = 4 - } else { - sideInset = 42.0 - buttonsCount = 3 - } - let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) - let y = layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + (controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)) - let secondButtonFrame: CGRect - if hasCameraButton { - let firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) - secondButtonFrame = CGRect(origin: CGPoint(x: firstButtonFrame.maxX + spacing, y: y), size: sideButtonSize) - } else { - secondButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) - } - let actionButtonFrame = CGRect(origin: CGPoint(x: secondButtonFrame.maxX + spacing, y: y), size: sideButtonSize) - return actionButtonFrame.center - } - } - } - } - } - - private let sharedContext: SharedAccountContext - public let call: PresentationGroupCall - private let presentationData: PresentationData - public var parentNavigationController: NavigationController? - - fileprivate let contentsReady = ValuePromise(false, ignoreRepeated: true) - fileprivate let dataReady = ValuePromise(false, ignoreRepeated: true) - fileprivate let audioOutputStateReady = ValuePromise(false, ignoreRepeated: true) - private let _ready = Promise(false) - override public var ready: Promise { - return self._ready - } - - public var onViewDidAppear: (() -> Void)? - public var onViewDidDisappear: (() -> Void)? - private var reclaimActionButton: (() -> Void)? - - private var didAppearOnce: Bool = false - private var isDismissed: Bool = false - private var isDisconnected: Bool = false - - private var controllerNode: Node { - return self.displayNode as! Node - } - - private let idleTimerExtensionDisposable = MetaDisposable() - - public weak var currentOverlayController: VoiceChatOverlayController? - - private var validLayout: ContainerViewLayout? - - public init(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) { - self.sharedContext = sharedContext - self.call = call - self.presentationData = sharedContext.currentPresentationData.with { $0 } - - super.init(navigationBarPresentationData: nil) - - self.automaticallyControlPresentationContextLayout = false - self.blocksBackgroundWhenInOverlay = true - - self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) - - self.statusBar.statusBarStyle = .Ignore - - self._ready.set(combineLatest([ - self.contentsReady.get(), - self.dataReady.get(), - self.audioOutputStateReady.get() - ]) - |> map { values -> Bool in - for value in values { - if !value { - return false - } - } - return true - } - |> filter { $0 }) - - self.scrollToTop = { [weak self] in - self?.controllerNode.scrollToTop() - } - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.idleTimerExtensionDisposable.dispose() - - if let currentOverlayController = self.currentOverlayController { - currentOverlayController.animateOut(reclaim: false, targetPosition: CGPoint(), completion: { _ in }) - } - } - - override public func loadDisplayNode() { - self.displayNode = Node(controller: self, sharedContext: self.sharedContext, call: self.call) - - self.displayNodeDidLoad() - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - self.isDismissed = false - - if !self.didAppearOnce { - self.didAppearOnce = true - - self.reclaimActionButton?() - self.controllerNode.animateIn() - - self.idleTimerExtensionDisposable.set(self.sharedContext.applicationBindings.pushIdleTimerExtension()) - } - - DispatchQueue.main.async { - self.onViewDidAppear?() - } - } - - override public func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - self.idleTimerExtensionDisposable.set(nil) - - DispatchQueue.main.async { - self.didAppearOnce = false - self.isDismissed = true - self.detachActionButton() - self.onViewDidDisappear?() - } - } - - private var dismissedManually: Bool = false - public func dismiss(closing: Bool, manual: Bool = false) { - if closing { - self.isDisconnected = true - } else { - if let navigationController = self.navigationController as? NavigationController { - let count = navigationController.viewControllers.count - if count == 2 || navigationController.viewControllers[count - 2] is ChatController { - if case .active(.cantSpeak) = self.controllerNode.actionButton.stateValue { - } else if case .button = self.controllerNode.actionButton.stateValue { - } else if case .scheduled = self.controllerNode.actionButton.stateValue { - } else if let chatController = navigationController.viewControllers[count - 2] as? ChatController, chatController.isSendButtonVisible { - } else if let tabBarController = navigationController.viewControllers[count - 2] as? TabBarController, let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive { - } else { - if manual { - self.dismissedManually = true - Queue.mainQueue().after(0.05) { - self.detachActionButton() - } - } else { - self.detachActionButton() - } - } - } - } - } - - self.dismiss() - } - - private func dismissAllTooltips() { - self.window?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - }) - self.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - if let controller = controller as? TooltipScreen { - controller.dismiss() - } - return true - }) - } - - private func detachActionButton() { - guard self.currentOverlayController == nil && !self.isDisconnected else { - return - } - - let overlayController = VoiceChatOverlayController(actionButton: self.controllerNode.actionButton, audioOutputNode: self.controllerNode.audioButton, cameraNode: self.controllerNode.cameraButton, leaveNode: self.controllerNode.leaveButton, navigationController: self.navigationController as? NavigationController, initiallyHidden: self.dismissedManually) - if let navigationController = self.navigationController as? NavigationController { - navigationController.presentOverlay(controller: overlayController, inGlobal: true, blockInteraction: false) - } - - self.currentOverlayController = overlayController - self.dismissedManually = false - - self.reclaimActionButton = { [weak self, weak overlayController] in - if let strongSelf = self { - overlayController?.animateOut(reclaim: true, targetPosition: strongSelf.controllerNode.actionButtonPosition, completion: { immediate in - if let strongSelf = self, immediate { - strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = true - strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.cameraButton) - strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.audioButton) - strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.leaveButton) - strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.actionButton) - - if immediate, let layout = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, transition: .immediate) - } - strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = false - } - }) - strongSelf.reclaimActionButton = nil - } - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - if !self.isDismissed { - self.isDismissed = true - self.didAppearOnce = false - - self.controllerNode.animateOut(completion: { [weak self] in - completion?() - self?.dismiss(animated: false) - }) - - DispatchQueue.main.async { - self.onViewDidDisappear?() - } - } - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - self.validLayout = layout - self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } -} - -private final class VoiceChatContextExtractedContentSource: ContextExtractedContentSource { - var keepInPlace: Bool - let ignoreContentTouches: Bool = false - let blurBackground: Bool - let maskView: UIView? - - private var animateTransitionIn: () -> Void - private var animateTransitionOut: () -> Void - - private let sourceNode: ContextExtractedContentContainingNode - - var centerVertically: Bool - var shouldBeDismissed: Signal - - init(sourceNode: ContextExtractedContentContainingNode, maskView: UIView?, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal, animateTransitionIn: @escaping () -> Void, animateTransitionOut: @escaping () -> Void) { - self.sourceNode = sourceNode - self.maskView = maskView - self.keepInPlace = keepInPlace - self.blurBackground = blurBackground - self.centerVertically = centerVertically - self.shouldBeDismissed = shouldBeDismissed - self.animateTransitionIn = animateTransitionIn - self.animateTransitionOut = animateTransitionOut - } - - func takeView() -> ContextControllerTakeViewInfo? { - self.animateTransitionIn() - return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds, maskView: self.maskView) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - self.animateTransitionOut() - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds, maskView: self.maskView) - } -} - -private final class VoiceChatContextReferenceContentSource: ContextReferenceContentSource { - private let controller: ViewController - private let sourceNode: ContextReferenceContentNode - - init(controller: ViewController, sourceNode: ContextReferenceContentNode) { - self.controller = controller - self.sourceNode = sourceNode - } - - func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) - } -} diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 19544e5667..79f576f190 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -665,8 +665,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { if call.isStream { strongSelf.hasGroupCallOnScreenPromise.set(true) - // TODO: remove sharedContext and accountContext from init - let groupCallController = _MediaStreamComponentController(sharedContext: strongSelf, accountContext: call.accountContext, call: call) // MediaStreamComponentController(call: call)ue + let groupCallController = MediaStreamComponentController(call: call) groupCallController.onViewDidAppear = { [weak self] in if let strongSelf = self { strongSelf.hasGroupCallOnScreenPromise.set(true)