From c21ebb06b542f52d6ee63f55f8038f8853ea23c4 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 24 Jan 2025 21:03:39 +0400 Subject: [PATCH] Finalize timestamp sharing --- .../Sources/ShareController.swift | 14 +- .../ChatItemGalleryFooterContentNode.swift | 33 +++ .../ChatVideoGalleryItemScrubberView.swift | 81 +++++++ .../Sources/GalleryControllerNode.swift | 16 +- .../Sources/GalleryFooterContentNode.swift | 9 + .../GalleryUI/Sources/GalleryFooterNode.swift | 26 +++ .../GalleryUI/Sources/GalleryItemNode.swift | 1 + .../Sources/GalleryItemTransitionNode.swift | 34 +++ .../GalleryUI/Sources/GalleryPagerNode.swift | 2 + .../Items/UniversalVideoGalleryItem.swift | 37 +++- .../InstantPagePlayableVideoNode.swift | 4 + .../Sources/MediaPlayerScrubbingNode.swift | 8 + submodules/ShareController/BUILD | 1 + .../Sources/ShareActionButtonNode.swift | 3 + .../Sources/ShareController.swift | 8 + .../Sources/ShareControllerNode.swift | 90 +++++++- .../Sources/ShareInputFieldNode.swift | 197 +++++++++++++++++- .../Sources/AnimatedTextComponent.swift | 16 +- .../ChatMessageInteractiveMediaNode.swift | 143 ++++++++++++- .../Sources/PeerReportScreen.swift | 4 +- .../ReportPeerDetailsActionSheetItem.swift | 12 +- 21 files changed, 696 insertions(+), 43 deletions(-) diff --git a/submodules/AccountContext/Sources/ShareController.swift b/submodules/AccountContext/Sources/ShareController.swift index 19281cc1fc..122c247bda 100644 --- a/submodules/AccountContext/Sources/ShareController.swift +++ b/submodules/AccountContext/Sources/ShareController.swift @@ -49,11 +49,23 @@ public enum ShareControllerError { } public enum ShareControllerSubject { + public final class PublicLinkPrefix { + public let visibleString: String + public let actualString: String + + public init(visibleString: String, actualString: String) { + self.visibleString = visibleString + self.actualString = actualString + } + } + public final class MediaParameters { public let startAtTimestamp: Int32? + public let publicLinkPrefix: PublicLinkPrefix? - public init(startAtTimestamp: Int32?) { + public init(startAtTimestamp: Int32?, publicLinkPrefix: PublicLinkPrefix?) { self.startAtTimestamp = startAtTimestamp + self.publicLinkPrefix = publicLinkPrefix } } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 5d1067eb15..04264bd336 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -1367,6 +1367,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll return panelHeight } + override func animateIn(transition: ContainedViewLayoutTransition) { + self.contentNode.alpha = 0.0 + transition.updateAlpha(node: self.contentNode, alpha: self.visibilityAlpha) + } + override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) { if let scrubberView = self.scrubberView, scrubberView.superview == self.view { if let previousContentNode = previousContentNode as? ChatItemGalleryFooterContentNode, previousContentNode.scrubberView != nil { @@ -1392,6 +1397,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.scrollWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } + override func animateOut(transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.contentNode, alpha: 0.0) + } + override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { if let scrubberView = self.scrubberView, scrubberView.superview == self.view { if let nextContentNode = nextContentNode as? ChatItemGalleryFooterContentNode, nextContentNode.scrubberView != nil { @@ -1718,6 +1727,30 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll shareController.dismissed = { [weak self] _ in self?.interacting?(false) } + shareController.onMediaTimestampLinkCopied = { [weak self] timestamp in + guard let self else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let text: String + if let timestamp { + //TODO:localize + let startTimeString: String + let hours = timestamp / (60 * 60) + let minutes = timestamp % (60 * 60) / 60 + let seconds = timestamp % 60 + if hours != 0 { + startTimeString = String(format: "%d:%02d:%02d", hours, minutes, seconds) + } else { + startTimeString = String(format: "%d:%02d", minutes, seconds) + } + text = "Link with start time at \(startTimeString) copied to clipboard." + } else { + text = presentationData.strings.Conversation_LinkCopied + } + + self.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) + } shareController.actionCompleted = { [weak self] in if let strongSelf = self, let actionCompletionText = actionCompletionText { diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index b98b543865..866455929a 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -42,6 +42,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { private var currentChapter: MediaPlayerScrubbingChapter? + private var isAnimatedOut: Bool = false + var hideWhenDurationIsUnknown = false { didSet { if self.hideWhenDurationIsUnknown { @@ -150,6 +152,9 @@ final class ChatVideoGalleryItemScrubberView: UIView { } func updateTimestampsVisibility(animated: Bool) { + if self.isAnimatedOut { + return + } let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate let alpha: CGFloat = self.isCollapsed == true || self.isLoading ? 0.0 : 1.0 transition.updateAlpha(node: self.leftTimestampNode, alpha: alpha) @@ -375,4 +380,80 @@ final class ChatVideoGalleryItemScrubberView: UIView { } return hitTestRect.contains(point) } + + func animateIn(from scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) { + if let scrubberTransition { + let fromRect = scrubberTransition.view.convert(scrubberTransition.view.bounds, to: self) + + let targetCloneView = scrubberTransition.makeView() + self.addSubview(targetCloneView) + targetCloneView.frame = fromRect + scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 0.0), .immediate) + targetCloneView.alpha = 1.0 + + transition.updateFrame(view: targetCloneView, frame: CGRect(origin: CGPoint(x: self.scrubberNode.frame.minX, y: self.scrubberNode.frame.maxY - fromRect.height - 3.0), size: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height))) + scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 1.0), transition) + let scrubberTransitionView = scrubberTransition.view + scrubberTransitionView.isHidden = true + ContainedViewLayoutTransition.animated(duration: 0.08, curve: .easeInOut).updateAlpha(layer: targetCloneView.layer, alpha: 0.0, completion: { [weak scrubberTransitionView, weak targetCloneView] _ in + scrubberTransitionView?.isHidden = false + targetCloneView?.removeFromSuperview() + }) + + let scrubberSourceRect = CGRect(origin: CGPoint(x: fromRect.minX, y: fromRect.maxY - 3.0), size: CGSize(width: fromRect.width, height: 3.0)) + + let leftTimestampOffset = CGPoint(x: self.leftTimestampNode.position.x - self.scrubberNode.frame.minX, y: self.leftTimestampNode.position.y - self.scrubberNode.frame.maxY) + let rightTimestampOffset = CGPoint(x: self.rightTimestampNode.position.x - self.scrubberNode.frame.maxX, y: self.rightTimestampNode.position.y - self.scrubberNode.frame.maxY) + + transition.animatePosition(node: self.scrubberNode, from: scrubberSourceRect.center) + self.scrubberNode.animateWidth(from: scrubberSourceRect.width, transition: transition) + + transition.animatePosition(node: self.leftTimestampNode, from: CGPoint(x: leftTimestampOffset.x + scrubberSourceRect.minX, y: leftTimestampOffset.y + scrubberSourceRect.maxY)) + transition.animatePosition(node: self.rightTimestampNode, from: CGPoint(x: rightTimestampOffset.x + scrubberSourceRect.maxX, y: rightTimestampOffset.y + scrubberSourceRect.maxY)) + } + + self.scrubberNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25) + self.leftTimestampNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25) + self.rightTimestampNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25) + self.infoNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25) + } + + func animateOut(to scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) { + self.isAnimatedOut = true + + if let scrubberTransition { + let toRect = scrubberTransition.view.convert(scrubberTransition.view.bounds, to: self) + let scrubberDestinationRect = CGRect(origin: CGPoint(x: toRect.minX, y: toRect.maxY - 3.0), size: CGSize(width: toRect.width, height: 3.0)) + + let targetCloneView = scrubberTransition.makeView() + self.addSubview(targetCloneView) + targetCloneView.frame = CGRect(origin: CGPoint(x: self.scrubberNode.frame.minX, y: self.scrubberNode.frame.maxY - toRect.height), size: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height)) + scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 0.0), .immediate) + targetCloneView.alpha = 0.0 + + transition.updateFrame(view: targetCloneView, frame: toRect) + scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 1.0), transition) + let scrubberTransitionView = scrubberTransition.view + scrubberTransitionView.isHidden = true + transition.updateAlpha(layer: targetCloneView.layer, alpha: 1.0, completion: { [weak scrubberTransitionView] _ in + scrubberTransitionView?.isHidden = false + }) + + let leftTimestampOffset = CGPoint(x: self.leftTimestampNode.position.x - self.scrubberNode.frame.minX, y: self.leftTimestampNode.position.y - self.scrubberNode.frame.maxY) + let rightTimestampOffset = CGPoint(x: self.rightTimestampNode.position.x - self.scrubberNode.frame.maxX, y: self.rightTimestampNode.position.y - self.scrubberNode.frame.maxY) + + transition.animatePositionAdditive(layer: self.scrubberNode.layer, offset: CGPoint(), to: CGPoint(x: scrubberDestinationRect.midX - self.scrubberNode.position.x, y: scrubberDestinationRect.midY - self.scrubberNode.position.y), removeOnCompletion: false) + + self.scrubberNode.animateWidth(to: scrubberDestinationRect.width, transition: transition) + + transition.animatePositionAdditive(layer: self.leftTimestampNode.layer, offset: CGPoint(), to: CGPoint(x: -self.leftTimestampNode.position.x + (leftTimestampOffset.x + scrubberDestinationRect.minX), y: -self.leftTimestampNode.position.y + (leftTimestampOffset.y + scrubberDestinationRect.maxY)), removeOnCompletion: false) + + transition.animatePositionAdditive(layer: self.rightTimestampNode.layer, offset: CGPoint(), to: CGPoint(x: -self.rightTimestampNode.position.x + (rightTimestampOffset.x + scrubberDestinationRect.maxX), y: -self.rightTimestampNode.position.y + (rightTimestampOffset.y + scrubberDestinationRect.maxY)), removeOnCompletion: false) + } + + transition.updateAlpha(layer: self.scrubberNode.layer, alpha: 0.0) + transition.updateAlpha(layer: self.leftTimestampNode.layer, alpha: 0.0) + transition.updateAlpha(layer: self.rightTimestampNode.layer, alpha: 0.0) + transition.updateAlpha(layer: self.infoNode.layer, alpha: 0.0) + } } diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 3553e7093c..4094cd3556 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -83,6 +83,13 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } } + self.pager.controlsVisibility = { [weak self] in + guard let self else { + return true + } + return !self.areControlsHidden && self.footerNode.alpha != 0.0 + } + self.pager.updateOrientation = { [weak self] orientation in if let strongSelf = self { strongSelf.updateOrientation?(orientation) @@ -364,11 +371,15 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture if !self.areControlsHidden { self.statusBar?.alpha = 1.0 self.navigationBar?.alpha = 1.0 - self.footerNode.alpha = 1.0 self.updateThumbnailContainerNodeAlpha(.immediate) } }) + if !self.areControlsHidden { + self.footerNode.alpha = 1.0 + self.footerNode.animateIn(transition: .animated(duration: 0.15, curve: .linear)) + } + if animateContent { self.scrollView.layer.animateBounds(from: self.scrollView.layer.bounds.offsetBy(dx: 0.0, dy: -self.scrollView.layer.bounds.size.height), to: self.scrollView.layer.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } else if useSimpleAnimation { @@ -402,13 +413,14 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture UIView.animate(withDuration: 0.1, animations: { self.statusBar?.alpha = 0.0 self.navigationBar?.alpha = 0.0 - self.footerNode.alpha = 0.0 self.currentThumbnailContainerNode?.alpha = 0.0 }, completion: { _ in interfaceAnimationCompleted = true intermediateCompletion() }) + self.footerNode.animateOut(transition: .animated(duration: 0.1, curve: .easeInOut)) + if animateContent { contentAnimationCompleted = false self.scrollView.layer.animateBounds(from: self.scrollView.layer.bounds, to: self.scrollView.layer.bounds.offsetBy(dx: 0.0, dy: -self.scrollView.layer.bounds.size.height), duration: 0.25, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { _ in diff --git a/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift index 4fa4895e78..e4d02722a8 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift @@ -37,9 +37,18 @@ open class GalleryFooterContentNode: ASDisplayNode { return 0.0 } + open func animateIn(transition: ContainedViewLayoutTransition) { + self.alpha = 0.0 + transition.updateAlpha(node: self, alpha: 1.0) + } + open func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) { } + open func animateOut(transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self, alpha: 0.0) + } + open func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { completion() } diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index 673c4e5a31..e37b28dea0 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -32,6 +32,32 @@ public final class GalleryFooterNode: ASDisplayNode { self.currentOverlayContentNode?.setVisibilityAlpha(alpha) } + func animateIn(transition: ContainedViewLayoutTransition) { + self.backgroundNode.alpha = 0.0 + transition.updateAlpha(node: self.backgroundNode, alpha: 1.0) + + if let currentFooterContentNode = self.currentFooterContentNode { + currentFooterContentNode.animateIn(transition: transition) + } + + if let currentOverlayContentNode = self.currentOverlayContentNode { + currentOverlayContentNode.alpha = 0.0 + transition.updateAlpha(node: currentOverlayContentNode, alpha: 1.0) + } + } + + func animateOut(transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.backgroundNode, alpha: 0.0) + + if let currentFooterContentNode = self.currentFooterContentNode { + currentFooterContentNode.animateOut(transition: transition) + } + + if let currentOverlayContentNode = self.currentOverlayContentNode { + transition.updateAlpha(node: currentOverlayContentNode, alpha: 0.0) + } + } + public func updateLayout(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, footerContentNode: GalleryFooterContentNode?, overlayContentNode: GalleryOverlayContentNode?, thumbnailPanelHeight: CGFloat, isHidden: Bool, transition: ContainedViewLayoutTransition) { self.currentLayout = (layout, navigationBarHeight, thumbnailPanelHeight, isHidden) let cleanInsets = layout.insets(options: []) diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 258aa016e0..b9d436ed43 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -27,6 +27,7 @@ open class GalleryItemNode: ASDisplayNode { public var toggleControlsVisibility: () -> Void = { } public var updateControlsVisibility: (Bool) -> Void = { _ in } + public var controlsVisibility: () -> Bool = { return true } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in } diff --git a/submodules/GalleryUI/Sources/GalleryItemTransitionNode.swift b/submodules/GalleryUI/Sources/GalleryItemTransitionNode.swift index 807c3d8148..7be9c6d783 100644 --- a/submodules/GalleryUI/Sources/GalleryItemTransitionNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemTransitionNode.swift @@ -1,8 +1,42 @@ import Foundation +import UIKit import AccountContext +import Display + +public final class GalleryItemScrubberTransition { + public struct TransitionState: Equatable { + public var sourceSize: CGSize + public var destinationSize: CGSize + public var progress: CGFloat + + public init( + sourceSize: CGSize, + destinationSize: CGSize, + progress: CGFloat + ) { + self.sourceSize = sourceSize + self.destinationSize = destinationSize + self.progress = progress + } + } + + public let view: UIView + public let makeView: () -> UIView + public let updateView: (UIView, TransitionState, ContainedViewLayoutTransition) -> Void + public let insertCloneTransitionView: ((UIView) -> Void)? + + public init(view: UIView, makeView: @escaping () -> UIView, updateView: @escaping (UIView, TransitionState, ContainedViewLayoutTransition) -> Void, insertCloneTransitionView: ((UIView) -> Void)?) { + self.view = view + self.makeView = makeView + self.updateView = updateView + self.insertCloneTransitionView = insertCloneTransitionView + } +} public protocol GalleryItemTransitionNode: AnyObject { func isAvailableForGalleryTransition() -> Bool func isAvailableForInstantPageTransition() -> Bool var decoration: UniversalVideoDecoration? { get } + + func scrubberTransition() -> GalleryItemScrubberTransition? } diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 892f00ac50..2fdac78959 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -114,6 +114,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest public var centralItemIndexOffsetUpdated: (([GalleryItem]?, Int, CGFloat)?) -> Void = { _ in } public var toggleControlsVisibility: () -> Void = { } public var updateControlsVisibility: (Bool) -> Void = { _ in } + public var controlsVisibility: () -> Bool = { return true } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in } @@ -595,6 +596,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest let node = self.items[index].node(synchronous: synchronous) node.toggleControlsVisibility = self.toggleControlsVisibility node.updateControlsVisibility = self.updateControlsVisibility + node.controlsVisibility = self.controlsVisibility node.updateOrientation = self.updateOrientation node.dismiss = self.dismiss node.beginCustomDismiss = self.beginCustomDismiss diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 677647b671..ac39dd5b8c 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1404,17 +1404,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.clipsToBounds = true - //TODO:wip-release - /*self.footerContentNode.shareMediaParameters = { [weak self] in + self.footerContentNode.shareMediaParameters = { [weak self] in guard let self, let playerStatusValue = self.playerStatusValue else { return nil } + if playerStatusValue.duration >= 60.0 * 10.0 { - return ShareControllerSubject.MediaParameters(startAtTimestamp: Int32(playerStatusValue.timestamp)) + var publicLinkPrefix: ShareControllerSubject.PublicLinkPrefix? + if case let .message(message, _) = self.item?.contentInfo, message.id.namespace == Namespaces.Message.Cloud, let peer = message.peers[message.id.peerId] as? TelegramChannel, let username = peer.username { + let visibleString = "t.me/\(username)/\(message.id.id)" + publicLinkPrefix = ShareControllerSubject.PublicLinkPrefix( + visibleString: visibleString, + actualString: "https://\(visibleString)" + ) + } + + return ShareControllerSubject.MediaParameters( + startAtTimestamp: Int32(playerStatusValue.timestamp), + publicLinkPrefix: publicLinkPrefix + ) } else { return nil } - }*/ + } self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) @@ -2456,6 +2468,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if let node = node.0 as? OverlayMediaItemNode, self.context.sharedContext.mediaManager.hasOverlayVideoNode(node) { + if let scrubberView = self.scrubberView { + scrubberView.animateIn(from: nil, transition: .animated(duration: 0.25, curve: .spring)) + } + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) @@ -2471,6 +2487,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.context.sharedContext.mediaManager.setOverlayVideoNode(nil) } else { + if let scrubberView = self.scrubberView { + let scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition() + scrubberView.animateIn(from: scrubberTransition, transition: .animated(duration: 0.25, curve: .spring)) + } + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) var transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) var transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) @@ -2570,6 +2591,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } + if let scrubberView = self.scrubberView { + var scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition() + if !self.controlsVisibility() { + scrubberTransition = nil + } + scrubberView.animateOut(to: scrubberTransition, transition: .animated(duration: 0.25, curve: .spring)) + } + let transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) var transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift index 12efc18acd..97a545133d 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift @@ -177,4 +177,8 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler } } } + + func scrubberTransition() -> GalleryItemScrubberTransition? { + return nil + } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index ff460a79ff..c9ad9ba20a 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -1091,4 +1091,12 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { } } } + + public func animateWidth(from: CGFloat, transition: ContainedViewLayoutTransition) { + transition.animateTransformScale(layer: self.layer, from: CGPoint(x: from / self.bounds.width, y: 1.0)) + } + + public func animateWidth(to: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateTransformScale(node: self, scale: CGPoint(x: to / self.bounds.width, y: 1.0)) + } } diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index f20bc90047..6f795ea13c 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/UndoUI", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/MessageInputPanelComponent", diff --git a/submodules/ShareController/Sources/ShareActionButtonNode.swift b/submodules/ShareController/Sources/ShareActionButtonNode.swift index c63b3df2ae..d43807b0b3 100644 --- a/submodules/ShareController/Sources/ShareActionButtonNode.swift +++ b/submodules/ShareController/Sources/ShareActionButtonNode.swift @@ -132,6 +132,8 @@ public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode { return self.checkNode.selected } + public var updated: (() -> Void)? + public init(titleText: String, titleTextColor: UIColor, checkNodeTheme: CheckNodeTheme) { self.titleText = titleText self.titleTextColor = titleTextColor @@ -154,6 +156,7 @@ public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode { @objc private func pressed() { self.checkNode.setSelected(!self.checkNode.selected, animated: true) + self.updated?() } override public func layout() { diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index ff18c24765..5d870191ef 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -436,6 +436,8 @@ public final class ShareController: ViewController { public var debugAction: (() -> Void)? + public var onMediaTimestampLinkCopied: ((Int32?) -> Void)? + public var parentNavigationController: NavigationController? public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false, collectibleItemInfo: TelegramCollectibleItemInfo? = nil) { @@ -1210,6 +1212,12 @@ public final class ShareController: ViewController { return false }), in: .current) } + self.controllerNode.onMediaTimestampLinkCopied = { [weak self] timestamp in + guard let self else { + return + } + self.onMediaTimestampLinkCopied?(timestamp) + } self.controllerNode.debugAction = { [weak self] in self?.debugAction?() } diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 0da8a2d193..89857d4a6e 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -326,6 +326,8 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate private let fromPublicChannel: Bool private let segmentedValues: [ShareControllerSegmentedValue]? private let collectibleItemInfo: TelegramCollectibleItemInfo? + private let mediaParameters: ShareControllerSubject.MediaParameters? + var selectedSegmentedIndex: Int = 0 private let defaultAction: ShareControllerAction? @@ -365,6 +367,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate var enqueued: (([PeerId], [Int64]) -> Void)? var present: ((ViewController) -> Void)? var disabledPeerSelected: ((EnginePeer) -> Void)? + var onMediaTimestampLinkCopied: ((Int32?) -> Void)? let ready = Promise() @@ -397,6 +400,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.fromPublicChannel = fromPublicChannel self.segmentedValues = segmentedValues self.collectibleItemInfo = collectibleItemInfo + self.mediaParameters = mediaParameters self.presetText = presetText @@ -471,7 +475,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.startAtTimestampNode = nil } - self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), placeholder: self.presentationData.strings.ShareMenu_Comment) + self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), strings: self.presentationData.strings, placeholder: self.presentationData.strings.ShareMenu_Comment) self.inputFieldNode.text = presetText ?? "" self.inputFieldNode.preselectText() self.inputFieldNode.alpha = 0.0 @@ -490,6 +494,15 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate super.init() self.isHidden = true + + self.startAtTimestampNode?.updated = { [weak self] in + guard let self else { + return + } + if let (layout, navigationBarHeight, _) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } self.actionButtonNode.shouldBegin = { [weak self] in if let strongSelf = self { @@ -593,7 +606,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate } if !openedTopicList { - strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil, inputField: true, actions: strongSelf.defaultAction == nil) + strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil && strongSelf.mediaParameters?.publicLinkPrefix == nil, inputField: true, actions: strongSelf.defaultAction == nil) strongSelf.updateButton() @@ -686,10 +699,44 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate } } } + self.inputFieldNode.onInputCopyText = { [weak self] in + guard let self else { + return + } + if let publicLinkPrefix = self.mediaParameters?.publicLinkPrefix { + var timestampSuffix = "" + var effectiveStartTimestamp: Int32? + if let startAtTimestamp = self.mediaParameters?.startAtTimestamp, let startAtTimestampNode = self.startAtTimestampNode, startAtTimestampNode.value { + var startAtTimestampString = "" + let hours = startAtTimestamp / 3600 + let minutes = startAtTimestamp / 60 % 60 + let seconds = startAtTimestamp % 60 + if hours == 0 && minutes == 0 { + startAtTimestampString = "\(startAtTimestamp)" + } else { + if hours != 0 { + startAtTimestampString += "\(hours)h" + } + if minutes != 0 { + startAtTimestampString += "\(minutes)m" + } + if seconds != 0 { + startAtTimestampString += "\(seconds)s" + } + } + timestampSuffix = "?t=\(startAtTimestampString)" + effectiveStartTimestamp = startAtTimestamp + } + let inputCopyText = "\(publicLinkPrefix.actualString)\(timestampSuffix)" + UIPasteboard.general.string = inputCopyText + self.onMediaTimestampLinkCopied?(effectiveStartTimestamp) + } + self.cancel?() + } self.updateButton() - if self.presetText != nil { + if self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil { self.setActionNodesHidden(false, inputField: true, actions: true, animated: false) } } @@ -971,7 +1018,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if contentNode is ShareSearchContainerNode { self.setActionNodesHidden(true, inputField: true, actions: true) } else if !(contentNode is ShareLoadingContainer) { - self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil, actions: true) + self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil, actions: true) } } else { if let contentNode = self.contentNode { @@ -1021,16 +1068,45 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate var bottomGridInset: CGFloat = 0 var actionButtonHeight: CGFloat = 0 - if self.defaultAction != nil || !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil { + if self.defaultAction != nil || !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil { actionButtonHeight = buttonHeight bottomGridInset += actionButtonHeight } if self.startAtTimestampNode != nil { bottomGridInset += buttonHeight } - - let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, transition: transition) + + var inputCopyText: String? if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil { + } else { + if let publicLinkPrefix = self.mediaParameters?.publicLinkPrefix { + var timestampSuffix = "" + if let startAtTimestamp = self.mediaParameters?.startAtTimestamp, let startAtTimestampNode = self.startAtTimestampNode, startAtTimestampNode.value { + var startAtTimestampString = "" + let hours = startAtTimestamp / 3600 + let minutes = startAtTimestamp / 60 % 60 + let seconds = startAtTimestamp % 60 + if hours == 0 && minutes == 0 { + startAtTimestampString = "\(startAtTimestamp)" + } else { + if hours != 0 { + startAtTimestampString += "\(hours)h" + } + if minutes != 0 { + startAtTimestampString += "\(minutes)m" + } + if seconds != 0 { + startAtTimestampString += "\(seconds)s" + } + } + timestampSuffix = "?t=\(startAtTimestampString)" + } + inputCopyText = "\(publicLinkPrefix.visibleString)\(timestampSuffix)" + } + } + + let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, inputCopyText: inputCopyText, transition: transition) + if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil { bottomGridInset += inputHeight } diff --git a/submodules/ShareController/Sources/ShareInputFieldNode.swift b/submodules/ShareController/Sources/ShareInputFieldNode.swift index 5ad9bf6393..014c980eb9 100644 --- a/submodules/ShareController/Sources/ShareInputFieldNode.swift +++ b/submodules/ShareController/Sources/ShareInputFieldNode.swift @@ -4,6 +4,9 @@ import AsyncDisplayKit import Display import TelegramPresentationData import AppBundle +import ComponentFlow +import MultilineTextComponent +import AnimatedTextComponent private func generateClearIcon(color: UIColor) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) @@ -55,19 +58,154 @@ public extension ShareInputFieldNodeTheme { } } +private final class ShareInputCopyComponent: Component { + let theme: ShareInputFieldNodeTheme + let strings: PresentationStrings + let text: String + let action: () -> Void + + init( + theme: ShareInputFieldNodeTheme, + strings: PresentationStrings, + text: String, + action: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.text = text + self.action = action + } + + static func ==(lhs: ShareInputCopyComponent, rhs: ShareInputCopyComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + final class View: UIView { + let text = ComponentView() + let button = ComponentView() + let textMask = UIImageView() + + var component: ShareInputCopyComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ShareInputCopyComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let textChanged = self.component != nil && self.component?.text != component.text + self.component = component + + var textItems: [AnimatedTextComponent.Item] = [] + if let range = component.text.range(of: "?", options: .backwards) { + textItems.append(AnimatedTextComponent.Item(id: 0, isUnbreakable: true, content: .text(String(component.text[component.text.startIndex ..< range.lowerBound])))) + textItems.append(AnimatedTextComponent.Item(id: 1, isUnbreakable: true, content: .text(String(component.text[range.lowerBound...])))) + } else { + textItems.append(AnimatedTextComponent.Item(id: 0, isUnbreakable: true, content: .text(component.text))) + } + + let sideInset: CGFloat = 12.0 + let textSize = self.text.update( + transition: textChanged ? .spring(duration: 0.4) : .immediate, + component: AnyComponent(AnimatedTextComponent( + font: Font.regular(17.0), + color: component.theme.textColor, + items: textItems, + animateScale: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((availableSize.height - textSize.height) * 0.5)), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + textView.mask = self.textMask + } + textView.frame = textFrame + } + + let buttonSize = self.button.update( + transition: .immediate, + component: AnyComponent(Button( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.Conversation_LinkDialogCopy, font: Font.regular(17.0), textColor: component.theme.accentColor)) + )), + action: { [weak self] in + guard let self else { + return + } + self.component?.action() + } + ).minSize(CGSize(width: 0.0, height: availableSize.height))), + environment: {}, + containerSize: CGSize(width: availableSize.width - 40.0, height: 1000.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - buttonSize.width, y: floor((availableSize.height - buttonSize.height) * 0.5)), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + buttonView.frame = buttonFrame + } + + if self.textMask.image == nil { + let gradientWidth: CGFloat = 26.0 + self.textMask.image = generateGradientImage(size: CGSize(width: gradientWidth, height: 8.0), colors: [ + UIColor(white: 1.0, alpha: 1.0), + UIColor(white: 1.0, alpha: 1.0), + UIColor(white: 1.0, alpha: 0.0) + ], locations: [ + 0.0, + 1.0 / gradientWidth, + 1.0 + ], direction: .horizontal)?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: gradientWidth - 1.0), resizingMode: .stretch) + self.textMask.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, buttonFrame.minX - 4.0 - textFrame.minX), height: textFrame.height)) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { private let theme: ShareInputFieldNodeTheme + private let strings: PresentationStrings private let backgroundNode: ASImageNode private let textInputNode: EditableTextNode private let placeholderNode: ASTextNode private let clearButton: HighlightableButtonNode + private var copyView: ComponentView? + public var updateHeight: (() -> Void)? public var updateText: ((String) -> Void)? private let backgroundInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 1.0, right: 16.0) private let inputInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 10.0, right: 22.0) private let accessoryButtonsWidth: CGFloat = 10.0 + private var inputCopyText: String? + public var onInputCopyText: (() -> Void)? private var selectTextOnce: Bool = false @@ -77,7 +215,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat } set { self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.textColor) - self.placeholderNode.isHidden = !newValue.isEmpty + self.placeholderNode.isHidden = !newValue.isEmpty || self.inputCopyText != nil self.clearButton.isHidden = newValue.isEmpty } } @@ -88,8 +226,9 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat } } - public init(theme: ShareInputFieldNodeTheme, placeholder: String) { + public init(theme: ShareInputFieldNodeTheme, strings: PresentationStrings, placeholder: String) { self.theme = theme + self.strings = strings self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -136,10 +275,11 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat self.selectTextOnce = true } - public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + public func updateLayout(width: CGFloat, inputCopyText: String?, transition: ContainedViewLayoutTransition) -> CGFloat { let backgroundInsets = self.backgroundInsets let inputInsets = self.inputInsets let accessoryButtonsWidth = self.accessoryButtonsWidth + self.inputCopyText = inputCopyText let textFieldHeight = self.calculateTextFieldMetrics(width: width) let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom @@ -156,6 +296,43 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: backgroundFrame.size.height))) + self.textInputNode.isUserInteractionEnabled = inputCopyText == nil + self.textInputNode.isHidden = inputCopyText != nil + self.placeholderNode.isHidden = !(self.textInputNode.textView.text ?? "").isEmpty || self.inputCopyText != nil + + if let inputCopyText { + let copyView: ComponentView + if let current = self.copyView { + copyView = current + } else { + copyView = ComponentView() + self.copyView = copyView + } + let copyViewSize = copyView.update( + transition: .immediate, + component: AnyComponent(ShareInputCopyComponent( + theme: self.theme, + strings: self.strings, + text: inputCopyText, + action: { + self.onInputCopyText?() + } + )), + environment: {}, + containerSize: backgroundFrame.size + ) + let copyViewFrame = CGRect(origin: backgroundFrame.origin, size: copyViewSize) + if let copyComponentView = copyView.view { + if copyComponentView.superview == nil { + self.view.addSubview(copyComponentView) + } + copyComponentView.frame = copyViewFrame + } + } else if let copyView = self.copyView { + self.copyView = nil + copyView.view?.removeFromSuperview() + } + return panelHeight } @@ -170,7 +347,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { self.updateTextNodeText(animated: true) self.updateText?(editableTextNode.attributedText?.string ?? "") - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty || self.inputCopyText != nil } public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { @@ -185,7 +362,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat } public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty || self.inputCopyText != nil self.clearButton.isHidden = true } @@ -194,9 +371,13 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat let inputInsets = self.inputInsets let accessoryButtonsWidth = self.accessoryButtonsWidth - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(41.0, unboundTextFieldHeight)) + if self.inputCopyText != nil { + return 41.0 + } else { + let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(41.0, unboundTextFieldHeight)) + } } private func updateTextNodeText(animated: Bool) { diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index b3ead20bb7..0967a73fbe 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -26,17 +26,20 @@ public final class AnimatedTextComponent: Component { public let color: UIColor public let items: [Item] public let noDelay: Bool + public let animateScale: Bool public init( font: UIFont, color: UIColor, items: [Item], - noDelay: Bool = false + noDelay: Bool = false, + animateScale: Bool = true ) { self.font = font self.color = color self.items = items self.noDelay = noDelay + self.animateScale = animateScale } public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool { @@ -52,6 +55,9 @@ public final class AnimatedTextComponent: Component { if lhs.noDelay != rhs.noDelay { return false } + if lhs.animateScale != rhs.animateScale { + return false + } return true } @@ -172,7 +178,9 @@ public final class AnimatedTextComponent: Component { } } - characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring) + if component.animateScale { + characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring) + } characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true) characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth) } @@ -202,7 +210,9 @@ public final class AnimatedTextComponent: Component { outFirstDelayWidth = characterComponentView.frame.minX } - outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth) + if component.animateScale { + outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth) + } outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: characterComponentView.center.y - characterComponentView.bounds.height * 0.4), delay: delayNorm * delayWidth) outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in characterComponentView?.removeFromSuperview() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 7bea53e5cf..419e2dafaa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -626,13 +626,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr transition.updateAlpha(node: statusNode, alpha: 1.0 - factor) } } - - self.imageNode.imageUpdated = { [weak self] image in - guard let self else { - return - } - self.timestampMaskView?.image = image - } } deinit { @@ -794,6 +787,66 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } + private struct MaskImageCornerKey: Hashable { + var bottomLeft: CGFloat + var bottomRight: CGFloat + var leftInset: CGFloat + var rightInset: CGFloat + + init(bottomLeft: CGFloat, bottomRight: CGFloat, leftInset: CGFloat, rightInset: CGFloat) { + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + self.leftInset = leftInset + self.rightInset = rightInset + } + } + private static var timestampMaskImageCache: [MaskImageCornerKey: UIImage] = [:] + + private func generateTimestampMaskImage(corners: ImageCorners) -> UIImage? { + var insets = corners.extendedEdges + insets.top = 0.0 + insets.bottom = 0.0 + + let cacheKey = MaskImageCornerKey(bottomLeft: corners.bottomLeft.radius, bottomRight: corners.bottomRight.radius, leftInset: insets.left, rightInset: insets.right) + if let image = ChatMessageInteractiveMediaNode.timestampMaskImageCache[cacheKey] { + return image + } + + let imageSize = CGSize(width: corners.bottomLeft.radius + corners.bottomRight.radius + insets.left + insets.right + 1.0, height: 1.0 + max(corners.bottomLeft.radius, corners.bottomRight.radius)) + + guard let context = DrawingContext(size: imageSize, clear: true) else { + return nil + } + + context.withContext { c in + c.setFillColor(UIColor.white.cgColor) + c.move(to: CGPoint(x: insets.left, y: insets.top)) + c.addLine(to: CGPoint(x: insets.left, y: imageSize.height - insets.bottom - corners.bottomLeft.radius)) + c.addArc(tangent1End: CGPoint(x: insets.left, y: imageSize.height - insets.bottom), tangent2End: CGPoint(x: insets.left + corners.bottomLeft.radius, y: imageSize.height - insets.bottom), radius: corners.bottomLeft.radius) + c.addLine(to: CGPoint(x: imageSize.width - insets.right - corners.bottomRight.radius, y: imageSize.height - insets.bottom)) + c.addArc(tangent1End: CGPoint(x: imageSize.width - insets.right, y: imageSize.height - insets.bottom), tangent2End: CGPoint(x: imageSize.width - insets.right, y: imageSize.height - insets.bottom - corners.bottomRight.radius), radius: corners.bottomRight.radius) + c.addLine(to: CGPoint(x: imageSize.width - insets.right, y: insets.top)) + c.closePath() + c.fillPath() + } + + let image = context.generateImage()?.resizableImage( + withCapInsets: UIEdgeInsets( + top: 0, + left: corners.bottomLeft.radius + insets.left, + bottom: imageSize.height - 1.0, + right: corners.bottomRight.radius + insets.right + ), + resizingMode: .stretch + ) + + if let image { + ChatMessageInteractiveMediaNode.timestampMaskImageCache[cacheKey] = image + } + + return image + } + public func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ mediaIndex: Int?, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let currentMessage = self.message let currentMedia = self.media @@ -1771,6 +1824,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate) strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) } + if strongSelf.currentImageArguments?.corners != arguments.corners, let timestampMaskView = strongSelf.timestampMaskView { + timestampMaskView.image = strongSelf.generateTimestampMaskImage(corners: arguments.corners) + } strongSelf.currentImageArguments = arguments imageApply() @@ -1950,8 +2006,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - //TODO:wip-release - /*var videoTimestamp: Int32? + var videoTimestamp: Int32? var storedVideoTimestamp: Int32? for attribute in message.attributes { if let attribute = attribute as? ForwardVideoTimestampAttribute { @@ -1985,7 +2040,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.timestampMaskView = timestampMaskView timestampContainerView.mask = timestampMaskView - timestampMaskView.image = strongSelf.imageNode.image + timestampMaskView.image = strongSelf.generateTimestampMaskImage(corners: arguments.corners) } let videoTimestampBackgroundLayer: SimpleLayer @@ -2038,7 +2093,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.videoTimestampForegroundLayer = nil videoTimestampForegroundLayer.removeFromSuperlayer() } - }*/ + } if let animatedStickerNode = strongSelf.animatedStickerNode { animatedStickerNode.frame = imageFrame @@ -3037,6 +3092,72 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr }) } + public func scrubberTransition() -> GalleryItemScrubberTransition? { + if let timestampContainerView = self.timestampContainerView, let timestampMaskView = self.timestampMaskView, let videoTimestampBackgroundLayer = self.videoTimestampBackgroundLayer, let videoTimestampForegroundLayer = self.videoTimestampForegroundLayer { + final class TimestampContainerTransitionView: UIView { + let containerView: UIView + let containerMaskView: UIImageView + let backgroundLayer: SimpleLayer + let foregroundLayer: SimpleLayer + let fraction: CGFloat + + init(timestampContainerView: UIView?, timestampMaskView: UIImageView?, videoTimestampBackgroundLayer: SimpleLayer?, videoTimestampForegroundLayer: SimpleLayer?) { + self.containerView = UIView() + self.containerMaskView = UIImageView() + self.backgroundLayer = SimpleLayer() + self.foregroundLayer = SimpleLayer() + + if let videoTimestampBackgroundLayer, let videoTimestampForegroundLayer { + self.fraction = videoTimestampForegroundLayer.bounds.width / videoTimestampBackgroundLayer.bounds.width + } else { + self.fraction = 0.0 + } + + super.init(frame: CGRect()) + + self.addSubview(self.containerView) + + self.containerView.mask = self.containerMaskView + self.containerMaskView.image = timestampMaskView?.image + + self.containerView.layer.addSublayer(self.backgroundLayer) + self.containerView.layer.addSublayer(self.foregroundLayer) + + self.backgroundLayer.backgroundColor = videoTimestampBackgroundLayer?.backgroundColor + self.foregroundLayer.backgroundColor = videoTimestampForegroundLayer?.backgroundColor + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(state: GalleryItemScrubberTransition.TransitionState, transition: ContainedViewLayoutTransition) { + let containerFrame = CGRect(origin: CGPoint(), size: state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)) + transition.updateFrame(view: self.containerView, frame: containerFrame) + transition.updateFrame(view: self.containerMaskView, frame: CGRect(origin: CGPoint(), size: containerFrame.size)) + + transition.updateFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.height - 3.0), size: CGSize(width: containerFrame.width, height: 3.0))) + transition.updateFrame(layer: self.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.height - 3.0), size: CGSize(width: containerFrame.width * self.fraction, height: 3.0))) + } + } + + return GalleryItemScrubberTransition( + view: timestampContainerView, + makeView: { [weak timestampContainerView, weak timestampMaskView, weak videoTimestampBackgroundLayer, weak videoTimestampForegroundLayer] in + return TimestampContainerTransitionView(timestampContainerView: timestampContainerView, timestampMaskView: timestampMaskView, videoTimestampBackgroundLayer: videoTimestampBackgroundLayer, videoTimestampForegroundLayer: videoTimestampForegroundLayer) + }, + updateView: { view, state, transition in + if let view = view as? TimestampContainerTransitionView { + view.update(state: state, transition: transition) + } + }, + insertCloneTransitionView: nil + ) + } else { + return nil + } + } + public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { var isAnimated = false if let file = self.media as? TelegramMediaFile { diff --git a/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift b/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift index d1a4f97e04..cdae86c7b0 100644 --- a/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift +++ b/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift @@ -176,7 +176,7 @@ public func presentPeerReportOptions( var message = "" var items: [ActionSheetItem] = [] items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText)) - items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in + items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, strings: presentationData.strings, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in message = text })) items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { @@ -309,7 +309,7 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe var message = "" var items: [ActionSheetItem] = [] items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText)) - items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in + items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, strings: presentationData.strings, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in message = text })) items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { diff --git a/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift b/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift index df156e2709..27aa262598 100644 --- a/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift +++ b/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift @@ -11,18 +11,20 @@ import AppBundle public final class ReportPeerDetailsActionSheetItem: ActionSheetItem { let context: AccountContext let theme: PresentationTheme + let strings: PresentationStrings let placeholderText: String let textUpdated: (String) -> Void - public init(context: AccountContext, theme: PresentationTheme, placeholderText: String, textUpdated: @escaping (String) -> Void) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, placeholderText: String, textUpdated: @escaping (String) -> Void) { self.context = context self.theme = theme + self.strings = strings self.placeholderText = placeholderText self.textUpdated = textUpdated } public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ReportPeerDetailsActionSheetItemNode(theme: theme, presentationTheme: self.theme, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated) + return ReportPeerDetailsActionSheetItemNode(theme: theme, presentationTheme: self.theme, strings: self.strings, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated) } public func updateNode(_ node: ActionSheetItemNode) { @@ -36,10 +38,10 @@ private final class ReportPeerDetailsActionSheetItemNode: ActionSheetItemNode { private let accessibilityArea: AccessibilityAreaNode - init(theme: ActionSheetControllerTheme, presentationTheme: PresentationTheme, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) { + init(theme: ActionSheetControllerTheme, presentationTheme: PresentationTheme, strings: PresentationStrings, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) { self.theme = theme - self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationTheme), placeholder: placeholderText) + self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationTheme), strings: strings, placeholder: placeholderText) self.accessibilityArea = AccessibilityAreaNode() @@ -58,7 +60,7 @@ private final class ReportPeerDetailsActionSheetItemNode: ActionSheetItemNode { } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let inputHeight = self.inputFieldNode.updateLayout(width: constrainedSize.width, transition: .immediate) + let inputHeight = self.inputFieldNode.updateLayout(width: constrainedSize.width, inputCopyText: nil, transition: .immediate) self.inputFieldNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: inputHeight)) let size = CGSize(width: constrainedSize.width, height: inputHeight)