Finalize timestamp sharing

This commit is contained in:
Isaac 2025-01-24 21:03:39 +04:00
parent d0b5f8b400
commit c21ebb06b5
21 changed files with 696 additions and 43 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [])

View File

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

View File

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

View File

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

View File

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

View File

@ -177,4 +177,8 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
}
}
}
func scrubberTransition() -> GalleryItemScrubberTransition? {
return nil
}
}

View File

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

View File

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

View File

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

View File

@ -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<PresentationData, NoError>)? = 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?()
}

View File

@ -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<Bool>()
@ -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
@ -491,6 +495,15 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
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 {
return !strongSelf.controllerInteraction!.selectedPeers.isEmpty
@ -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,7 +1068,7 @@ 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
}
@ -1029,8 +1076,37 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
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
}

View File

@ -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<Empty>()
let button = ComponentView<Empty>()
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<Empty>, 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<Empty>, 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<Empty>?
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<Empty>
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))
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))
return min(61.0, max(41.0, unboundTextFieldHeight))
}
}
private func updateTextNodeText(animated: Bool) {

View File

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

View File

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

View File

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

View File

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