From 2f9f07d8604edffb4a44a392c86dad82e1bc3cb0 Mon Sep 17 00:00:00 2001 From: Ilya Yelagov Date: Sat, 26 Nov 2022 13:22:38 +0400 Subject: [PATCH] Adding components, proper animations and info, video glowing and basic fullscreen --- .../Components/MediaStreamComponent.swift | 374 ++++++++++++------ .../MediaStreamVideoComponent.swift | 97 ++++- .../Components/StreamSheetComponent.swift | 286 ++++++++++++++ .../Sources/VoiceChatController.swift | 10 +- .../Sources/SharedAccountContext.swift | 3 +- 5 files changed, 622 insertions(+), 148 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index cb9049b7b1..fb8ed57d3e 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -106,10 +106,12 @@ final class NavigationBackButtonComponent: Component { final class StreamTitleComponent: Component { let text: String let isRecording: Bool + let isActive: Bool - init(text: String, isRecording: Bool) { + init(text: String, isRecording: Bool, isActive: Bool) { self.text = text self.isRecording = isRecording + self.isActive = isActive } static func ==(lhs: StreamTitleComponent, rhs: StreamTitleComponent) -> Bool { @@ -119,12 +121,66 @@ final class StreamTitleComponent: Component { if lhs.isRecording != rhs.isRecording { return false } + if lhs.isActive != rhs.isActive { + return false + } return false } + final class LiveIndicatorView: UIView { + private let label = UILabel() + private let stalledAnimatedGradient = CAGradientLayer() + private var wasLive = false + + override init(frame: CGRect = .zero) { + super.init(frame: frame) + + addSubview(label) + label.text = "LIVE" + label.font = .systemFont(ofSize: 10, weight: .medium) + label.textAlignment = .center + layer.addSublayer(stalledAnimatedGradient) + self.clipsToBounds = true + toggle(isLive: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + label.frame = bounds + stalledAnimatedGradient.frame = bounds + self.layer.cornerRadius = min(bounds.width, bounds.height) / 2 + } + + func toggle(isLive: Bool) { + // TODO: get actual colors + if isLive { + if !wasLive { + // TODO: animate + } + self.backgroundColor = .systemPink + stalledAnimatedGradient.opacity = 0 + stalledAnimatedGradient.removeAllAnimations() + } else { + if wasLive { + // TODO: animate + } + self.backgroundColor = .gray + stalledAnimatedGradient.opacity = 1 +// stalledAnimatedGradient.add(<#T##anim: CAAnimation##CAAnimation#>, forKey: <#T##String?#>) + } + wasLive = isLive + } + } + public final class View: UIView { private let textView: ComponentHostView private var indicatorView: UIImageView? + let liveIndicatorView = LiveIndicatorView() private let trackingLayer: HierarchyTrackingLayer @@ -136,6 +192,7 @@ final class StreamTitleComponent: Component { super.init(frame: frame) self.addSubview(self.textView) + self.addSubview(self.liveIndicatorView) self.trackingLayer.didEnterHierarchy = { [weak self] in guard let strongSelf = self else { @@ -190,14 +247,16 @@ final class StreamTitleComponent: Component { indicatorView.removeFromSuperview() } } - + liveIndicatorView.toggle(isLive: component.isActive) let sideInset: CGFloat = 20.0 let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height) let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize) self.textView.frame = textFrame + liveIndicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + 1.0), size: .init(width: 40, height: 18)) + if let indicatorView = self.indicatorView, let image = indicatorView.image { - indicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: floorToScreenPixels((size.height - image.size.height) / 2.0) + 1.0), size: image.size) + indicatorView.frame = CGRect(origin: CGPoint(x: liveIndicatorView.frame.maxX + 6.0, y: floorToScreenPixels((size.height - image.size.height) / 2.0) + 1.0), size: image.size) } return size @@ -227,7 +286,7 @@ private final class NavigationBarComponent: CombinedComponent { rightItems: [AnyComponentWithIdentity], centerItem: AnyComponent? ) { - self.topInset = topInset + self.topInset = 0 // topInset self.sideInset = sideInset self.leftItem = leftItem self.rightItems = rightItems @@ -255,7 +314,6 @@ private final class NavigationBarComponent: CombinedComponent { } static var body: Body { - let background = Child(Rectangle.self) let leftItem = Child(environment: Empty.self) let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let centerItem = Child(environment: Empty.self) @@ -267,8 +325,6 @@ private final class NavigationBarComponent: CombinedComponent { let contentHeight: CGFloat = 44.0 let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight) - let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition) - let leftItem = context.component.leftItem.flatMap { leftItemComponent in return leftItem.update( component: leftItemComponent, @@ -302,10 +358,6 @@ private final class NavigationBarComponent: CombinedComponent { availableWidth -= centerItem.size.width } - context.add(background - .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) - ) - var centerLeftInset = sideInset if let leftItem = leftItem { context.add(leftItem @@ -450,7 +502,7 @@ private final class ToolbarComponent: CombinedComponent { let contentHeight: CGFloat = 44.0 let size = CGSize(width: context.availableSize.width, height: contentHeight + context.component.bottomInset) - let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition) + let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition) let leftItem = context.component.leftItem.flatMap { leftItemComponent in return leftItem.update( @@ -550,8 +602,10 @@ public final class _MediaStreamComponent: CombinedComponent { private(set) var displayUI: Bool = true var dismissOffset: CGFloat = 0.0 - + // TODO: remove (replaced by isFullscreen) var storedIsLandscape: Bool? + var isFullscreen: Bool = false + var videoSize: CGSize? private(set) var canManageCall: Bool = false let isPictureInPictureSupported: Bool @@ -569,8 +623,9 @@ public final class _MediaStreamComponent: CombinedComponent { let deactivatePictureInPictureIfVisible = StoredActionSlot(Void.self) - // MARK: - Added var videoHiddenForPip = false + /// To update videoHiddenForPip + var onExpandedFromPictureInPicture: ((State) -> Void)? init(call: PresentationGroupCallImpl) { self.call = call @@ -711,8 +766,8 @@ public final class _MediaStreamComponent: CombinedComponent { public static var body: Body { let background = Child(Rectangle.self) let video = Child(MediaStreamVideoComponent.self) - let navigationBar = Child(NavigationBarComponent.self) - let toolbar = Child(ToolbarComponent.self) +// let navigationBar = Child(NavigationBarComponent.self) +// let toolbar = Child(ToolbarComponent.self) let sheet = Child(StreamSheetComponent.self) @@ -721,6 +776,8 @@ public final class _MediaStreamComponent: CombinedComponent { let moreButtonTag = GenericComponentViewTag() let moreAnimationTag = GenericComponentViewTag() + var debugUpdate = true + var lastVideoPos: CGFloat = 0 return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value if environment.isVisible { @@ -737,7 +794,10 @@ public final class _MediaStreamComponent: CombinedComponent { 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 @@ -749,7 +809,7 @@ public final class _MediaStreamComponent: CombinedComponent { state.updated(transition: .easeInOut(duration: 3)) deactivatePictureInPicture.invoke(Void()) } - + let isFullscreen = state.isFullscreen let video = video.update( component: MediaStreamVideoComponent( call: context.component.call, @@ -757,6 +817,9 @@ public final class _MediaStreamComponent: CombinedComponent { isVisible: environment.isVisible && context.state.isVisibleInHierarchy, isAdmin: context.state.canManageCall, peerTitle: context.state.peerTitle, + // TODO: find out how to get image + peerImage: nil, + isFullscreen: isFullscreen, activatePictureInPicture: activatePictureInPicture, deactivatePictureInPicture: deactivatePictureInPicture, bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in @@ -771,28 +834,19 @@ public final class _MediaStreamComponent: CombinedComponent { }, pictureInPictureClosed: { [weak call] in let _ = call?.leave(terminateIfPossible: false) + }, + onVideoSizeRetrieved: { [weak state] size in + state?.videoSize = size } ), availableSize: context.availableSize, transition: context.transition - ) - - let sheet = sheet.update( - component: StreamSheetComponent( - leftItem: 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) - }) - ) - ), - availableSize: context.availableSize, - transition: context.transition - ) + )// .opacity(state.videoHiddenForPip ? 0 : 1) +// let height = context.availableSize.height var navigationRightItems: [AnyComponentWithIdentity] = [] - let contextView = context.view - if /*true || context.state.isPictureInPictureSupported,*/ context.state.hasVideo { +// let contextView = context.view + if context.state.isPictureInPictureSupported, context.state.hasVideo { navigationRightItems.append(AnyComponentWithIdentity(id: "pip", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Media Gallery/PictureInPictureButton", @@ -804,22 +858,17 @@ public final class _MediaStreamComponent: CombinedComponent { return } state.videoHiddenForPip = true - UIView.animate(withDuration: 5, animations: { - contextView.alpha = 0 - }, completion: { _ in - state.videoHiddenForPip = true - state.updateDismissOffset(value: 2000, interactive: false) - controller.dismiss(closing: false, manual: true) - contextView.alpha = 1 - }) + + 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(AnyComponentWithIdentity(id: "more", component: AnyComponent(Button( + /*navigationRightItems.append(*/ topLeftButton = //AnyComponentWithIdentity(id: "more", component: + AnyComponent(Button( content: AnyComponent(ZStack([ AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( strokeColor: .white, @@ -1048,26 +1097,27 @@ public final class _MediaStreamComponent: CombinedComponent { }*/ controller.presentInGlobalOverlay(contextController) } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(moreButtonTag)))) + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(moreButtonTag))//)//) } - - let navigationBar = navigationBar.update( - component: NavigationBarComponent( - topInset: environment.statusBarHeight, - sideInset: environment.safeInsets.left, - leftItem: 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: environment.strings.VoiceChatChannel_Title, isRecording: state.recordingStartTimestamp != nil)) - ), - availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), - transition: context.transition + 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: call.hasVideo)) ) +// let navigationBar = navigationBar.update( +// component: navigationComponent, +// availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), +// transition: context.transition +// ) + let isLandscape = context.availableSize.width > context.availableSize.height if context.state.storedIsLandscape != isLandscape { context.state.storedIsLandscape = isLandscape @@ -1092,40 +1142,49 @@ public final class _MediaStreamComponent: CombinedComponent { )) } - let toolbar = toolbar.update( - component: 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() + let toolbar = 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 } - ).minSize(CGSize(width: 44.0, height: 44.0))), - rightItem: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: isLandscape ? "Media Gallery/Minimize" : "Media Gallery/Fullscreen", - tintColor: .white - )), - action: { - if let controller = controller() as? MediaStreamComponentController { - controller.updateOrientation(orientation: isLandscape ? .portrait : .landscapeRight) + controller.presentShare() + } + ).minSize(CGSize(width: 44.0, height: 44.0))), + // TODO: disable button instead of hiding + rightItem: state.hasVideo ? AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: isLandscape ? "Media Gallery/Minimize" : "Media Gallery/Fullscreen", + tintColor: .white + )), + action: { + 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) } +// controller.updateOrientation(orientation: isLandscape ? .portrait : .landscapeRight) } - ).minSize(CGSize(width: 44.0, height: 44.0))), - centerItem: infoItem - ), - availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), - transition: context.transition + } + ).minSize(CGSize(width: 44.0, height: 44.0))) : nil, + centerItem: infoItem ) - let height = context.availableSize.height context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .gesture(.tap { [weak state] in @@ -1145,41 +1204,82 @@ public final class _MediaStreamComponent: CombinedComponent { state.updateDismissOffset(value: offset.y, interactive: true) case let .ended(velocity): // TODO: Dismiss sheet depending on velocity - if abs(velocity.y) > 200.0 { - activatePictureInPicture.invoke(Action { [weak state] in + if velocity.y > 200.0 { + 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 { state.updateDismissOffset(value: 0.0, interactive: false) } } }) ) + let videoHeight: CGFloat = context.availableSize.width / 16 * 9 + let sheetHeight: CGFloat = (50 + 70 + 20 + 80 + videoHeight) + let isFullyDragged = context.availableSize.height - sheetHeight + state.dismissOffset < 30 - let sheetHeight: CGFloat = 300 + let sheet = sheet.update( + component: StreamSheetComponent( + topComponent: AnyComponent(navigationComponent), + bottomButtonsRow: AnyComponent(toolbar), + topOffset: context.availableSize.height - sheetHeight + context.state.dismissOffset, + sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight), + backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor) + ), + availableSize: context.availableSize, + transition: context.transition + ) + // TODO: calculate (although not necessary currently) let sheetOffset: CGFloat = context.availableSize.height - sheetHeight + context.state.dismissOffset - // TODO: work with sheet here - context.add(sheet - .position(.init(x: context.availableSize.width / 2.0, y: sheetOffset)) - ) + 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)) + ) + } print("DismissOffset: \(context.state.dismissOffset)") + // Only modal// context.state.videoSize?.height ?? 160 + var videoPos: CGFloat = 0 + if 2 < 10 { + if debugUpdate { + videoPos = videoHeight - videoHeight / 2 * 2 + sheetPosition - sheetPosition + debugUpdate = false + } else { + videoPos = lastVideoPos + } + if isFullscreen { + videoPos = context.availableSize.height / 2 + } else { + videoPos = sheetPosition - sheetHeight / 2 + videoHeight / 2 + 50 + } + lastVideoPos = videoPos + } context.add(video - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0 + context.state.dismissOffset)) + .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)) ) - 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) - ) + if isFullscreen { + context.add(sheet + .position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2)) + ) + } - 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) - ) +// 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 } @@ -1231,26 +1331,31 @@ public final class _MediaStreamComponentController: ViewControllerComponentConta view.expandFromPictureInPicture() } - if let validLayout = self.validLayout { + if let _ = self.validLayout { self.view.clipsToBounds = true - self.view.layer.cornerRadius = validLayout.deviceMetrics.screenCornerRadius +// 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.width * 0.9, y: 117.0), to: self.view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in - self?.view.layer.cornerRadius = 0.0 + 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.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } self.view.layer.allowsGroupOpacity = true - self.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in + 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 } @@ -1267,13 +1372,32 @@ public final class _MediaStreamComponentController: ViewControllerComponentConta } } + 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: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in + 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 @@ -1281,18 +1405,18 @@ public final class _MediaStreamComponentController: ViewControllerComponentConta strongSelf.view.layer.allowsGroupOpacity = false strongSelf.dismissImpl(completion: completion) }) - - 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.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.frame.width * 0.9, y: 117.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in - }) - self.view.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - } + 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.3, 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) { diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 23843689eb..ace32beefd 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -19,17 +19,22 @@ 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 init( call: PresentationGroupCallImpl, hasVideo: Bool, isVisible: Bool, isAdmin: Bool, peerTitle: String, + peerImage: Any?, + isFullscreen: Bool, activatePictureInPicture: ActionSlot>, deactivatePictureInPicture: ActionSlot, bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void, - pictureInPictureClosed: @escaping () -> Void + pictureInPictureClosed: @escaping () -> Void, + onVideoSizeRetrieved: @escaping (CGSize) -> Void ) { self.call = call self.hasVideo = hasVideo @@ -40,6 +45,10 @@ final class _MediaStreamVideoComponent: Component { self.deactivatePictureInPicture = deactivatePictureInPicture self.bringBackControllerForPictureInPictureDeactivation = bringBackControllerForPictureInPictureDeactivation self.pictureInPictureClosed = pictureInPictureClosed + + self.peerImage = peerImage + self.isFullscreen = isFullscreen + self.onVideoSizeRetrieved = onVideoSizeRetrieved } public static func ==(lhs: _MediaStreamVideoComponent, rhs: _MediaStreamVideoComponent) -> Bool { @@ -81,6 +90,7 @@ final class _MediaStreamVideoComponent: Component { private var videoBlurView: VideoRenderingView? private var videoView: VideoRenderingView? private var activityIndicatorView: ComponentHostView? + private var videoPlaceholderView: UIView? private var noSignalView: ComponentHostView? private var pictureInPictureController: AVPictureInPictureController? @@ -124,12 +134,8 @@ final class _MediaStreamVideoComponent: Component { self.pictureInPictureController?.stopPictureInPicture() } } - -// let sheetView = UIView() -// let sheetBackdropView = UIView() -// var sheetTop: CGFloat = 0 -// var sheetHeight: CGFloat = 0 - + let maskGradientLayer = CAGradientLayer() + private var wasVisible = true func update(component: _MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { self.state = state @@ -141,13 +147,21 @@ final class _MediaStreamVideoComponent: Component { if let videoBlurView = self.videoRenderingContext.makeView(input: input, blur: true) { self.videoBlurView = videoBlurView self.insertSubview(videoBlurView, belowSubview: self.blurTintView) + + self.maskGradientLayer.type = .radial + self.maskGradientLayer.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] + self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.maskGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) +// self.maskGradientLayer.transform = CATransform3DMakeScale(0.3, 0.3, 1.0) +// self.maskGradientLayer.isHidden = true + } if let videoView = self.videoRenderingContext.makeView(input: input, blur: false, forceSampleBufferDisplayLayer: true) { self.videoView = videoView self.addSubview(videoView) - + videoView.alpha = 1 if let sampleBufferVideoView = videoView as? SampleBufferVideoRenderingView { if #available(iOS 13.0, *) { sampleBufferVideoView.sampleBufferLayer.preventsDisplaySleepDuringVideoPlayback = true @@ -167,6 +181,7 @@ final class _MediaStreamVideoComponent: Component { } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) { + print("pip finished") } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { @@ -227,9 +242,42 @@ final class _MediaStreamVideoComponent: Component { } // sheetView.frame = .init(x: 0, y: sheetTop, width: availableSize.width, height: sheetHeight) + // var aspect = videoView.getAspect() +// if aspect <= 0.01 { + // let aspect = !component.isFullscreen ? 16.0 / 9.0 : // 3.0 / 4.0 +// } + + let videoInset: CGFloat + if !component.isFullscreen { + videoInset = 16 + } else { + videoInset = 0 + } if let videoView = self.videoView { + var aspect = videoView.getAspect() + // saveAspect(aspect) + if component.isFullscreen { + if aspect <= 0.01 { + aspect = 3.0 / 4.0 + } + } else { + aspect = 16.0 / 9 + } + + let videoSize = CGSize(width: aspect * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height)) + let blurredVideoSize = videoSize.aspectFilled(availableSize) + + component.onVideoSizeRetrieved(videoSize) + var isVideoVisible = component.isVisible + + if !wasVisible && component.isVisible { + videoView.layer.animateAlpha(from: 0, to: 1, duration: 0.2) + } else if wasVisible && !component.isVisible { + videoView.layer.animateAlpha(from: 1, to: 0, duration: 0.2) + } + if let pictureInPictureController = self.pictureInPictureController { if pictureInPictureController.isPictureInPictureActive { isVideoVisible = true @@ -237,25 +285,40 @@ final class _MediaStreamVideoComponent: Component { } videoView.updateIsEnabled(isVideoVisible) + videoView.clipsToBounds = true + videoView.layer.cornerRadius = component.isFullscreen ? 0 : 10 + // var aspect = videoView.getAspect() +// if aspect <= 0.01 { - var aspect = videoView.getAspect() - if aspect <= 0.01 { - aspect = 3.0 / 4.0 - } - - let videoSize = CGSize(width: aspect * 100.0, height: 100.0).aspectFitted(availableSize) - let blurredVideoSize = videoSize.aspectFilled(availableSize) 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) if let videoBlurView = self.videoBlurView { videoBlurView.updateIsEnabled(component.isVisible) +// videoBlurView.isHidden = component.isFullscreen + if component.isFullscreen { + transition.withAnimation(.none).setFrame(view: videoBlurView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - blurredVideoSize.width) / 2.0), y: floor((availableSize.height - blurredVideoSize.height) / 2.0)), size: blurredVideoSize), completion: nil) + } else { + videoBlurView.frame = videoView.frame.insetBy(dx: -69 * aspect, dy: -69) + } - transition.withAnimation(.none).setFrame(view: videoBlurView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - blurredVideoSize.width) / 2.0), y: floor((availableSize.height - blurredVideoSize.height) / 2.0)), size: blurredVideoSize), completion: nil) + if !component.isFullscreen { + videoBlurView.layer.mask = maskGradientLayer + } else { + videoBlurView.layer.mask = nil + } + + self.maskGradientLayer.frame = videoBlurView.bounds// CGRect(x: videoBlurView.bounds.midX, y: videoBlurView.bounds.midY, width: videoBlurView.bounds.width, height: videoBlurView.bounds.height) } } 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 { diff --git a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift new file mode 100644 index 0000000000..4e369283c7 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift @@ -0,0 +1,286 @@ +import Foundation +import UIKit +import ComponentFlow +import ActivityIndicatorComponent +import AccountContext +import AVKit +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 + let topOffset: CGFloat + let backgroundColor: UIColor + init( +// color: UIColor, + topComponent: AnyComponent, + bottomButtonsRow: AnyComponent, + topOffset: CGFloat, + sheetHeight: CGFloat, + backgroundColor: UIColor + ) { +// self.leftItem = leftItem + self.topComponent = topComponent +// self.viewerCounter = AnyComponent(ViewerCountComponent(count: 0)) + self.bottomButtonsRow = bottomButtonsRow + self.topOffset = topOffset + self.sheetHeight = sheetHeight + self.backgroundColor = backgroundColor + } + + static func ==(lhs: StreamSheetComponent, rhs: StreamSheetComponent) -> Bool { + if lhs.topComponent != rhs.topComponent { + return false + } + if lhs.bottomButtonsRow != rhs.bottomButtonsRow { + return false + } + if lhs.topOffset != rhs.topOffset { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.sheetHeight != rhs.sheetHeight { + return false + } + + return true + } +// + final class View: UIView { + var overlayComponentsFrames = [CGRect]() + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + for subframe in overlayComponentsFrames { + if subframe.contains(point) { return true } + } + return false + } + + func update(component: StreamSheetComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { + self.backgroundColor = .purple.withAlphaComponent(0.6) + return availableSize + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + +// guard let context = UIGraphicsGetCurrentContext() else { return } +// context.setFillColor(UIColor.red.cgColor) +// overlayComponentsFrames.forEach { frame in +// context.addRect(frame) +// context.fillPath() +// } + } + } + + func makeView() -> View { + View() + } + + public final class State: ComponentState { + override init() { + super.init() + } + } + + public func makeState() -> State { + return State() + } + + private weak var state: State? +// func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { +// view.isUserInteractionEnabled = false +// return availableSize +// } + /*public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, transition: transition) + }*/ + + static var body: Body { + let background = Child(SheetBackgroundComponent.self) +// let leftItem = Child(environment: Empty.self) + let topItem = Child(environment: Empty.self) +// let viewerCounter = Child(environment: Empty.self) + let bottomButtonsRow = Child(environment: Empty.self) +// let bottomButtons = Child(environment: Empty.self) +// let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) +// let centerItem = Child(environment: Empty.self) + + return { context in + let availableWidth = context.availableSize.width +// let sideInset: CGFloat = 16.0 + context.component.sideInset + + let contentHeight: CGFloat = 44.0 + let size = context.availableSize// CGSize(width: context.availableSize.width, height:44)// context.component.topInset + contentHeight) + + let background = background.update(component: SheetBackgroundComponent(color: context.component.backgroundColor), availableSize: CGSize(width: size.width, height: context.component.sheetHeight), transition: context.transition) + + let topItem = context.component.topComponent.flatMap { topItemComponent in + return topItem.update( + component: topItemComponent, + availableSize: CGSize(width: availableWidth, height: contentHeight), + transition: context.transition + ) + } + +// let viewerCounter = context.component.viewerCounter.flatMap { viewerCounterComponent in +// return viewerCounter.update( +// component: viewerCounterComponent, +// availableSize: context.availableSize, +// transition: context.transition +// ) +// } + + let bottomButtonsRow = context.component.bottomButtonsRow.flatMap { bottomButtonsRowComponent in + return bottomButtonsRow.update( + component: bottomButtonsRowComponent, + availableSize: CGSize(width: availableWidth, height: contentHeight), + transition: context.transition + ) + } + + let topOffset = context.component.topOffset + + context.add(background + .position(CGPoint(x: size.width / 2.0, y: context.component.topOffset + context.component.sheetHeight / 2)) + ) + + (context.view as? StreamSheetComponent.View)?.overlayComponentsFrames = [] + context.view.backgroundColor = .clear + if let topItem = topItem { + context.add(topItem + .position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + contentHeight / 2.0)) + ) + (context.view as? StreamSheetComponent.View)?.overlayComponentsFrames.append(.init(x: 0, y: topOffset, width: topItem.size.width, height: topItem.size.height)) + } + +// if let viewerCounter = viewerCounter { +// let videoHeight = availableWidth / 2 +// let topRowHeight: CGFloat = 50 +// context.add(viewerCounter +// .position(CGPoint(x: viewerCounter.size.width / 2, y: topRowHeight + videoHeight + 32)) +// ) +// } + + if let bottomButtonsRow = bottomButtonsRow { + context.add(bottomButtonsRow + .position(CGPoint(x: bottomButtonsRow.size.width / 2, y: context.component.sheetHeight - 50 / 2 - 16 + topOffset)) + ) + (context.view as? StreamSheetComponent.View)?.overlayComponentsFrames.append(.init(x: 0, y: context.component.sheetHeight - 50 - 16 + topOffset, width: bottomButtonsRow.size.width, height: bottomButtonsRow.size.height)) + } + /*if let leftItem = leftItem { + print(leftItem) + context.add(leftItem + .position(CGPoint(x: leftItem.size.width / 2.0, y: contentHeight / 2.0)) + ) + (context.view as? StreamSheetComponent.View)?.overlayComponentsFrames = [ + .init(x: 0, y: 0, width: leftItem.size.width, height: leftItem.size.height) + ] + }*/ + + return size + } + } +} + +import TelegramPresentationData +import TelegramStringFormatting + +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) + +private let latePurple = UIColor(rgb: 0x974aa9) +private let latePink = UIColor(rgb: 0xf0436c) + +final class ViewerCountComponent: Component { + private let count: Int + +// private let counterView: VoiceChatTimerNode + + static func ==(lhs: ViewerCountComponent, rhs: ViewerCountComponent) -> Bool { + if lhs.count != rhs.count { + return false + } + return true + } + + init(count: Int) { + self.count = count + } + + public func update(view: UIView, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + + /*self.foregroundView.frame = CGRect(origin: CGPoint(), size: size) + self.foregroundGradientLayer.frame = self.foregroundView.bounds + self.maskView.frame = self.foregroundView.bounds + + let text: String = presentationStringsFormattedNumber(participants, groupingSeparator) + let subtitle = "listening" + + 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) + + self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 68.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.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 21.0, design: .round, weight: .semibold, traits: []), textColor: .white) + 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) + */ + return availableSize + } +} + +final class SheetBackgroundComponent: Component { + private let color: UIColor + private let backgroundView = UIView() + + static func ==(lhs: SheetBackgroundComponent, rhs: SheetBackgroundComponent) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } +// if lhs.width != rhs.width { +// return false +// } +// if lhs.height != rhs.height { +// return false +// } + return true + } + + public init(color: UIColor) { + self.color = color + } + + public func update(view: UIView, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if backgroundView.superview == nil { + view.addSubview(backgroundView) + } + backgroundView.frame = .init(origin: .zero, size: availableSize) + backgroundView.backgroundColor = self.color// .withAlphaComponent(0.4) + backgroundView.isUserInteractionEnabled = false + backgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + backgroundView.layer.cornerRadius = 16 + backgroundView.clipsToBounds = true + backgroundView.layer.masksToBounds = true + return availableSize + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 83cfe1a95d..284fd6ee37 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -36,12 +36,12 @@ 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 smallButtonSize = CGSize(width: 36.0, height: 36.0) +let sideButtonSize = CGSize(width: 56.0, height: 56.0) +let topPanelHeight: CGFloat = 63.0 let bottomAreaHeight: CGFloat = 206.0 -private let fullscreenBottomAreaHeight: CGFloat = 80.0 -private let bottomGradientHeight: CGFloat = 70.0 +let fullscreenBottomAreaHeight: CGFloat = 80.0 +let bottomGradientHeight: CGFloat = 70.0 func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? { if !top && !bottom { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 2e787f91bf..19544e5667 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -665,7 +665,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { if call.isStream { strongSelf.hasGroupCallOnScreenPromise.set(true) - let groupCallController = MediaStreamingControllerImpl(sharedContext: strongSelf, accountContext: call.accountContext, call: call) // MediaStreamComponentController(call: call) + // TODO: remove sharedContext and accountContext from init + let groupCallController = _MediaStreamComponentController(sharedContext: strongSelf, accountContext: call.accountContext, call: call) // MediaStreamComponentController(call: call)ue groupCallController.onViewDidAppear = { [weak self] in if let strongSelf = self { strongSelf.hasGroupCallOnScreenPromise.set(true)