diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 04264bd336..9bdc79bc6f 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -1615,7 +1615,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll preferredAction = .saveToCameraRoll actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved case .video: - preferredAction = .saveToCameraRoll + if let message = messages.first, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.addressName != nil { + } else { + preferredAction = .saveToCameraRoll + } actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved case .file: preferredAction = .saveToCameraRoll diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index 866455929a..8f520156b5 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -382,17 +382,17 @@ final class ChatVideoGalleryItemScrubberView: UIView { } func animateIn(from scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) { - if let scrubberTransition { + if let scrubberTransition = scrubberTransition?.scrubber { 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) + scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.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) + scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.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 @@ -421,18 +421,18 @@ final class ChatVideoGalleryItemScrubberView: UIView { func animateOut(to scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) { self.isAnimatedOut = true - if let scrubberTransition { + if let scrubberTransition = scrubberTransition?.scrubber { 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) + scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.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) + scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.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 diff --git a/submodules/GalleryUI/Sources/GalleryItemTransitionNode.swift b/submodules/GalleryUI/Sources/GalleryItemTransitionNode.swift index 7be9c6d783..c5b2853b25 100644 --- a/submodules/GalleryUI/Sources/GalleryItemTransitionNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemTransitionNode.swift @@ -4,32 +4,73 @@ 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 final class Scrubber { + 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 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 init(view: UIView, makeView: @escaping () -> UIView, updateView: @escaping (UIView, TransitionState, ContainedViewLayoutTransition) -> Void) { + self.view = view + self.makeView = makeView + self.updateView = updateView } } - public let view: UIView - public let makeView: () -> UIView - public let updateView: (UIView, TransitionState, ContainedViewLayoutTransition) -> Void - public let insertCloneTransitionView: ((UIView) -> Void)? + public final class Content { + public struct TransitionState: Equatable { + public var sourceSize: CGSize + public var destinationSize: CGSize + public var destinationCornerRadius: CGFloat + public var progress: CGFloat + + public init( + sourceSize: CGSize, + destinationSize: CGSize, + destinationCornerRadius: CGFloat, + progress: CGFloat + ) { + self.sourceSize = sourceSize + self.destinationSize = destinationSize + self.destinationCornerRadius = destinationCornerRadius + self.progress = progress + } + } + + public let sourceView: UIView + public let sourceRect: CGRect + public let makeView: () -> UIView + public let updateView: (UIView, TransitionState, ContainedViewLayoutTransition) -> Void + + public init(sourceView: UIView, sourceRect: CGRect, makeView: @escaping () -> UIView, updateView: @escaping (UIView, TransitionState, ContainedViewLayoutTransition) -> Void) { + self.sourceView = sourceView + self.sourceRect = sourceRect + self.makeView = makeView + self.updateView = updateView + } + } - 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 let scrubber: Scrubber? + public let content: Content? + + public init(scrubber: Scrubber?, content: Content?) { + self.scrubber = scrubber + self.content = content } } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index ac39dd5b8c..868c4d27c3 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1374,6 +1374,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var activeEdgeRateState: (initialRate: Double, currentRate: Double)? private var activeEdgeRateIndicator: ComponentView? + private var isAnimatingOut: Bool = false + init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData @@ -1568,7 +1570,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.hideControlsDisposable = (shouldHideControlsSignal |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self { + if let strongSelf = self, !strongSelf.isAnimatingOut { strongSelf.updateControlsVisibility(false) } }).strict() @@ -1882,17 +1884,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let status = status { let shouldStorePlaybacksState: Bool - #if DEBUG && false - shouldStorePlaybacksState = status.duration >= 10.0 - #else - shouldStorePlaybacksState = status.duration >= 60.0 * 10.0 - #endif + shouldStorePlaybacksState = status.duration >= 20.0 - var timestamp: Double? - if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 { - timestamp = status.timestamp + if shouldStorePlaybacksState { + var timestamp: Double? + if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 { + timestamp = status.timestamp + } + item.storeMediaPlaybackState(message.id, timestamp, status.baseRate) + } else { + item.storeMediaPlaybackState(message.id, nil, status.baseRate) } - item.storeMediaPlaybackState(message.id, timestamp, status.baseRate) } })) } @@ -2401,6 +2403,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var isAnimated = false var seek = MediaPlayerSeek.start if let item = self.item { + if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo { + for attribute in message.attributes { + if let attribute = attribute as? ForwardVideoTimestampAttribute { + seek = .timecode(Double(attribute.timestamp)) + } + } + } + if let content = item.content as? NativeVideoContent { isAnimated = content.fileReference.media.isAnimated if let time = item.timecode { @@ -2416,14 +2426,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { seek = .timecode(time) } } - - if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo { - for attribute in message.attributes { - if let attribute = attribute as? ForwardVideoTimestampAttribute { - seek = .timecode(Double(attribute.timestamp)) - } - } - } } videoNode.setBaseRate(self.playbackRate ?? 1.0) @@ -2458,7 +2460,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { + guard let videoNode = self.videoNode, let validLayout = self.validLayout else { return } @@ -2487,8 +2489,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.context.sharedContext.mediaManager.setOverlayVideoNode(nil) } else { + let scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition() + if let scrubberView = self.scrubberView { - let scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition() scrubberView.animateIn(from: scrubberTransition, transition: .animated(duration: 0.25, curve: .spring)) } @@ -2563,6 +2566,43 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + if let scrubberTransition, let contentTransition = scrubberTransition.content { + let transitionContentView = contentTransition.makeView() + let transitionSelfContentView = contentTransition.makeView() + + addToTransitionSurface(transitionContentView) + self.view.insertSubview(transitionSelfContentView, at: 0) + transitionSelfContentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false) + + if let transitionContentSuperview = transitionContentView.superview { + let transitionContentSourceFrame = contentTransition.sourceView.convert(contentTransition.sourceRect, to: transitionContentSuperview) + let transitionContentDestinationFrame = self.view.convert(self.view.bounds, to: transitionContentSuperview) + + let transitionContentSelfSourceFrame = contentTransition.sourceView.convert(contentTransition.sourceRect, to: self.view) + let transitionContentSelfDestinationFrame = self.view.convert(self.view.bounds, to: self.view) + + let screenCornerRadius: CGFloat = validLayout.layout.deviceMetrics.screenCornerRadius + + transitionContentView.frame = transitionContentSourceFrame + contentTransition.updateView(transitionContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSourceFrame.size, destinationSize: transitionContentDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 0.0), .immediate) + + transitionSelfContentView.frame = transitionContentSelfSourceFrame + contentTransition.updateView(transitionSelfContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSelfSourceFrame.size, destinationSize: transitionContentSelfDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 0.0), .immediate) + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + + transition.updateFrame(view: transitionContentView, frame: transitionContentDestinationFrame, completion: { [weak transitionContentView] _ in + transitionContentView?.removeFromSuperview() + }) + contentTransition.updateView(transitionContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSourceFrame.size, destinationSize: transitionContentDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 1.0), transition) + + transition.updateFrame(view: transitionSelfContentView, frame: transitionContentSelfDestinationFrame, completion: { [weak transitionSelfContentView] _ in + transitionSelfContentView?.removeFromSuperview() + }) + contentTransition.updateView(transitionSelfContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSelfSourceFrame.size, destinationSize: transitionContentSelfDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 1.0), transition) + } + } + if self.item?.fromPlayingVideo ?? false { Queue.mainQueue().after(0.001) { videoNode.canAttachContent = true @@ -2586,17 +2626,21 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + self.isAnimatingOut = true + guard let videoNode = self.videoNode else { completion() return } + let scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition() + if let scrubberView = self.scrubberView { - var scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition() + var scrubberEffectiveTransition = scrubberTransition if !self.controlsVisibility() { - scrubberTransition = nil + scrubberEffectiveTransition = nil } - scrubberView.animateOut(to: scrubberTransition, transition: .animated(duration: 0.25, curve: .spring)) + scrubberView.animateOut(to: scrubberEffectiveTransition, transition: .animated(duration: 0.25, curve: .spring)) } let transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) @@ -2753,6 +2797,47 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { intermediateCompletion() }) + var scrubberContentTransition = scrubberTransition + if !self.controlsVisibility() { + scrubberContentTransition = nil + } + if let scrubberContentTransition, let contentTransition = scrubberContentTransition.content { + let transitionContentView = contentTransition.makeView() + let transitionSelfContentView = contentTransition.makeView() + + addToTransitionSurface(transitionContentView) + //self.view.insertSubview(transitionSelfContentView, at: 0) + transitionSelfContentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) + + if let validLayout = self.validLayout, let transitionContentSuperview = transitionContentView.superview { + let transitionContentSourceFrame = contentTransition.sourceView.convert(contentTransition.sourceRect, to: transitionContentSuperview) + let transitionContentDestinationFrame = self.view.convert(self.view.bounds, to: transitionContentSuperview) + + let transitionContentSelfSourceFrame = contentTransition.sourceView.convert(contentTransition.sourceRect, to: self.view) + let transitionContentSelfDestinationFrame = self.view.convert(self.view.bounds, to: self.view) + + let screenCornerRadius: CGFloat = validLayout.layout.deviceMetrics.screenCornerRadius + + transitionContentView.frame = transitionContentDestinationFrame + contentTransition.updateView(transitionContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSourceFrame.size, destinationSize: transitionContentDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 1.0), .immediate) + + transitionSelfContentView.frame = transitionContentSelfDestinationFrame + contentTransition.updateView(transitionSelfContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSelfSourceFrame.size, destinationSize: transitionContentSelfDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 1.0), .immediate) + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + + transition.updateFrame(view: transitionContentView, frame: transitionContentSourceFrame, completion: { [weak transitionContentView] _ in + transitionContentView?.removeFromSuperview() + }) + contentTransition.updateView(transitionContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSourceFrame.size, destinationSize: transitionContentDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 0.0), transition) + + transition.updateFrame(view: transitionSelfContentView, frame: transitionContentSelfSourceFrame, completion: { [weak transitionSelfContentView] _ in + transitionSelfContentView?.removeFromSuperview() + }) + contentTransition.updateView(transitionSelfContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSelfSourceFrame.size, destinationSize: transitionContentSelfDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 0.0), transition) + } + } + if let pictureInPictureNode = self.pictureInPictureNode { let transformedPlaceholderFrame = node.0.view.convert(node.0.view.bounds, to: pictureInPictureNode.view) let pictureInPictureTransform = CATransform3DScale(pictureInPictureNode.layer.transform, transformedPlaceholderFrame.size.width / pictureInPictureNode.layer.bounds.size.width, transformedPlaceholderFrame.size.height / pictureInPictureNode.layer.bounds.size.height, 1.0) diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 89857d4a6e..cb255ae56a 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -950,6 +950,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate } if actions { actionNodes.append(contentsOf: [self.actionsBackgroundNode, self.actionButtonNode, self.actionSeparatorNode]) + if let startAtTimestampNode = self.startAtTimestampNode { + actionNodes.append(startAtTimestampNode) + } } updateActionNodesAlpha(actionNodes, alpha: hidden ? 0.0 : 1.0) } @@ -1326,6 +1329,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) + if let startAtTimestampNode = self.startAtTimestampNode { + transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0) + } let peerIds: [PeerId] var topicIds: [PeerId: Int64] = [:] @@ -1623,6 +1629,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate transition.updateAlpha(node: strongSelf.inputFieldNode, alpha: 0.0) transition.updateAlpha(node: strongSelf.actionSeparatorNode, alpha: 0.0) transition.updateAlpha(node: strongSelf.actionsBackgroundNode, alpha: 0.0) + if let startAtTimestampNode = strongSelf.startAtTimestampNode { + transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0) + } strongSelf.transitionToContentNode(ShareLoadingContainerNode(theme: strongSelf.presentationData.theme, forceNativeAppearance: true), fastOut: true) loadingTimestamp = CACurrentMediaTime() if reportReady { @@ -1777,6 +1786,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) + if let startAtTimestampNode = self.startAtTimestampNode { + transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0) + } self.transitionToContentNode(ShareProlongedLoadingContainerNode(theme: self.presentationData.theme, strings: self.presentationData.strings, forceNativeAppearance: true, postbox: self.context?.stateManager.postbox, environment: self.environment), fastOut: true) let timestamp = CACurrentMediaTime() @@ -1815,6 +1827,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) + if let startAtTimestampNode = self.startAtTimestampNode { + transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0) + } self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: true), fastOut: true) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 419e2dafaa..81cf5f0258 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1863,6 +1863,21 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr statusFrame.origin.y = floor(imageFrame.height / 2.0 - statusFrame.height / 2.0) statusNode.frame = statusFrame } + + var videoTimestamp: Int32? + var storedVideoTimestamp: Int32? + for attribute in message.attributes { + if let attribute = attribute as? ForwardVideoTimestampAttribute { + videoTimestamp = attribute.timestamp + } else if let attribute = attribute as? DerivedDataMessageAttribute { + if let value = attribute.data["mps"]?.get(MediaPlaybackStoredState.self) { + storedVideoTimestamp = Int32(value.timestamp) + } + } + } + if let storedVideoTimestamp { + videoTimestamp = storedVideoTimestamp + } var updatedVideoNodeReadySignal: Signal? var updatedPlayerStatusSignal: Signal? @@ -1920,8 +1935,15 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false + var firstTime = true videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { + if firstTime { + firstTime = false + if let videoTimestamp { + videoNode.seek(Double(videoTimestamp)) + } + } strongSelf.videoNode?.isHidden = !owns if owns { strongSelf.videoNode?.setBaseRate(1.0) @@ -2006,21 +2028,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - var videoTimestamp: Int32? - var storedVideoTimestamp: Int32? - for attribute in message.attributes { - if let attribute = attribute as? ForwardVideoTimestampAttribute { - videoTimestamp = attribute.timestamp - } else if let attribute = attribute as? DerivedDataMessageAttribute { - if let value = attribute.data["mps"]?.get(MediaPlaybackStoredState.self) { - storedVideoTimestamp = Int32(value.timestamp) - } - } - } - if let storedVideoTimestamp { - videoTimestamp = storedVideoTimestamp - } - if let videoTimestamp, let file = media as? TelegramMediaFile, let duration = file.duration, duration > 1.0 { let timestampContainerView: UIView if let current = strongSelf.timestampContainerView { @@ -2064,7 +2071,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr videoTimestampBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.5).cgColor videoTimestampForegroundLayer.backgroundColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.accentControlColor.cgColor : presentationData.theme.theme.chat.message.outgoing.accentControlColor.cgColor - timestampContainerView.frame = imageFrame + timestampContainerView.frame = imageFrame.offsetBy(dx: arguments.corners.extendedEdges.left, dy: 0.0) timestampMaskView.frame = imageFrame let videoTimestampBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: imageFrame.height - 3.0), size: CGSize(width: imageFrame.width, height: 3.0)) @@ -3093,55 +3100,175 @@ 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 + 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() - 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 + if let videoTimestampBackgroundLayer, let videoTimestampForegroundLayer { + self.fraction = videoTimestampForegroundLayer.bounds.width / videoTimestampBackgroundLayer.bounds.width + } else { + self.fraction = 0.0 } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + super.init(frame: CGRect()) - 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))) - } + 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 } - return GalleryItemScrubberTransition( + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(state: GalleryItemScrubberTransition.Scrubber.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))) + } + } + + final class MediaContentTransitionView: UIView { + let backgroundLayer: SimpleLayer + let backgroundMaskLayer: SimpleShapeLayer + let sourceCorners: ImageCorners + + init(imageNode: TransformImageNode) { + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.backgroundColor = UIColor.black.cgColor + + self.backgroundMaskLayer = SimpleShapeLayer() + self.backgroundMaskLayer.fillColor = UIColor.white.cgColor + self.backgroundLayer.mask = self.backgroundMaskLayer + + self.sourceCorners = imageNode.currentArguments?.corners ?? ImageCorners() + + super.init(frame: CGRect()) + + self.layer.addSublayer(self.backgroundLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(state: GalleryItemScrubberTransition.Content.TransitionState, transition: ContainedViewLayoutTransition) { + let sourceCorners: (topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) = (max(0.1, self.sourceCorners.topLeft.radius), max(0.1, self.sourceCorners.topRight.radius), max(0.1, self.sourceCorners.bottomLeft.radius), max(0.1, self.sourceCorners.bottomRight.radius)) + let destinationCorners: (topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) = (max(0.1, state.destinationCornerRadius), max(0.1, state.destinationCornerRadius), max(0.1, state.destinationCornerRadius), max(0.1, state.destinationCornerRadius)) + + let currentCornersData = CGRect(x: sourceCorners.topLeft, y: sourceCorners.topRight, width: sourceCorners.bottomLeft, height: sourceCorners.bottomRight).interpolate(to: CGRect(x: destinationCorners.topLeft, y: destinationCorners.topRight, width: destinationCorners.bottomLeft, height: destinationCorners.bottomRight), amount: state.progress) + let currentCorners: (topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) = (currentCornersData.minX, currentCornersData.minY, currentCornersData.width, currentCornersData.height) + + func makeRoundedRectPath( + in rect: CGRect, + topLeft: CGFloat, + topRight: CGFloat, + bottomRight: CGFloat, + bottomLeft: CGFloat + ) -> CGPath { + let path = CGMutablePath() + + // Move to top-left, offset by its corner radius + path.move(to: CGPoint(x: rect.minX + topLeft, y: rect.minY)) + + // Top edge (straight line) + path.addLine(to: CGPoint(x: rect.maxX - topRight, y: rect.minY)) + // Top-right corner arc + if topRight > 0 { + path.addArc( + center: CGPoint(x: rect.maxX - topRight, y: rect.minY + topRight), + radius: topRight, + startAngle: -CGFloat.pi / 2, + endAngle: 0, + clockwise: false + ) + } + + // Right edge (straight line) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - bottomRight)) + // Bottom-right corner arc + if bottomRight > 0 { + path.addArc( + center: CGPoint(x: rect.maxX - bottomRight, y: rect.maxY - bottomRight), + radius: bottomRight, + startAngle: 0, + endAngle: CGFloat.pi / 2, + clockwise: false + ) + } + + // Bottom edge (straight line) + path.addLine(to: CGPoint(x: rect.minX + bottomLeft, y: rect.maxY)) + // Bottom-left corner arc + if bottomLeft > 0 { + path.addArc( + center: CGPoint(x: rect.minX + bottomLeft, y: rect.maxY - bottomLeft), + radius: bottomLeft, + startAngle: CGFloat.pi / 2, + endAngle: CGFloat.pi, + clockwise: false + ) + } + + // Left edge (straight line) + path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + topLeft)) + // Top-left corner arc + if topLeft > 0 { + path.addArc( + center: CGPoint(x: rect.minX + topLeft, y: rect.minY + topLeft), + radius: topLeft, + startAngle: CGFloat.pi, + endAngle: 3 * CGFloat.pi / 2, + clockwise: false + ) + } + + path.closeSubpath() + return path + } + + let backgroundFrame = CGRect(origin: CGPoint(), size: state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)) + + transition.updatePath(layer: self.backgroundMaskLayer, path: makeRoundedRectPath(in: CGRect(origin: CGPoint(), size: backgroundFrame.size), topLeft: currentCorners.topLeft, topRight: currentCorners.topRight, bottomRight: currentCorners.bottomLeft, bottomLeft: currentCorners.bottomRight)) + + transition.updateFrame(layer: self.backgroundLayer, frame: backgroundFrame) + + transition.updateFrame(layer: self.backgroundMaskLayer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + } + } + + guard let currentImageArguments = self.currentImageArguments else { + return nil + } + + var sourceContentRect = self.imageNode.bounds + sourceContentRect.origin.x += currentImageArguments.insets.left + sourceContentRect.origin.y += currentImageArguments.insets.top + sourceContentRect.size.width -= currentImageArguments.insets.left + currentImageArguments.insets.right + sourceContentRect.size.height -= currentImageArguments.insets.top + currentImageArguments.insets.bottom + + var scrubber: GalleryItemScrubberTransition.Scrubber? + if let timestampContainerView = self.timestampContainerView, let timestampMaskView = self.timestampMaskView, let videoTimestampBackgroundLayer = self.videoTimestampBackgroundLayer, let videoTimestampForegroundLayer = self.videoTimestampForegroundLayer { + scrubber = GalleryItemScrubberTransition.Scrubber( view: timestampContainerView, makeView: { [weak timestampContainerView, weak timestampMaskView, weak videoTimestampBackgroundLayer, weak videoTimestampForegroundLayer] in return TimestampContainerTransitionView(timestampContainerView: timestampContainerView, timestampMaskView: timestampMaskView, videoTimestampBackgroundLayer: videoTimestampBackgroundLayer, videoTimestampForegroundLayer: videoTimestampForegroundLayer) @@ -3150,12 +3277,33 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if let view = view as? TimestampContainerTransitionView { view.update(state: state, transition: transition) } - }, - insertCloneTransitionView: nil + } ) - } else { - return nil } + + var content: GalleryItemScrubberTransition.Content? + content = GalleryItemScrubberTransition.Content( + sourceView: self.imageNode.view, + sourceRect: sourceContentRect, + makeView: { [weak self] in + guard let self else { + return UIView() + } + + return MediaContentTransitionView(imageNode: self.imageNode) + }, + updateView: { view, state, transition in + guard let view = view as? MediaContentTransitionView else { + return + } + view.update(state: state, transition: transition) + } + ) + + return GalleryItemScrubberTransition( + scrubber: scrubber, + content: content + ) } public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index a7d9564ab0..deb455dbab 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -1439,7 +1439,8 @@ private final class ChatSendStarsScreenComponent: Component { }) } - self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false) + //TODO:wip-release + /*self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false) |> deliverOnMainQueue).startStrict(next: { [weak self] peers in guard let self else { return @@ -1448,7 +1449,7 @@ private final class ChatSendStarsScreenComponent: Component { self.channelsForPublicReaction = peers self.state?.updated(transition: .immediate) } - }) + })*/ } self.component = component diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift index 0bc97815ad..00f36f7753 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift @@ -51,7 +51,33 @@ extension ChatControllerImpl { return } - UIPasteboard.general.string = timecode + if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, let addressName = channel.addressName { + var timestampSuffix = "" + let startAtTimestamp = parseTimeString(timecode) + + 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)" + let inputCopyText = "https://t.me/\(addressName)/\(message.id.id)\(timestampSuffix)" + UIPasteboard.general.string = inputCopyText + } else { + UIPasteboard.general.string = timecode + } self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) })) @@ -67,3 +93,30 @@ extension ChatControllerImpl { self.window?.presentInGlobalOverlay(controller) } } + +private func parseTimeString(_ timeString: String) -> Int { + let parts = timeString.split(separator: ":").map(String.init) + + switch parts.count { + case 1: + // Single component (e.g. "1", "10") => seconds + return Int(parts[0]) ?? 0 + + case 2: + // Two components (e.g. "1:01", "10:30") => minutes:seconds + let minutes = Int(parts[0]) ?? 0 + let seconds = Int(parts[1]) ?? 0 + return minutes * 60 + seconds + + case 3: + // Three components (e.g. "1:01:01", "10:00:00") => hours:minutes:seconds + let hours = Int(parts[0]) ?? 0 + let minutes = Int(parts[1]) ?? 0 + let seconds = Int(parts[2]) ?? 0 + return hours * 3600 + minutes * 60 + seconds + + default: + // Fallback to 0 or handle invalid format + return 0 + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a1ff060f8f..a5fc09262f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -10964,10 +10964,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G public func updatePushedTransition(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { if !transition.isAnimated { - self.chatDisplayNode.historyNodeContainer.layer.removeAllAnimations() + self.chatDisplayNode.historyNode.layer.removeAnimation(forKey: "sublayerTransform") } let scale: CGFloat = 1.0 - 0.06 * fraction - transition.updateTransformScale(node: self.chatDisplayNode.historyNodeContainer, scale: scale) + transition.updateSublayerTransformScale(node: self.chatDisplayNode.historyNode, scale: scale) } func restrictedSendingContentsText() -> String {