From 8422b482169b8bf5f8752b52e0eb2ab98edbca05 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 27 Jan 2026 21:04:08 +0800 Subject: [PATCH] Glass gallery --- .../ChatItemGalleryFooterContentNode.swift | 393 +++++++++++++++--- .../ChatVideoGalleryItemScrubberView.swift | 69 +-- .../Sources/GalleryControllerNode.swift | 11 +- .../GalleryUI/Sources/GalleryFooterNode.swift | 11 +- .../Sources/Items/ChatImageGalleryItem.swift | 52 ++- .../Items/UniversalVideoGalleryItem.swift | 291 ++----------- .../Sources/MediaPlayerScrubbingNode.swift | 17 +- .../ReactionContextBackgroundNode.swift | 27 +- .../Sources/ReactionContextNode.swift | 2 +- ...tControllerExtractedPresentationNode.swift | 8 +- .../EdgeEffect/Sources/EdgeEffect.swift | 78 +++- .../Sources/GlassControlGroup.swift | 35 +- .../VideoPlaybackControlsComponent.swift | 12 +- .../Sources/ChatControllerNode.swift | 2 + .../Sources/WallpaperEdgeEffectNodeImpl.swift | 1 + 15 files changed, 611 insertions(+), 398 deletions(-) diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 290c218763..3b70e74503 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -35,6 +35,8 @@ import GlassControls import ComponentFlow import ComponentDisplayAdapters import EdgeEffect +import RasterizedCompositionComponent +import BadgeComponent private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white) private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) @@ -45,25 +47,6 @@ private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: " private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white) -private let fullscreenOnImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: .white) -private let fullscreenOffImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Collapse"), color: .white) - -private let captionMaskImage = generateImage(CGSize(width: 1.0, height: 17.0), opaque: false, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - let gradientColors = [UIColor.white.withAlphaComponent(1.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] as CFArray - - var locations: [CGFloat] = [0.0, 1.0] - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 17.0), options: CGGradientDrawingOptions()) -}) - -private let titleFont = Font.medium(15.0) -private let dateFont = Font.regular(14.0) - enum ChatItemGalleryFooterContent: Equatable { case info case fetch(status: MediaResourceStatus, seekable: Bool) @@ -128,6 +111,26 @@ class CaptionScrollWrapperNode: ASDisplayNode { } final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScrollViewDelegate { + public final class SettingsButtonState: Equatable { + public let speed: String? + public let quality: String? + + public init(speed: String?, quality: String?) { + self.speed = speed + self.quality = quality + } + + public static func ==(lhs: SettingsButtonState, rhs: SettingsButtonState) -> Bool { + if lhs.speed != rhs.speed { + return false + } + if lhs.quality != rhs.quality { + return false + } + return true + } + } + private let context: AccountContext private var presentationData: PresentationData private var theme: PresentationTheme @@ -140,6 +143,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll private let textSelectionKnobContainer: UIView private let textSelectionKnobSurface: UIView private let scrollWrapperNode: CaptionScrollWrapperNode + private let scrollWrapperMask: EdgeMaskView private let scrollWrapperEffect: VariableBlurEffect private let scrollNode: ASScrollNode @@ -178,7 +182,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll displayActionButton: Bool, displayEditButton: Bool, displayPictureInPictureButton: Bool, - displaySettingsButton: Bool + settingsButtonState: SettingsButtonState?, + displayTextRecognitionButton: Bool, + displayStickersButton: Bool )? private var codeHighlightState: (id: EngineMessage.Id, specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)? @@ -304,7 +310,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } didSet { if let scrubberView = self.scrubberView { - scrubberView.setCollapsed(self.visibilityAlpha < 1.0, animated: false) self.view.addSubview(scrubberView) scrubberView.updateScrubbingVisual = { [weak self] value in guard let strongSelf = self else { @@ -338,8 +343,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll override func setVisibilityAlpha(_ alpha: CGFloat, animated: Bool) { self.visibilityAlpha = alpha - self.contentNode.alpha = alpha - self.scrubberView?.setCollapsed(alpha < 1.0, animated: animated) + + let transition: ComponentTransition = animated ? .easeInOut(duration: 0.2) : .immediate + transition.animateView { + self.contentNode.alpha = alpha + } + //transition.setAlpha(view: self.contentNode.view, alpha: alpha) + + if let validLayout = self.validLayout { + let _ = self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, contentInset: validLayout.5, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) + } } private var hasExpandedCaptionPromise = ValuePromise(false) @@ -368,6 +381,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.scrollWrapperNode.backgroundColor = .clear self.scrollWrapperNode.isOpaque = false + self.scrollWrapperMask = EdgeMaskView() + //self.scrollWrapperNode.view.addSubview(self.scrollWrapperMask) + self.scrollNode = ASScrollNode() self.scrollNode.clipsToBounds = false @@ -795,7 +811,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll displayActionButton: false, displayEditButton: false, displayPictureInPictureButton: false, - displaySettingsButton: false + settingsButtonState: nil, + displayTextRecognitionButton: false, + displayStickersButton: false ) } @@ -815,7 +833,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } - func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, displayPictureInPictureButton: Bool = false, displaySettingsButton: Bool = false) { + func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, displayPictureInPictureButton: Bool = false, settingsButtonState: SettingsButtonState? = nil, displayTextRecognitionButton: Bool = false, displayStickersButton: Bool = false, animated: Bool = false) { self.currentMessage = message var displayInfo = displayInfo @@ -1061,10 +1079,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll displayActionButton: displayActionButton, displayEditButton: displayEditButton, displayPictureInPictureButton: displayPictureInPictureButton, - displaySettingsButton: displaySettingsButton + settingsButtonState: settingsButtonState, + displayTextRecognitionButton: displayTextRecognitionButton, + displayStickersButton: displayStickersButton ) - self.requestLayout?(.immediate) + self.requestLayout?(animated ? .animated(duration: 0.4, curve: .spring) : .immediate) } } @@ -1169,6 +1189,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll let textSize = self.textNode.updateLayout(constrainSize) var textOffset: CGFloat = 0.0 + var additionalTextHeight: CGFloat = 0.0 if displayCaption { visibleTextHeight = textSize.height if visibleTextHeight > 100.0 { @@ -1190,7 +1211,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll var maxTextOffset: CGFloat = size.height - bottomInset - 238.0 - UIScreenPixel if let _ = self.scrubberView { - maxTextOffset -= 44.0 + //maxTextOffset -= 100.0 + maxTextOffset += 0.0 + additionalTextHeight += 44.0 } textOffset = min(maxTextOffset, self.scrollNode.view.contentOffset.y) let originalPanelHeight = panelHeight @@ -1198,47 +1221,22 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if self.scrollNode.view.isScrollEnabled { if self.scrollWrapperNode.view.mask == nil { - self.scrollWrapperNode.layer.rasterizationScale = UIScreenScale - let maskView = UIImageView() - - let height: CGFloat = 70.0 - let baseGradientAlpha: CGFloat = 1.0 - let numSteps = 8 - let firstStep = 0 - let firstLocation = 0.0 - let colors = (0 ..< numSteps).map { i -> UIColor in - if i < firstStep { - return UIColor(white: 1.0, alpha: 1.0) - } else { - let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) - let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) - return UIColor(white: 0.0, alpha: baseGradientAlpha * value) - } - } - let locations = (0 ..< numSteps).map { i -> CGFloat in - if i < firstStep { - return 0.0 - } else { - let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) - return (firstLocation + (1.0 - firstLocation) * step) - } - } - - maskView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors, locations: locations)!.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1) - - //maskView.image = EdgeEffectView.generateEdgeGradient(baseHeight: originalPanelHeight + 20.0, isInverted: false) - self.scrollWrapperNode.view.mask = maskView + self.scrollWrapperNode.view.mask = self.scrollWrapperMask } } else { self.scrollWrapperNode.view.mask = nil } - let scrollWrapperNodeFrame = CGRect(x: 0.0, y: 0.0, width: width, height: max(0.0, visibleTextPanelHeight + textOffset + originalPanelHeight - 14.0)) + let scrollWrapperNodeFrame = CGRect(x: 0.0, y: 0.0, width: width, height: max(0.0, visibleTextPanelHeight + textOffset + originalPanelHeight - 14.0 + additionalTextHeight)) if self.scrollWrapperNode.frame != scrollWrapperNodeFrame { self.scrollWrapperNode.frame = scrollWrapperNodeFrame - self.scrollWrapperNode.view.mask?.frame = self.scrollWrapperNode.bounds - self.scrollWrapperNode.view.mask?.layer.removeAllAnimations() - self.scrollWrapperEffect.update(size: scrollWrapperNodeFrame.size, constantHeight: 80.0, placement: VariableBlurEffect.Placement(position: .bottom, extendsInwards: true), gradient: EdgeEffectView.generateEdgeGradientData(baseHeight: 80.0), transition: transition) + do { + let mask = self.scrollWrapperMask + mask.update(size: self.scrollWrapperNode.bounds.size, color: UIColor.blue, gradientHeight: 90.0 + additionalTextHeight + contentInset, extensionHeight: 0.0, transition: ComponentTransition(transition)) + mask.frame = self.scrollWrapperNode.bounds + mask.layer.removeAllAnimations() + } + self.scrollWrapperEffect.update(size: CGSize(width: scrollWrapperNodeFrame.width, height: scrollWrapperNodeFrame.height), constantHeight: 90.0, placement: VariableBlurEffect.Placement(position: .bottom, inwardsExtension: additionalTextHeight + contentInset), gradient: EdgeEffectView.generateEdgeGradientData(baseHeight: 90.0), transition: transition) } if let buttonNode = self.buttonNode { @@ -1295,7 +1293,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } let scrubberFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: scrubberY), size: CGSize(width: width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0)) - scrubberView.updateLayout(size: scrubberFrame.size, leftInset: 0.0, rightInset: 0.0, transition: .immediate) + scrubberView.updateLayout(size: scrubberFrame.size, leftInset: 0.0, rightInset: 0.0, isCollapsed: self.visibilityAlpha < 1.0, transition: transition) transition.updateBounds(layer: scrubberView.layer, bounds: CGRect(origin: CGPoint(), size: scrubberFrame.size)) transition.updatePosition(layer: scrubberView.layer, position: CGPoint(x: scrubberFrame.midX, y: scrubberFrame.midY)) } @@ -1330,10 +1328,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } )) } - if buttonsState.displaySettingsButton { + if let settingsButtonState = buttonsState.settingsButtonState { centerControlItems.append(GlassControlGroupComponent.Item( id: AnyHashable("settings"), - content: .icon("Chat/Context Menu/Settings"), + content: .customIcon(id: AnyHashable("settings"), component: AnyComponent(SettingsNavigationIconComponent( + speed: settingsButtonState.speed, + quality: settingsButtonState.quality, + isOpen: false + ))), action: { [weak self] in guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View, let centerItemView = buttonPanelView.centerItemView else { return @@ -1357,6 +1359,30 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } )) } + if buttonsState.displayTextRecognitionButton { + centerControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("textRecognition"), + content: .icon("Media Gallery/LiveTextIcon"), + action: { [weak self] in + guard let self else { + return + } + self.textRecognitionButtonPressed() + } + )) + } + if buttonsState.displayStickersButton { + centerControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("stickers"), + content: .icon("Media Gallery/Stickers"), + action: { [weak self] in + guard let self else { + return + } + self.stickersButtonPressed() + } + )) + } if buttonsState.displayFullscreenButton && !metrics.isTablet { centerControlItems.append(GlassControlGroupComponent.Item( id: AnyHashable("fullscreen"), @@ -2126,6 +2152,21 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.controllerInteraction?.editMedia(message.id) } + private func textRecognitionButtonPressed() { + guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? ChatImageGalleryItemNode else { + return + } + currentItemNode.textRecognitionButtonPressed() + } + + private func stickersButtonPressed() { + if let currentItemNode = self.controllerInteraction?.currentItemNode() as? ChatImageGalleryItemNode { + currentItemNode.openStickersButtonPressed() + } else if let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode { + currentItemNode.openStickersButtonPressed() + } + } + private func pipButtonPressed() { guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode else { return @@ -2333,3 +2374,225 @@ private final class PlaybackButtonNode: HighlightTrackingButtonNode { self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0) + UIScreenPixel), size: textSize) } } + +final class SettingsNavigationIconComponent: Component { + let speed: String? + let quality: String? + let isOpen: Bool + + init(speed: String?, quality: String?, isOpen: Bool) { + self.speed = speed + self.quality = quality + self.isOpen = isOpen + } + + static func ==(lhs: SettingsNavigationIconComponent, rhs: SettingsNavigationIconComponent) -> Bool { + if lhs.speed != rhs.speed { + return false + } + if lhs.quality != rhs.quality { + return false + } + if lhs.isOpen != rhs.isOpen { + return false + } + return true + } + + final class View: UIView { + private let iconLayer: RasterizedCompositionMonochromeLayer + + private let gearsLayer: RasterizedCompositionImageLayer + private let dotLayer: RasterizedCompositionImageLayer + + private var speedBadge: ComponentView? + private var qualityBadge: ComponentView? + + private var speedBadgeText: String? + private var qualityBadgeText: String? + + private let badgeFont: UIFont + + private var isMenuOpen: Bool = false + + override init(frame: CGRect) { + self.iconLayer = RasterizedCompositionMonochromeLayer() + + self.gearsLayer = RasterizedCompositionImageLayer() + self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white) + + self.dotLayer = RasterizedCompositionImageLayer() + self.dotLayer.image = generateFilledCircleImage(diameter: 4.0, color: .white) + + self.iconLayer.contentsLayer.addSublayer(self.gearsLayer) + self.iconLayer.contentsLayer.addSublayer(self.dotLayer) + + self.badgeFont = Font.with(size: 8.0, design: .round, weight: .bold) + + super.init(frame: frame) + + self.layer.addSublayer(self.iconLayer) + + let size = CGSize(width: 44.0, height: 44.0) + + if let image = self.gearsLayer.image { + let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0) + let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom) + let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + self.iconLayer.position = iconFrame.center + self.iconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center + self.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconLayer.maskedLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center + self.iconLayer.maskedLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + self.iconLayer.maskedLayer.backgroundColor = UIColor.white.cgColor + + let gearsFrame = CGRect(origin: CGPoint(x: floor((iconSize.width - image.size.width) * 0.5), y: floor((iconSize.height - image.size.height) * 0.5)), size: image.size) + self.gearsLayer.position = gearsFrame.center + self.gearsLayer.bounds = CGRect(origin: CGPoint(), size: gearsFrame.size) + + if let dotImage = self.dotLayer.image { + let dotFrame = CGRect(origin: CGPoint(x: gearsFrame.minX + floorToScreenPixels((gearsFrame.width - dotImage.size.width) * 0.5), y: gearsFrame.minY + floorToScreenPixels((gearsFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size) + self.dotLayer.position = dotFrame.center + self.dotLayer.bounds = CGRect(origin: CGPoint(), size: dotFrame.size) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setIsMenuOpen(isMenuOpen: Bool) { + if self.isMenuOpen == isMenuOpen { + return + } + self.isMenuOpen = isMenuOpen + + let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + rotationTransition.updateTransform(layer: self.gearsLayer, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0)) + self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.gearsLayer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: true) + }) + + self.dotLayer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.dotLayer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: true) + }) + } + + func setBadges(speed: String?, quality: String?, transition: ComponentTransition) { + if self.speedBadgeText == speed && self.qualityBadgeText == quality { + return + } + self.speedBadgeText = speed + self.qualityBadgeText = quality + + if let badgeText = speed { + var badgeTransition = transition + let speedBadge: ComponentView + if let current = self.speedBadge { + speedBadge = current + } else { + speedBadge = ComponentView() + self.speedBadge = speedBadge + badgeTransition = badgeTransition.withAnimation(.none) + } + let badgeSize = speedBadge.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + text: badgeText, + font: self.badgeFont, + cornerRadius: .custom(3.0), + insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), + outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let speedBadgeView = speedBadge.view { + if speedBadgeView.layer.superlayer == nil { + self.iconLayer.contentsLayer.addSublayer(speedBadgeView.layer) + + transition.animateAlpha(layer: speedBadgeView.layer, from: 0.0, to: 1.0) + transition.animateScale(layer: speedBadgeView.layer, from: 0.001, to: 1.0) + } + badgeTransition.setFrame(layer: speedBadgeView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeSize)) + } + } else if let speedBadge = self.speedBadge { + self.speedBadge = nil + if let speedBadgeView = speedBadge.view { + let speedBadgeLayer = speedBadgeView.layer + transition.setAlpha(layer: speedBadgeLayer, alpha: 0.0, completion: { [weak speedBadgeLayer] _ in + speedBadgeLayer?.removeFromSuperlayer() + }) + transition.setScale(layer: speedBadgeLayer, scale: 0.001) + } + } + + if let badgeText = quality { + var badgeTransition = transition + let qualityBadge: ComponentView + if let current = self.qualityBadge { + qualityBadge = current + } else { + qualityBadge = ComponentView() + self.qualityBadge = qualityBadge + badgeTransition = badgeTransition.withAnimation(.none) + } + let badgeSize = qualityBadge.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + text: badgeText, + font: self.badgeFont, + cornerRadius: .custom(3.0), + insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), + outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let qualityBadgeView = qualityBadge.view { + if qualityBadgeView.layer.superlayer == nil { + self.iconLayer.contentsLayer.addSublayer(qualityBadgeView.layer) + + transition.animateAlpha(layer: qualityBadgeView.layer, from: 0.0, to: 1.0) + transition.animateScale(layer: qualityBadgeView.layer, from: 0.001, to: 1.0) + } + badgeTransition.setFrame(layer: qualityBadgeView.layer, frame: CGRect(origin: CGPoint(x: self.iconLayer.bounds.width - badgeSize.width, y: self.iconLayer.bounds.height - badgeSize.height), size: badgeSize)) + } + } else if let qualityBadge = self.qualityBadge { + self.qualityBadge = nil + if let qualityBadgeView = qualityBadge.view { + let qualityBadgeLayer = qualityBadgeView.layer + transition.setAlpha(layer: qualityBadgeLayer, alpha: 0.0, completion: { [weak qualityBadgeLayer] _ in + qualityBadgeLayer?.removeFromSuperlayer() + }) + transition.setScale(layer: qualityBadgeLayer, scale: 0.001) + } + } + } + + func update(component: SettingsNavigationIconComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + self.setBadges(speed: component.speed, quality: component.quality, transition: transition) + self.setIsMenuOpen(isMenuOpen: component.isOpen) + + return CGSize(width: 44.0, height: 44.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index 4eb20f145c..dd1fa15b92 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -21,7 +21,7 @@ private let scrubberForegroundColor = UIColor.white private let scrubberBufferingColor = UIColor(rgb: 0xffffff, alpha: 0.5) final class ChatVideoGalleryItemScrubberView: UIView { - private var containerLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)? + private var containerLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat, isCollapsed: Bool)? private let backgroundContainer: GlassBackgroundContainerView private let backgroundView: GlassBackgroundView @@ -80,6 +80,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { self.chapters = chapters self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 8.0, lineCap: .round, scrubberHandle: .none, backgroundColor: scrubberBackgroundColor, foregroundColor: scrubberForegroundColor, bufferingColor: scrubberBufferingColor, chapters: chapters)) + self.scrubberNode.layer.allowsGroupOpacity = true self.shimmerEffectNode = ShimmerEffectForegroundNode() self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white) @@ -127,9 +128,9 @@ final class ChatVideoGalleryItemScrubberView: UIView { } } - self.backgroundView.contentView.addSubview(self.scrubberNode.view) self.backgroundView.contentView.addSubview(self.leftTimestampNode.view) self.backgroundView.contentView.addSubview(self.rightTimestampNode.view) + self.addSubview(self.scrubberNode.view) //self.backgroundView.contentView.addSubview(self.infoNode.view) } @@ -146,20 +147,6 @@ final class ChatVideoGalleryItemScrubberView: UIView { var isLoading = false var isCollapsed: Bool? - func setCollapsed(_ collapsed: Bool, animated: Bool) { - guard self.isCollapsed != collapsed else { - return - } - - self.isCollapsed = collapsed - - self.updateTimestampsVisibility(animated: animated) - self.updateScrubberVisibility(animated: animated) - - if let (size, _, _) = self.containerLayout { - self.infoNode.alpha = size.width < size.height && !collapsed ? 1.0 : 0.0 - } - } func updateTimestampsVisibility(animated: Bool) { if self.isAnimatedOut { @@ -174,15 +161,20 @@ final class ChatVideoGalleryItemScrubberView: UIView { private func updateScrubberVisibility(animated: Bool) { var collapsed = self.isCollapsed var alpha: CGFloat = 1.0 + var controlAlpha: CGFloat = 1.0 if let playbackStatus = self.playbackStatus, playbackStatus.duration <= 30.0 { + alpha = self.isCollapsed == true ? 0.0 : 1.0 + controlAlpha = 1.0 } else { alpha = self.isCollapsed == true ? 0.0 : 1.0 + controlAlpha = alpha collapsed = false } self.scrubberNode.setCollapsed(collapsed == true, animated: animated) let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate ComponentTransition(transition).setAlpha(view: self.backgroundContainer, alpha: alpha) + ComponentTransition(transition).setAlpha(view: self.scrubberNode.view, alpha: controlAlpha) } func animateTo(_ timestamp: Double) { @@ -232,8 +224,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { strongSelf.currentLeftString = leftString strongSelf.currentRightString = rightString - if let (size, leftInset, rightInset) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + if let (size, leftInset, rightInset, isCollapsed) = strongSelf.containerLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) } } } @@ -284,8 +276,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { } strongSelf.infoNode.attributedText = NSAttributedString(string: chapter.title, font: textFont, textColor: .white) - if let (size, leftInset, rightInset) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + if let (size, leftInset, rightInset, isCollapsed) = strongSelf.containerLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) } } } @@ -315,7 +307,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { strongSelf.infoNodePushed = infoNodePushed if let layout = strongSelf.containerLayout { - strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .animated(duration: 0.35, curve: .spring)) + strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, isCollapsed: layout.3, transition: .animated(duration: 0.35, curve: .spring)) } } })) @@ -343,8 +335,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { } strongSelf.infoNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white) - if let (size, leftInset, rightInset) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + if let (size, leftInset, rightInset, isCollapsed) = strongSelf.containerLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) } } })) @@ -356,8 +348,10 @@ final class ChatVideoGalleryItemScrubberView: UIView { } } - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (size, leftInset, rightInset) + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, isCollapsed: Bool, transition: ContainedViewLayoutTransition) { + self.containerLayout = (size, leftInset, rightInset, isCollapsed) + + self.isCollapsed = isCollapsed let transition = ComponentTransition(transition) @@ -386,6 +380,11 @@ final class ChatVideoGalleryItemScrubberView: UIView { rightTimestampOffset = 14.0 infoOffset = 0.0 + if isCollapsed { + scrubberLeftInset = 0.0 + scrubberRightInset = 0.0 + } + transition.setFrame(view: self.leftTimestampNode.view, frame: CGRect(origin: CGPoint(x: 16.0, y: leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) transition.setFrame(view: self.rightTimestampNode.view, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 16.0, y: rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) @@ -397,8 +396,21 @@ final class ChatVideoGalleryItemScrubberView: UIView { transition.setPosition(view: self.infoNode.view, position: CGPoint(x: size.width / 2.0, y: infoOffset + infoSize.height / 2.0)) self.infoNode.alpha = size.width < size.height && self.isCollapsed == false ? 1.0 : 0.0 - let scrubberFrame = CGRect(origin: CGPoint(x: scrubberLeftInset, y: 15.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberLeftInset - scrubberRightInset, height: scrubberHeight)) - self.scrubberNode.frame = scrubberFrame + var scrubberFrame = CGRect(origin: CGPoint(x: scrubberLeftInset, y: 15.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberLeftInset - scrubberRightInset, height: scrubberHeight)) + if isCollapsed { + scrubberFrame.origin.y = size.height - scrubberHeight + } + transition.setFrame(view: self.scrubberNode.view, frame: scrubberFrame) + + let scrubberTransition: ControlledTransition + switch transition.animation { + case let .curve(duration, curve): + scrubberTransition = ControlledTransition(duration: duration, curve: curve.containedViewLayoutTransitionCurve, interactive: false) + default: + scrubberTransition = ControlledTransition(duration: 0.0, curve: .linear, interactive: false) + } + + self.scrubberNode.update(size: scrubberFrame.size, animator: scrubberTransition.legacyAnimator) self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: scrubberFrame.size), within: scrubberFrame.size) self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.75), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) self.shimmerEffectNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: scrubberFrame.size.width, height: 5.0)) @@ -409,6 +421,9 @@ final class ChatVideoGalleryItemScrubberView: UIView { transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) self.backgroundView.update(size: size, cornerRadius: min(44.0 * 0.5, size.height * 0.5), isDark: true, tintColor: .init(kind: .panel), transition: transition) + + self.updateTimestampsVisibility(animated: !transition.animation.isImmediate) + self.updateScrubberVisibility(animated: !transition.animation.isImmediate) } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 10ec3ffd9e..a9ea21f700 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -75,6 +75,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } self.headerEdgeEffectView = EdgeEffectView() + self.headerEdgeEffectView.isUserInteractionEnabled = false self.pager = GalleryPagerNode(pageGap: pageGap, disableTapNavigation: disableTapNavigation) self.footerNode = GalleryFooterNode(controllerInteraction: controllerInteraction) @@ -303,7 +304,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture edgeEffectFrame.origin.y -= navigationBarHeight } transition.updateFrame(view: self.headerEdgeEffectView, frame: edgeEffectFrame) - self.headerEdgeEffectView.update(content: .black, alpha: 0.35, rect: edgeEffectFrame, edge: .top, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: ComponentTransition(transition)) + self.headerEdgeEffectView.update(content: .black, alpha: 0.6, rect: edgeEffectFrame, edge: .top, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: ComponentTransition(transition)) transition.updateAlpha(layer: self.headerEdgeEffectView.layer, alpha: self.areControlsHidden ? 0.0 : 1.0) if let navigationBar = self.navigationBar { @@ -363,16 +364,16 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture self.areControlsHidden = hidden self.controlsVisibilityChanged?(!hidden) if animated { + let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 UIView.animate(withDuration: 0.3, animations: { - let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 self.navigationBar?.alpha = alpha self.statusBar?.updateAlpha(alpha, transition: .animated(duration: 0.3, curve: .easeInOut)) - self.footerNode.setVisibilityAlpha(alpha, animated: animated) - self.updateThumbnailContainerNodeAlpha(.immediate) }) + self.footerNode.setVisibilityAlpha(alpha, animated: animated) + self.updateThumbnailContainerNodeAlpha(.immediate) if let (navigationBarHeight, layout) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } } else { let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index beff9b0fac..148e66246a 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -8,6 +8,7 @@ import ComponentDisplayAdapters public final class GalleryFooterNode: ASDisplayNode { private let edgeEffectView: EdgeEffectView + private var contentsFrame = CGRect() private var currentThumbnailPanelHeight: CGFloat? private var currentFooterContentNode: GalleryFooterContentNode? @@ -20,6 +21,7 @@ public final class GalleryFooterNode: ASDisplayNode { self.controllerInteraction = controllerInteraction self.edgeEffectView = EdgeEffectView() + self.edgeEffectView.isUserInteractionEnabled = false super.init() @@ -29,7 +31,8 @@ public final class GalleryFooterNode: ASDisplayNode { private var visibilityAlpha: CGFloat = 1.0 public func setVisibilityAlpha(_ alpha: CGFloat, animated: Bool) { self.visibilityAlpha = alpha - self.edgeEffectView.alpha = alpha + let transition: ComponentTransition = animated ? .easeInOut(duration: 0.2) : .immediate + transition.setAlpha(view: self.edgeEffectView, alpha: alpha) self.currentFooterContentNode?.setVisibilityAlpha(alpha, animated: true) self.currentOverlayContentNode?.setVisibilityAlpha(alpha) } @@ -132,13 +135,15 @@ public final class GalleryFooterNode: ASDisplayNode { backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight)) } + self.contentsFrame = backgroundFrame + var edgeEffectFrame = backgroundFrame let edgeEffectHeight: CGFloat = 120.0 let edgeEffectOffset: CGFloat = 70.0 edgeEffectFrame.origin.y -= edgeEffectOffset edgeEffectFrame.size.height += edgeEffectOffset edgeEffectTransition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) - self.edgeEffectView.update(content: .black, alpha: 0.35, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: edgeEffectTransition) + self.edgeEffectView.update(content: .black, alpha: 0.6, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: edgeEffectTransition) let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) if let overlayContentNode = self.currentOverlayContentNode { @@ -171,7 +176,7 @@ public final class GalleryFooterNode: ASDisplayNode { if let overlayResult = self.currentOverlayContentNode?.hitTest(point, with: event) { return overlayResult } - if !self.edgeEffectView.frame.contains(point) || self.visibilityAlpha < 1.0 { + if !self.contentsFrame.contains(point) || self.visibilityAlpha < 1.0 { return nil } let result = super.hitTest(point, with: event) diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 11d0b7daf0..3fdc1ac8d5 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -232,6 +232,7 @@ class ChatImageGalleryItem: GalleryItem { final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let context: AccountContext private var message: Message? + private var displayInfo: Bool = false private var translateToLanguage: String? private var peerIsCopyProtected: Bool = false private var isSecret: Bool = false @@ -242,6 +243,9 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let recognitionOverlayContentNode: ImageRecognitionOverlayContentNode + private var displayTextRecognitionButton: Bool = false + private var displayStickersButton: Bool = false + private let moreBarButton: MoreHeaderButton private var tilingNode: TilingNode? @@ -445,6 +449,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.imageNode.addSubnode(recognizedContentNode) strongSelf.recognizedContentNode = recognizedContentNode strongSelf.recognitionOverlayContentNode.transitionIn() + + if !strongSelf.displayTextRecognitionButton { + strongSelf.displayTextRecognitionButton = true + strongSelf.updateFooter(animated: true) + } } } })) @@ -452,11 +461,19 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool, isSecret: Bool) { self.message = message + self.displayInfo = displayInfo self.translateToLanguage = translateToLanguage self.peerIsCopyProtected = peerIsCopyProtected self.isSecret = isSecret self.imageNode.captureProtected = message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.isCopyProtected() || peerIsCopyProtected || isSecret || message.paidContent != nil - self.footerContentNode.setMessage(message, displayInfo: displayInfo, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected) + self.updateFooter(animated: false) + } + + private func updateFooter(animated: Bool) { + guard let message = self.message else { + return + } + self.footerContentNode.setMessage(message, displayInfo: self.displayInfo, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, displayTextRecognitionButton: self.displayTextRecognitionButton, displayStickersButton: self.displayStickersButton, animated: animated) } fileprivate func setImage(userLocation: MediaResourceUserLocation, imageReference: ImageMediaReference) { @@ -497,9 +514,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { var barButtonItems: [UIBarButtonItem] = [] if imageReference.media.flags.contains(.hasStickers) { - let rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Stickers"), color: .white), style: .plain, target: self, action: #selector(self.openStickersButtonPressed)) - rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_Stickers - barButtonItems.append(rightBarButtonItem) + self.displayStickersButton = true } if self.message != nil { let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! @@ -509,6 +524,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self._rightBarButtonItems.set(.single(barButtonItems)) } self.contextAndMedia = (self.context, imageReference.abstract) + self.updateFooter(animated: false) } private func updateImageFromFile(path: String) { @@ -768,11 +784,16 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { controller.presentInGlobalOverlay(contextController) } + func textRecognitionButtonPressed() { + self.recognitionOverlayContentNode.isSelected = true + self.recognitionOverlayContentNode.action?(true) + } + @objc func openStickersButtonPressed() { guard let (context, media) = self.contextAndMedia else { return } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let topController = (self.baseNavigationController()?.topViewController as? ViewController) let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) @@ -800,7 +821,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } let baseNavigationController = strongSelf.baseNavigationController() baseNavigationController?.view.endEditing(true) - let controller = StickerPackScreen(context: context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { actions in + let controller = StickerPackScreen(context: context, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { actions in if let (info, items, action) = actions.first { let animateInAsReplacement = false switch action { @@ -1562,7 +1583,7 @@ private class ImageRecognitionOverlayContentNode: GalleryOverlayContentNode { private var validLayout: (CGSize, LayoutMetrics, UIEdgeInsets)? private var interfaceIsHidden: Bool = false - private var isSelected: Bool = false + var isSelected: Bool = false init(theme: PresentationTheme) { self.backgroundContainer = GlassBackgroundContainerView() @@ -1619,33 +1640,24 @@ private class ImageRecognitionOverlayContentNode: GalleryOverlayContentNode { self.backgroundContainer.update(size: buttonSize, isDark: true, transition: transition) self.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: buttonSize) - let tintColor: GlassBackgroundView.TintColor - if self.isSelected { - tintColor = .init(kind: .custom(style: .default, color: UIColor(white: 1.0, alpha: 1.0))) - } else { - tintColor = .init(kind: .panel) - } + let tintColor: GlassBackgroundView.TintColor = .init(kind: .custom(style: .default, color: UIColor(white: 1.0, alpha: 1.0))) self.backgroundView.update(size: buttonSize, cornerRadius: buttonSize.height * 0.5, isDark: true, tintColor: tintColor, isInteractive: true, transition: transition) self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: buttonSize) - transition.setTintColor(view: self.iconView, color: self.isSelected ? .black : .white) + transition.setTintColor(view: self.iconView, color: .black) if self.appeared { if !self.isSelected && isHidden { transition.setAlpha(view: self.backgroundContainer, alpha: 0.0) } else { - transition.setAlpha(view: self.backgroundContainer, alpha: 1.0) + transition.setAlpha(view: self.backgroundContainer, alpha: self.isSelected ? 1.0 : 0.0) } } else { transition.setAlpha(view: self.backgroundContainer, alpha: 0.0) } var buttonPosition: CGPoint - if isHidden && !self.isSelected { - buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - 66.0 - 10.0, y: -52.0) - } else { - buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - (self.isSelected ? 24.0 : 70.0), y: insets.top - 50.0) - } + buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - 16.0, y: insets.top - 50.0) transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: buttonPosition, size: buttonSize)) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 4cf2ba9756..48a8fd5786 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -611,226 +611,6 @@ final class MoreHeaderButton: HighlightableButtonNode { } } -final class SettingsHeaderButton: HighlightableButtonNode { - let referenceNode: ContextReferenceContentNode - let containerNode: ContextControllerSourceNode - - private let iconLayer: RasterizedCompositionMonochromeLayer - - private let gearsLayer: RasterizedCompositionImageLayer - private let dotLayer: RasterizedCompositionImageLayer - - private var speedBadge: ComponentView? - private var qualityBadge: ComponentView? - - private var speedBadgeText: String? - private var qualityBadgeText: String? - - private let badgeFont: UIFont - - private var isMenuOpen: Bool = false - - var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? - - private let wide: Bool - - init(wide: Bool = false) { - self.wide = wide - - self.referenceNode = ContextReferenceContentNode() - self.containerNode = ContextControllerSourceNode() - self.containerNode.animateScale = false - - self.iconLayer = RasterizedCompositionMonochromeLayer() - //self.iconLayer.backgroundColor = UIColor.green.cgColor - - self.gearsLayer = RasterizedCompositionImageLayer() - self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white) - - self.dotLayer = RasterizedCompositionImageLayer() - self.dotLayer.image = generateFilledCircleImage(diameter: 4.0, color: .white) - - self.iconLayer.contentsLayer.addSublayer(self.gearsLayer) - self.iconLayer.contentsLayer.addSublayer(self.dotLayer) - - self.badgeFont = Font.with(size: 8.0, design: .round, weight: .bold) - - super.init() - - self.containerNode.addSubnode(self.referenceNode) - self.referenceNode.layer.addSublayer(self.iconLayer) - self.addSubnode(self.containerNode) - - self.containerNode.shouldBegin = { [weak self] location in - guard let strongSelf = self, let _ = strongSelf.contextAction else { - return false - } - return true - } - self.containerNode.activated = { [weak self] gesture, _ in - guard let strongSelf = self else { - return - } - strongSelf.contextAction?(strongSelf.containerNode, gesture) - } - - self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0)) - self.referenceNode.frame = self.containerNode.bounds - - if let image = self.gearsLayer.image { - let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0) - let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom) - let iconFrame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - iconSize.width) / 2.0), y: floor((self.containerNode.bounds.height - iconSize.height) / 2.0)), size: iconSize) - self.iconLayer.position = iconFrame.center - self.iconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) - - self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center - self.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) - - self.iconLayer.maskedLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center - self.iconLayer.maskedLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) - self.iconLayer.maskedLayer.backgroundColor = UIColor.white.cgColor - - let gearsFrame = CGRect(origin: CGPoint(x: floor((iconSize.width - image.size.width) * 0.5), y: floor((iconSize.height - image.size.height) * 0.5)), size: image.size) - self.gearsLayer.position = gearsFrame.center - self.gearsLayer.bounds = CGRect(origin: CGPoint(), size: gearsFrame.size) - - if let dotImage = self.dotLayer.image { - let dotFrame = CGRect(origin: CGPoint(x: gearsFrame.minX + floorToScreenPixels((gearsFrame.width - dotImage.size.width) * 0.5), y: gearsFrame.minY + floorToScreenPixels((gearsFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size) - self.dotLayer.position = dotFrame.center - self.dotLayer.bounds = CGRect(origin: CGPoint(), size: dotFrame.size) - } - } - } - - override func didLoad() { - super.didLoad() - self.view.isOpaque = false - } - - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: 44.0, height: 44.0) - } - - func onLayout() { - } - - func setIsMenuOpen(isMenuOpen: Bool) { - if self.isMenuOpen == isMenuOpen { - return - } - self.isMenuOpen = isMenuOpen - - let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) - rotationTransition.updateTransform(layer: self.gearsLayer, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0)) - self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in - guard let self, finished else { - return - } - self.gearsLayer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: true) - }) - - self.dotLayer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in - guard let self, finished else { - return - } - self.dotLayer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: true) - }) - } - - func setBadges(speed: String?, quality: String?, transition: ComponentTransition) { - if self.speedBadgeText == speed && self.qualityBadgeText == quality { - return - } - self.speedBadgeText = speed - self.qualityBadgeText = quality - - if let badgeText = speed { - var badgeTransition = transition - let speedBadge: ComponentView - if let current = self.speedBadge { - speedBadge = current - } else { - speedBadge = ComponentView() - self.speedBadge = speedBadge - badgeTransition = badgeTransition.withAnimation(.none) - } - let badgeSize = speedBadge.update( - transition: badgeTransition, - component: AnyComponent(BadgeComponent( - text: badgeText, - font: self.badgeFont, - cornerRadius: .custom(3.0), - insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), - outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - if let speedBadgeView = speedBadge.view { - if speedBadgeView.layer.superlayer == nil { - self.iconLayer.contentsLayer.addSublayer(speedBadgeView.layer) - - transition.animateAlpha(layer: speedBadgeView.layer, from: 0.0, to: 1.0) - transition.animateScale(layer: speedBadgeView.layer, from: 0.001, to: 1.0) - } - badgeTransition.setFrame(layer: speedBadgeView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeSize)) - } - } else if let speedBadge = self.speedBadge { - self.speedBadge = nil - if let speedBadgeView = speedBadge.view { - let speedBadgeLayer = speedBadgeView.layer - transition.setAlpha(layer: speedBadgeLayer, alpha: 0.0, completion: { [weak speedBadgeLayer] _ in - speedBadgeLayer?.removeFromSuperlayer() - }) - transition.setScale(layer: speedBadgeLayer, scale: 0.001) - } - } - - if let badgeText = quality { - var badgeTransition = transition - let qualityBadge: ComponentView - if let current = self.qualityBadge { - qualityBadge = current - } else { - qualityBadge = ComponentView() - self.qualityBadge = qualityBadge - badgeTransition = badgeTransition.withAnimation(.none) - } - let badgeSize = qualityBadge.update( - transition: badgeTransition, - component: AnyComponent(BadgeComponent( - text: badgeText, - font: self.badgeFont, - cornerRadius: .custom(3.0), - insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), - outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - if let qualityBadgeView = qualityBadge.view { - if qualityBadgeView.layer.superlayer == nil { - self.iconLayer.contentsLayer.addSublayer(qualityBadgeView.layer) - - transition.animateAlpha(layer: qualityBadgeView.layer, from: 0.0, to: 1.0) - transition.animateScale(layer: qualityBadgeView.layer, from: 0.001, to: 1.0) - } - badgeTransition.setFrame(layer: qualityBadgeView.layer, frame: CGRect(origin: CGPoint(x: self.iconLayer.bounds.width - badgeSize.width, y: self.iconLayer.bounds.height - badgeSize.height), size: badgeSize)) - } - } else if let qualityBadge = self.qualityBadge { - self.qualityBadge = nil - if let qualityBadgeView = qualityBadge.view { - let qualityBadgeLayer = qualityBadgeView.layer - transition.setAlpha(layer: qualityBadgeLayer, alpha: 0.0, completion: { [weak qualityBadgeLayer] _ in - qualityBadgeLayer?.removeFromSuperlayer() - }) - transition.setScale(layer: qualityBadgeLayer, scale: 0.001) - } - } - } -} - @available(iOS 15.0, *) private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPictureControllerDelegate { private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { @@ -1113,8 +893,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var moreBarButtonRate: Double = 1.0 private var moreBarButtonRateTimestamp: Double? - private let settingsBarButton: SettingsHeaderButton - private var videoNode: UniversalVideoNode? private var videoNodeUserInteractionEnabled: Bool = false private var videoFramePreview: FramePreview? @@ -1139,7 +917,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var dismissOnOrientationChange = false private var keepSoundOnDismiss = false private var hasPictureInPicture = false - private var hasSettingsButton = false + private var displayStickersButton: Bool = false + private var settingsButtonState: ChatItemGalleryFooterContentNode.SettingsButtonState? private var pictureInPictureButton: UIBarButtonItem? @@ -1155,7 +934,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() private let moreButtonStateDisposable = MetaDisposable() - private let settingsButtonStateDisposable = MetaDisposable() private let mediaPlaybackStateDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() @@ -1215,9 +993,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.isUserInteractionEnabled = true self.moreBarButton.setContent(.more(optionsCircleImage(dark: false))) - self.settingsBarButton = SettingsHeaderButton() - self.settingsBarButton.isUserInteractionEnabled = true - super.init() self.clipsToBounds = true @@ -1247,7 +1022,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) - //self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) self.footerContentNode.interacting = { [weak self] value in self?.isInteractingPromise.set(value) @@ -1256,8 +1030,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode.addSubnode(self.statusNode) self.statusButtonNode.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside) - //self.addSubnode(self.statusButtonNode) - self.footerContentNode.playbackControl = { [weak self] in if let strongSelf = self { if !strongSelf.isPaused { @@ -1391,7 +1163,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { deinit { self.statusDisposable.dispose() self.moreButtonStateDisposable.dispose() - self.settingsButtonStateDisposable.dispose() self.mediaPlaybackStateDisposable.dispose() self.scrubbingFrameDisposable?.dispose() self.hideControlsDisposable?.dispose() @@ -1864,15 +1635,27 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - self.settingsBarButton.setBadges(speed: rateString, quality: qualityString, transition: .spring(duration: 0.35)) - })) - - self.settingsButtonStateDisposable.set((self.isShowingSettingsMenuPromise.get() - |> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in - guard let self else { - return + if self.settingsButtonState != nil { + let settingsButtonState = ChatItemGalleryFooterContentNode.SettingsButtonState( + speed: rateString, + quality: qualityString + ) + if self.settingsButtonState != settingsButtonState { + self.settingsButtonState = settingsButtonState + if let validLayout = self.validLayout { + if let contentInfo = item.contentInfo { + switch contentInfo { + case let .message(message, _): + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, settingsButtonState: self.settingsButtonState, displayStickersButton: self.displayStickersButton, animated: true) + case let .webPage(webPage, media, _): + self.footerContentNode.setWebPage(webPage, media: media) + } + } + + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } } - self.settingsBarButton.setIsMenuOpen(isMenuOpen: isShowingSettingsMenu) })) self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus) @@ -1936,11 +1719,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { isPaused = false } } else if strongSelf.actionAtEnd == .stop { - strongSelf.isPlayingPromise.set(false) - strongSelf.isPlaying = false - if strongSelf.isCentral == true { - if !item.isSecret && !strongSelf.playOnDismiss { - strongSelf.updateControlsVisibility(true) + if strongSelf.isPlaying { + strongSelf.isPlayingPromise.set(false) + strongSelf.isPlaying = false + if strongSelf.isCentral == true { + if !item.isSecret && !strongSelf.playOnDismiss { + strongSelf.updateControlsVisibility(true) + } } } } @@ -2030,9 +1815,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var barButtonItems: [UIBarButtonItem] = [] if hasLinkedStickers { - let rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Stickers"), color: .white), style: .plain, target: self, action: #selector(self.openStickersButtonPressed)) - rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_Stickers - barButtonItems.append(rightBarButtonItem) + self.displayStickersButton = true } if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) { @@ -2080,9 +1863,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if !isAnimated && !disablePlayerControls { - /*let settingsMenuItem = UIBarButtonItem(customDisplayNode: self.settingsBarButton)! - settingsMenuItem.accessibilityLabel = self.presentationData.strings.Settings_Title - barButtonItems.append(settingsMenuItem)*/ hasSettingsButton = true } @@ -2093,7 +1873,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - self.hasSettingsButton = hasSettingsButton + if hasSettingsButton { + self.settingsButtonState = ChatItemGalleryFooterContentNode.SettingsButtonState( + speed: nil, + quality: nil + ) + } self._rightBarButtonItems.set(.single(barButtonItems)) videoNode.playbackCompleted = { [weak self, weak videoNode] in @@ -2148,7 +1933,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message, _): isAd = message.adAttribute != nil - self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, displaySettingsButton: self.hasSettingsButton) + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, settingsButtonState: self.settingsButtonState, displayStickersButton: self.displayStickersButton) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) } @@ -3111,7 +2896,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message, _): isAd = message.adAttribute != nil - self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, displaySettingsButton: self.hasSettingsButton) + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, settingsButtonState: self.settingsButtonState, displayStickersButton: self.displayStickersButton) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index ab98ca0550..d5ee06bab1 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -891,8 +891,13 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - node.lineHeight) / 2.0)), size: CGSize(width: bounds.size.width, height: node.lineHeight)) let foregroundContentFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) - node.backgroundNode.position = CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY) - node.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size) + if let animator { + animator.updatePosition(layer: node.backgroundNode.layer, position: CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY), completion: nil) + animator.updateBounds(layer: node.backgroundNode.layer, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size), completion: nil) + } else { + node.backgroundNode.position = CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY) + node.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size) + } node.foregroundContentNode.position = CGPoint(x: foregroundContentFrame.midX, y: foregroundContentFrame.midY) node.foregroundContentNode.bounds = CGRect(origin: CGPoint(), size: foregroundContentFrame.size) @@ -1090,10 +1095,14 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { switch self.contentNodes { case let .standard(node): if let handleNodeContainer = node.handleNodeContainer, handleNodeContainer.isUserInteractionEnabled, handleNodeContainer.frame.insetBy(dx: 0.0, dy: -16.0).contains(point) { - if let handleNode = node.handleNode, handleNode.convert(handleNode.bounds, to: self).insetBy(dx: -32.0, dy: -16.0).contains(point) { + if case .none = node.handle { return handleNodeContainer.view } else { - return nil + if let handleNode = node.handleNode, handleNode.convert(handleNode.bounds, to: self).insetBy(dx: -32.0, dy: -16.0).contains(point) { + return handleNodeContainer.view + } else { + return nil + } } } else { return nil diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index 167e3e4471..5a5f721893 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -40,9 +40,11 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shado final class ReactionContextBackgroundNode: ASDisplayNode { struct GlassParams { + var isDark: Bool var isTinted: Bool - init(isTinted: Bool) { + init(isDark: Bool, isTinted: Bool) { + self.isDark = isDark self.isTinted = isTinted } } @@ -244,7 +246,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { transition.updateFrame(view: self.backgroundView, frame: contentBounds, beginWithCurrentState: true) self.backgroundView.update(size: contentBounds.size, transition: transition) - if let glassBackgroundView = self.glassBackgroundView { + if let glass = self.glass, let glassBackgroundView = self.glassBackgroundView { var glassBackgroundFrame = contentBounds.insetBy(dx: 10.0, dy: 10.0) glassBackgroundFrame.size.height -= 8.0 transition.updateFrame(view: glassBackgroundView.container, frame: glassBackgroundFrame, beginWithCurrentState: true) @@ -255,7 +257,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { } else { glassTintColor = .init(kind: .panel) } - glassBackgroundView.container.update(size: glassBackgroundFrame.size, isDark: true, transition: ComponentTransition(transition)) + glassBackgroundView.container.update(size: glassBackgroundFrame.size, isDark: glass.isDark, transition: ComponentTransition(transition)) glassBackgroundView.view.update(size: glassBackgroundFrame.size, cornerRadius: 23.0, isDark: true, tintColor: glassTintColor, transition: ComponentTransition(transition)) transition.updateFrame(view: self.backgroundTintView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentBounds.width, height: contentBounds.height)).insetBy(dx: -10.0, dy: -10.0)) @@ -283,6 +285,13 @@ final class ReactionContextBackgroundNode: ASDisplayNode { self.backgroundClippingLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.01, delay: mainCircleDelay) self.backgroundClippingLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) self.backgroundShadowLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) + + if let glassBackgroundView = self.glassBackgroundView { + glassBackgroundView.container.alpha = 0.0 + + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.setAlpha(view: glassBackgroundView.container, alpha: 1.0) + } } func animateInFromAnchorRect(size: CGSize, sourceBackgroundFrame: CGRect) { @@ -304,6 +313,13 @@ final class ReactionContextBackgroundNode: ASDisplayNode { self.backgroundClippingLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualSourceBackgroundFrame.size)), to: NSValue(cgRect: self.backgroundClippingLayer.bounds), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) self.backgroundShadowLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceShadowFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.backgroundShadowLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceShadowFrame.size)), to: NSValue(cgRect: self.backgroundShadowLayer.bounds), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + + if let glassBackgroundView = self.glassBackgroundView { + glassBackgroundView.container.alpha = 0.0 + + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.setAlpha(view: glassBackgroundView.container, alpha: 1.0) + } } func animateOut() { @@ -314,5 +330,10 @@ final class ReactionContextBackgroundNode: ASDisplayNode { self.largeCircleShadowLayer.animateAlpha(from: CGFloat(self.largeCircleShadowLayer.opacity), to: 0.0, duration: 0.1, removeOnCompletion: false) //self.smallCircleLayer.animateAlpha(from: CGFloat(self.smallCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) self.smallCircleShadowLayer.animateAlpha(from: CGFloat(self.smallCircleShadowLayer.opacity), to: 0.0, duration: 0.1, removeOnCompletion: false) + + if let glassBackgroundView = self.glassBackgroundView { + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.setAlpha(view: glassBackgroundView.container, alpha: 0.0) + } } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 4b752d3e6a..45045f3107 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -510,7 +510,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.backgroundMaskNode = ASDisplayNode() var backgroundGlassParams: ReactionContextBackgroundNode.GlassParams? if case let .glass(isTinted) = style { - backgroundGlassParams = ReactionContextBackgroundNode.GlassParams(isTinted: isTinted) + backgroundGlassParams = ReactionContextBackgroundNode.GlassParams(isDark: presentationData.theme.overallDarkAppearance, isTinted: isTinted) } self.backgroundNode = ReactionContextBackgroundNode(glass: backgroundGlassParams, largeCircleSize: largeCircleSize, smallCircleSize: smallCircleSize, maskNode: self.backgroundMaskNode) self.leftBackgroundMaskNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift index 78cde2f0e3..b4c5117282 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift @@ -674,6 +674,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo context: reactionItems.context, animationCache: reactionItems.animationCache, presentationData: presentationData, + //style: .glass(isTinted: false), items: reactionItems.reactionItems, selectedItems: reactionItems.selectedReactionItems, title: reactionItems.reactionsTitle, @@ -1058,12 +1059,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true) + var totalActionsHeight: CGFloat = actionsSize.height + if additionalActionsSize.height != 0.0 { + totalActionsHeight += 10.0 + additionalActionsSize.height + } + var actionsFrame: CGRect if let contextExtractableContainer { let _ = contextExtractableContainer actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY), size: actionsSize) } else if case let .reference(source) = self.source, let actionsPosition = source.transitionInfo()?.actionsPosition, case .top = actionsPosition { - actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY - contentActionsSpacing - actionsSize.height), size: actionsSize) + actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY - contentActionsSpacing - totalActionsHeight), size: actionsSize) } else { actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) } diff --git a/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift b/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift index 510b0b1754..f9f7f795bc 100644 --- a/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift +++ b/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift @@ -591,11 +591,11 @@ public final class VariableBlurEffect { } public let position: Position - public let extendsInwards: Bool + public let inwardsExtension: CGFloat? - public init(position: Position, extendsInwards: Bool) { + public init(position: Position, inwardsExtension: CGFloat?) { self.position = position - self.extendsInwards = extendsInwards + self.inwardsExtension = inwardsExtension } } @@ -664,7 +664,7 @@ public final class VariableBlurEffect { let mainEffectFrame: CGRect let additionalEffectFrame: CGRect - if params.placement.extendsInwards { + if params.placement.inwardsExtension != nil { mainEffectFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) additionalEffectFrame = CGRect() } else if params.placement.position == .bottom { @@ -698,8 +698,8 @@ public final class VariableBlurEffect { let isHeightUpdated = gradient.height != self.params?.gradient.height || size.height != self.params?.size.height if isGradientUpdated { - if params.placement.extendsInwards { - let baseHeight = max(1.0, params.gradient.height) + if let inwardsExtension = params.placement.inwardsExtension { + let baseHeight = max(1.0, params.gradient.height + inwardsExtension) let resizingInverted = params.placement.position != .bottom self.gradientImage = generateImage(CGSize(width: 1.0, height: baseHeight), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -712,13 +712,17 @@ public final class VariableBlurEffect { let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! if params.placement.position == .bottom { - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: max(0.0, size.height - inwardsExtension)), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + if inwardsExtension > 0.0 { + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - inwardsExtension), size: CGSize(width: size.width, height: inwardsExtension))) + } } else { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) } })?.resizableImage(withCapInsets: UIEdgeInsets(top: resizingInverted ? baseHeight : 0.0, left: 0.0, bottom: resizingInverted ? 0.0 : baseHeight, right: 0.0), resizingMode: .stretch) } else { - self.gradientImage = EdgeEffectView.generateEdgeGradient(baseHeight: max(1.0, params.gradient.height), isInverted: params.placement.position == .bottom, extendsInwards: params.placement.extendsInwards) + self.gradientImage = EdgeEffectView.generateEdgeGradient(baseHeight: max(1.0, params.gradient.height), isInverted: params.placement.position == .bottom, extendsInwards: params.placement.inwardsExtension != nil) } } @@ -775,6 +779,62 @@ public final class VariableBlurView: UIView { } public func update(size: CGSize, constantHeight: CGFloat, isInverted: Bool, gradient: VariableBlurEffect.Gradient, transition: ContainedViewLayoutTransition) { - self.effect?.update(size: size, constantHeight: constantHeight, placement: VariableBlurEffect.Placement(position: isInverted ? .bottom : .top, extendsInwards: false), gradient: gradient, transition: transition) + self.effect?.update(size: size, constantHeight: constantHeight, placement: VariableBlurEffect.Placement(position: isInverted ? .bottom : .top, inwardsExtension: nil), gradient: gradient, transition: transition) + } +} + +public final class EdgeMaskView: UIView { + private struct MaskParams: Equatable { + let gradientHeight: CGFloat + let extensionHeight: CGFloat + + init(gradientHeight: CGFloat, extensionHeight: CGFloat) { + self.gradientHeight = gradientHeight + self.extensionHeight = extensionHeight + } + } + + private let imageView: UIImageView + + private var maskParams: MaskParams? + + override public init(frame: CGRect) { + self.imageView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.imageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(size: CGSize, color: UIColor, gradientHeight: CGFloat, extensionHeight: CGFloat, transition: ComponentTransition) { + let maskParams = MaskParams(gradientHeight: gradientHeight, extensionHeight: extensionHeight) + if maskParams != self.maskParams { + self.maskParams = maskParams + + let baseHeight = max(1.0, maskParams.gradientHeight + maskParams.extensionHeight) + let resizingInverted = !"".isEmpty + self.imageView.image = generateImage(CGSize(width: 1.0, height: baseHeight), opaque: false, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [UIColor.white.withAlphaComponent(1.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + if "".isEmpty { + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height - maskParams.extensionHeight), options: CGGradientDrawingOptions()) + } else { + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + })?.resizableImage(withCapInsets: UIEdgeInsets(top: resizingInverted ? baseHeight : 0.0, left: 0.0, bottom: resizingInverted ? 0.0 : baseHeight, right: 0.0), resizingMode: .stretch).withRenderingMode(.alwaysTemplate) + } + self.imageView.tintColor = color + transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: size)) } } diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift index 3ba9ab6695..ab004a6d8d 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift @@ -10,9 +10,38 @@ import MultilineTextComponent public final class GlassControlGroupComponent: Component { public final class Item: Equatable { - public enum Content: Hashable { + public enum Content: Equatable { case icon(String) case text(String) + case customIcon(id: AnyHashable, component: AnyComponent) + + enum Id: Hashable { + case icon(String) + case text(String) + case customIcon(AnyHashable) + } + + var id: Id { + switch self { + case let .icon(icon): + return .icon(icon) + case let .text(text): + return .text(text) + case let .customIcon(id, _): + return .customIcon(id) + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case let .icon(icon): + icon.hash(into: &hasher) + case let .text(text): + text.hash(into: &hasher) + case let .customIcon(id, _): + id.hash(into: &hasher) + } + } } public let id: AnyHashable @@ -125,7 +154,7 @@ public final class GlassControlGroupComponent: Component { var validIds: [AnyHashable] = [] var isInteractive = false for item in component.items { - let itemId = ItemId(id: item.id, contentId: item.content) + let itemId = ItemId(id: item.id, contentId: item.content.id) validIds.append(itemId) @@ -157,6 +186,8 @@ public final class GlassControlGroupComponent: Component { )) itemInsets.left = 10.0 itemInsets.right = itemInsets.left + case let .customIcon(_, customIcon): + content = customIcon } var minItemWidth: CGFloat = availableSize.height diff --git a/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift index dea2dcc6c4..3804ed3694 100644 --- a/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift +++ b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift @@ -142,19 +142,21 @@ public final class VideoPlaybackControlsComponent: Component { self.isUserInteractionEnabled = component.isVisible + let containerInset: CGFloat = 32.0 + let size = CGSize(width: component.layoutParams.sideButtonSize * 2.0 + component.layoutParams.centerButtonSize + component.layoutParams.spacing * 2.0, height: component.layoutParams.centerButtonSize) - let leftButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)) - let centerButtonFrame = CGRect(origin: CGPoint(x: component.layoutParams.sideButtonSize + component.layoutParams.spacing, y: floorToScreenPixels((size.height - component.layoutParams.centerButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.centerButtonSize, height: component.layoutParams.centerButtonSize)) - let rightButtonFrame = CGRect(origin: CGPoint(x: size.width - component.layoutParams.sideButtonSize, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)) + let leftButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)).offsetBy(dx: containerInset, dy: containerInset) + let centerButtonFrame = CGRect(origin: CGPoint(x: component.layoutParams.sideButtonSize + component.layoutParams.spacing, y: floorToScreenPixels((size.height - component.layoutParams.centerButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.centerButtonSize, height: component.layoutParams.centerButtonSize)).offsetBy(dx: containerInset, dy: containerInset) + let rightButtonFrame = CGRect(origin: CGPoint(x: size.width - component.layoutParams.sideButtonSize, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)).offsetBy(dx: containerInset, dy: containerInset) if isVisibleChanged && !transition.animation.isImmediate { self.backgroundContainer.isHidden = true self.backgroundContainer.isHidden = false } - transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundContainer.update(size: size, isDark: true, transition: transition) + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -containerInset, dy: -containerInset)) + self.backgroundContainer.update(size: CGSize(width: size.width + containerInset * 2.0, height: size.height + containerInset * 2.0), isDark: true, transition: transition) let areSideButtonsVisible = component.isVisible && component.displaySeekControls let buttonsTintColor: GlassBackgroundView.TintColor = .init(kind: .custom(style: .clear, color: UIColor(white: 0.0, alpha: 0.2))) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 7b19171d2c..a916a270ac 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2256,6 +2256,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let value = self.backgroundNode.makeEdgeEffectNode() { bottomBackgroundEdgeEffectNode = value self.bottomBackgroundEdgeEffectNode = value + value.isUserInteractionEnabled = false self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view) } } @@ -2466,6 +2467,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let value = self.backgroundNode.makeEdgeEffectNode() { topBackgroundEdgeEffectNode = value self.topBackgroundEdgeEffectNode = value + value.isUserInteractionEnabled = false self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view) } } diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift index ff46717703..3cc29ae11f 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift @@ -121,6 +121,7 @@ final class WallpaperEdgeEffectNodeImpl: ASDisplayNode, WallpaperEdgeEffectNode return } self.contentNode.contents = parentNode.contentNode.contents + self.contentNode.contentMode = parentNode.contentNode.contentMode self.contentNode.backgroundColor = parentNode.contentNode.backgroundColor self.contentNode.alpha = parentNode.contentNode.alpha self.contentNode.isHidden = parentNode.contentNode.isHidden