Video improvements

This commit is contained in:
Isaac 2025-01-28 20:51:04 +04:00
parent 846e495d12
commit 3728be84cb
9 changed files with 465 additions and 119 deletions

View File

@ -1615,7 +1615,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
preferredAction = .saveToCameraRoll
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved
case .video:
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

View File

@ -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

View File

@ -4,6 +4,7 @@ import AccountContext
import Display
public final class GalleryItemScrubberTransition {
public final class Scrubber {
public struct TransitionState: Equatable {
public var sourceSize: CGSize
public var destinationSize: CGSize
@ -23,13 +24,53 @@ public final class GalleryItemScrubberTransition {
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)?) {
public init(view: UIView, makeView: @escaping () -> UIView, updateView: @escaping (UIView, TransitionState, ContainedViewLayoutTransition) -> Void) {
self.view = view
self.makeView = makeView
self.updateView = updateView
self.insertCloneTransitionView = insertCloneTransitionView
}
}
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 let scrubber: Scrubber?
public let content: Content?
public init(scrubber: Scrubber?, content: Content?) {
self.scrubber = scrubber
self.content = content
}
}

View File

@ -1374,6 +1374,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var activeEdgeRateState: (initialRate: Double, currentRate: Double)?
private var activeEdgeRateIndicator: ComponentView<Empty>?
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
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)
}
}
}))
}
@ -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 {
if let scrubberView = self.scrubberView {
let scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition()
if let scrubberView = self.scrubberView {
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)

View File

@ -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)

View File

@ -1864,6 +1864,21 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
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<Void, NoError>?
var updatedPlayerStatusSignal: Signal<MediaPlayerStatus?, NoError>?
if let currentReplaceVideoNode = replaceVideoNode {
@ -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,7 +3100,6 @@ 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
@ -3131,7 +3137,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
fatalError("init(coder:) has not been implemented")
}
func update(state: GalleryItemScrubberTransition.TransitionState, transition: ContainedViewLayoutTransition) {
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))
@ -3141,7 +3147,128 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
return GalleryItemScrubberTransition(
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?)? {

View File

@ -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

View File

@ -51,7 +51,33 @@ extension ChatControllerImpl {
return
}
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
}
}

View File

@ -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 {