From 9a1cdb0813c77e71f9ac514281e509fbed0b3da9 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 19:02:23 +0400 Subject: [PATCH 1/9] Pinch --- .../Sources/BotCheckoutTipItem.swift | 95 +++-- .../ContextUI/Sources/PinchController.swift | 395 ++++++++++++++++++ ...teractiveTransitionGestureRecognizer.swift | 4 + .../Source/TransformImageArguments.swift | 16 +- .../Display/Source/WindowPanRecognizer.swift | 4 + .../Sources/InstantPageAnchorItem.swift | 3 +- .../Sources/InstantPageArticleItem.swift | 3 +- .../Sources/InstantPageAudioItem.swift | 3 +- .../Sources/InstantPageContentNode.swift | 4 +- .../Sources/InstantPageController.swift | 2 +- .../Sources/InstantPageControllerNode.swift | 11 +- .../Sources/InstantPageDetailsItem.swift | 3 +- .../Sources/InstantPageFeedbackItem.swift | 3 +- .../Sources/InstantPageImageItem.swift | 5 +- .../Sources/InstantPageImageNode.swift | 36 +- .../Sources/InstantPageItem.swift | 3 +- .../InstantPagePeerReferenceItem.swift | 3 +- .../InstantPagePlayableVideoItem.swift | 3 +- .../Sources/InstantPageShapeItem.swift | 3 +- .../Sources/InstantPageSlideshowItem.swift | 3 +- .../InstantPageSlideshowItemNode.swift | 2 +- .../Sources/InstantPageTableItem.swift | 5 +- .../Sources/InstantPageTextItem.swift | 7 +- .../Sources/InstantPageWebEmbedItem.swift | 3 +- .../Sources/PhotoResources.swift | 4 +- .../Sources/CurrencyFormat.swift | 41 +- .../TelegramUI/Sources/ChatController.swift | 6 + .../Sources/ChatControllerInteraction.swift | 5 +- .../ChatMessageAnimatedStickerItemNode.swift | 25 +- .../ChatMessageAttachedContentNode.swift | 163 +++++--- .../ChatMessageDateAndStatusNode.swift | 2 +- ...entLogPreviousDescriptionContentNode.swift | 4 +- ...ssageEventLogPreviousLinkContentNode.swift | 4 +- ...geEventLogPreviousMessageContentNode.swift | 4 +- .../ChatMessageGameBubbleContentNode.swift | 4 +- .../ChatMessageInteractiveMediaNode.swift | 175 ++++++-- .../ChatMessageInvoiceBubbleContentNode.swift | 4 +- .../ChatMessageMediaBubbleContentNode.swift | 192 ++++----- .../Sources/ChatMessageStickerItemNode.swift | 25 +- .../ChatMessageWebpageBubbleContentNode.swift | 4 +- .../ChatRecentActionsControllerNode.swift | 1 + .../Sources/DrawingStickersScreen.swift | 3 +- .../OverlayAudioPlayerControllerNode.swift | 1 + .../Sources/PeerInfo/PeerInfoScreen.swift | 1 + .../Sources/SharedAccountContext.swift | 3 +- submodules/TgVoipWebrtc/tgcalls | 2 +- 46 files changed, 1005 insertions(+), 287 deletions(-) create mode 100644 submodules/ContextUI/Sources/PinchController.swift diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift index 1022bfce83..bd199b9122 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift @@ -120,7 +120,7 @@ private final class TipValueNode: ASDisplayNode { self.action?() } - func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> CGFloat { + func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> (CGFloat, (CGFloat) -> Void) { var updateBackground = false let backgroundColor = isHighlighted ? UIColor(rgb: 0x00A650) : UIColor(rgb: 0xE5F6ED) if let currentBackgroundColor = self.currentBackgroundColor { @@ -142,20 +142,22 @@ private final class TipValueNode: ASDisplayNode { let calculatedWidth = max(titleSize.width + 16.0 * 2.0, minWidth) - self.titleNode.frame = CGRect(origin: CGPoint(x: floor((calculatedWidth - titleSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize) + return (calculatedWidth, { calculatedWidth in + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((calculatedWidth - titleSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize) - let size = CGSize(width: calculatedWidth, height: height) - self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + let size = CGSize(width: calculatedWidth, height: height) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) - self.button.frame = CGRect(origin: CGPoint(), size: size) - - return size.width + self.button.frame = CGRect(origin: CGPoint(), size: size) + }) } } class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { let titleNode: TextNode let labelNode: TextNode + let tipMeasurementNode: ImmediateTextNode + let tipCurrencyNode: ImmediateTextNode private let textNode: TextFieldNode private var formatterDelegate: CurrencyUITextFieldDelegate? @@ -172,6 +174,9 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false + self.tipMeasurementNode = ImmediateTextNode() + self.tipCurrencyNode = ImmediateTextNode() + self.textNode = TextFieldNode() self.scrollNode = ASScrollNode() @@ -190,6 +195,7 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { self.addSubnode(self.titleNode) self.addSubnode(self.labelNode) self.addSubnode(self.textNode) + self.addSubnode(self.tipCurrencyNode) self.addSubnode(self.scrollNode) self.textNode.clipsToBounds = true @@ -221,7 +227,7 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - //TODO:locali + //TODO:localize let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Enter Custom", font: textFont, textColor: textColor.withMultipliedAlpha(0.8)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in @@ -236,17 +242,6 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((labelsContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size) strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - labelLayout.size.width, y: floor((labelsContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size) - let text: String - if item.numericValue == 0 { - text = "" - } else { - text = formatCurrencyAmount(item.numericValue, currency: item.currency) - } - if strongSelf.textNode.textField.text ?? "" != text { - strongSelf.textNode.textField.text = text - strongSelf.labelNode.isHidden = !text.isEmpty - } - if strongSelf.formatterDelegate == nil { strongSelf.formatterDelegate = CurrencyUITextFieldDelegate(formatter: CurrencyFormatter(currency: item.currency, { formatter in formatter.maxValue = currencyToFractionalAmount(value: item.maxValue, currency: item.currency) ?? 10000.0 @@ -275,18 +270,31 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { strongSelf.textNode.textField.keyboardType = .decimalPad strongSelf.textNode.textField.tintColor = item.theme.list.itemAccentColor - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - 150.0, y: -2.0), size: CGSize(width: 150.0, height: labelsContentHeight)) + var textInputFrame = CGRect(origin: CGPoint(x: params.width - leftInset - 150.0, y: -2.0), size: CGSize(width: 150.0, height: labelsContentHeight)) + + var currencyText: (String, String) = formatCurrencyAmountCustom(item.numericValue, currency: item.currency) + if strongSelf.textNode.textField.text ?? "" != currencyText.0 { + strongSelf.textNode.textField.text = currencyText.0 + strongSelf.labelNode.isHidden = !currencyText.0.isEmpty + } + + strongSelf.tipMeasurementNode.attributedText = NSAttributedString(string: currencyText.0, font: titleFont, textColor: textColor) + let inputTextSize = strongSelf.tipMeasurementNode.updateLayout(textInputFrame.size) + + strongSelf.tipCurrencyNode.attributedText = NSAttributedString(string: " \(currencyText.1)", font: titleFont, textColor: textColor) + let currencySize = strongSelf.tipCurrencyNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude)) + strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize) + textInputFrame.origin.x -= currencySize.width + + strongSelf.textNode.frame = textInputFrame let valueHeight: CGFloat = 52.0 let valueY: CGFloat = labelsContentHeight + 9.0 var index = 0 - var variantsOffset: CGFloat = 16.0 + var variantLayouts: [(CGFloat, (CGFloat) -> Void)] = [] + var totalMinWidth: CGFloat = 0.0 for (variantText, variantValue) in item.availableVariants { - if index != 0 { - variantsOffset += 12.0 - } - let valueNode: TipValueNode if strongSelf.valueNodes.count > index { valueNode = strongSelf.valueNodes[index] @@ -295,14 +303,39 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { strongSelf.valueNodes.append(valueNode) strongSelf.scrollNode.addSubnode(valueNode) } - let nodeWidth = valueNode.update(theme: item.theme, text: variantText, isHighlighted: item.value == variantText, height: valueHeight) + let (nodeMinWidth, nodeApply) = valueNode.update(theme: item.theme, text: variantText, isHighlighted: item.value == variantText, height: valueHeight) valueNode.action = { guard let strongSelf = self else { return } strongSelf.item?.updateValue(variantValue) } + totalMinWidth += nodeMinWidth + variantLayouts.append((nodeMinWidth, nodeApply)) + index += 1 + } + + let sideInset: CGFloat = 16.0 + var scaleFactor: CGFloat = 1.0 + let availableWidth = params.width - sideInset * 2.0 - CGFloat(max(0, item.availableVariants.count - 1)) * 12.0 + if totalMinWidth < availableWidth { + scaleFactor = availableWidth / totalMinWidth + } + + index = 0 + var variantsOffset: CGFloat = 16.0 + for _ in item.availableVariants { + if index != 0 { + variantsOffset += 12.0 + } + + let valueNode: TipValueNode = strongSelf.valueNodes[index] + let (minWidth, nodeApply) = variantLayouts[index] + + let nodeWidth = floor(scaleFactor * minWidth) + valueNode.frame = CGRect(origin: CGPoint(x: variantsOffset, y: 0.0), size: CGSize(width: nodeWidth, height: valueHeight)) + nodeApply(nodeWidth) variantsOffset += nodeWidth index += 1 } @@ -342,7 +375,15 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { return } - if let value = fractionalToCurrencyAmount(value: doubleValue, currency: item.currency) { + if var value = fractionalToCurrencyAmount(value: doubleValue, currency: item.currency) { + if value > item.maxValue { + value = item.maxValue + + let currencyText: (String, String) = formatCurrencyAmountCustom(value, currency: item.currency) + if self.textNode.textField.text ?? "" != currencyText.0 { + self.textNode.textField.text = currencyText.0 + } + } item.updateValue(value) } } diff --git a/submodules/ContextUI/Sources/PinchController.swift b/submodules/ContextUI/Sources/PinchController.swift new file mode 100644 index 0000000000..38be8faf46 --- /dev/null +++ b/submodules/ContextUI/Sources/PinchController.swift @@ -0,0 +1,395 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import TextSelectionNode +import ReactionSelectionNode +import TelegramCore +import SyncCore +import SwiftSignalKit + +private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect { + let sourceWindowFrame = fromView.convert(frame, to: nil) + var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil) + + if let fromWindow = fromView.window, let toWindow = toView.window { + targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width + } + return targetWindowFrame +} + +final class PinchSourceGesture: UIPinchGestureRecognizer { + private final class Target { + var updated: (() -> Void)? + + @objc func onGesture(_ gesture: UIPinchGestureRecognizer) { + self.updated?() + } + } + + private let target: Target + + private(set) var currentTransform: (CGFloat, CGPoint)? + + var began: (() -> Void)? + var updated: ((CGFloat, CGPoint) -> Void)? + var ended: (() -> Void)? + + private var lastLocation: CGPoint? + private var currentOffset = CGPoint() + + init() { + self.target = Target() + + super.init(target: self.target, action: #selector(self.target.onGesture(_:))) + + self.target.updated = { [weak self] in + self?.gestureUpdated() + } + } + + override func reset() { + super.reset() + + self.lastLocation = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if touches.count >= 2 { + var locationSum = CGPoint() + for touch in touches { + let point = touch.location(in: self.view) + locationSum.x += point.x + locationSum.y += point.y + } + locationSum.x /= CGFloat(touches.count) + locationSum.y /= CGFloat(touches.count) + if let lastLocation = self.lastLocation { + self.currentOffset = CGPoint(x: locationSum.x - lastLocation.x, y: locationSum.y - lastLocation.y) + } else { + self.lastLocation = locationSum + self.currentOffset = CGPoint() + } + if let (scale, _) = self.currentTransform { + self.currentTransform = (scale, self.currentOffset) + self.updated?(scale, self.currentOffset) + } + } + } + + private func gestureUpdated() { + switch self.state { + case .began: + self.lastLocation = nil + self.currentOffset = CGPoint() + self.currentTransform = nil + self.began?() + case .changed: + let scale = max(1.0, self.scale) + self.currentTransform = (scale, self.currentOffset) + self.updated?(scale, self.currentOffset) + case .ended, .cancelled: + self.ended?() + default: + break + } + } +} + +private func cancelContextGestures(node: ASDisplayNode) { + if let node = node as? ContextControllerSourceNode { + node.cancelGesture() + } + + if let supernode = node.supernode { + cancelContextGestures(node: supernode) + } +} + +private func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for recognizer in gestureRecognizers { + if let recognizer = recognizer as? InteractiveTransitionGestureRecognizer { + recognizer.cancel() + } else if let recognizer = recognizer as? WindowPanRecognizer { + recognizer.cancel() + } + } + } + + if let superview = view.superview { + cancelContextGestures(view: superview) + } +} + +public final class PinchSourceContainerNode: ASDisplayNode { + public let contentNode: ASDisplayNode + public var contentRect: CGRect = CGRect() + private(set) var naturalContentFrame: CGRect? + + fileprivate let gesture: PinchSourceGesture + + public var isPinchGestureEnabled: Bool = false { + didSet { + if self.isPinchGestureEnabled != oldValue { + self.gesture.isEnabled = self.isPinchGestureEnabled + } + } + } + + private var isActive: Bool = false + + public var activate: ((PinchSourceContainerNode) -> Void)? + public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + var deactivate: (() -> Void)? + var updated: ((CGFloat, CGPoint) -> Void)? + + override public init() { + self.gesture = PinchSourceGesture() + self.contentNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.contentNode) + + self.gesture.began = { [weak self] in + guard let strongSelf = self else { + return + } + cancelContextGestures(node: strongSelf) + cancelContextGestures(view: strongSelf.view) + strongSelf.isActive = true + + strongSelf.activate?(strongSelf) + } + + self.gesture.ended = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.isActive = false + strongSelf.deactivate?() + } + + self.gesture.updated = { [weak self] scale, offset in + guard let strongSelf = self else { + return + } + strongSelf.updated?(scale, offset) + strongSelf.scaleUpdated?(scale, .immediate) + } + } + + override public func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(self.gesture) + self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let strongSelf = self else { + return false + } + return strongSelf.isActive + } + } + + public func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let contentFrame = CGRect(origin: CGPoint(), size: size) + self.naturalContentFrame = contentFrame + if !self.isActive { + transition.updateFrame(node: self.contentNode, frame: contentFrame) + } + } + + func restoreToNaturalSize() { + guard let naturalContentFrame = self.naturalContentFrame else { + return + } + self.contentNode.frame = naturalContentFrame + } +} + +private final class PinchControllerNode: ViewControllerTracingNode { + private weak var controller: PinchController? + private let sourceNode: PinchSourceContainerNode + + private let dimNode: ASDisplayNode + + private var validLayout: ContainerViewLayout? + private var isAnimatingOut: Bool = false + + private var hapticFeedback: HapticFeedback? + + init(controller: PinchController, sourceNode: PinchSourceContainerNode) { + self.controller = controller + self.sourceNode = sourceNode + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.dimNode.alpha = 0.0 + + super.init() + + self.addSubnode(self.dimNode) + + self.sourceNode.deactivate = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controller?.dismiss() + } + + self.sourceNode.updated = { [weak self] scale, offset in + guard let strongSelf = self else { + return + } + strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0)) + strongSelf.sourceNode.contentNode.transform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1.0), offset.x / scale, offset.y / scale, 0.0) + } + } + + deinit { + } + + override func didLoad() { + super.didLoad() + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) { + if self.isAnimatingOut { + return + } + + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + + func animateIn() { + let convertedFrame = convertFrame(self.sourceNode.contentNode.frame, from: self.sourceNode.view, to: self.view) + self.sourceNode.contentNode.frame = convertedFrame + self.addSubnode(self.sourceNode.contentNode) + } + + func animateOut(completion: @escaping () -> Void) { + let performCompletion: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.sourceNode.restoreToNaturalSize() + strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode) + + completion() + } + + if let (scale, offset) = self.sourceNode.gesture.currentTransform { + let duration = 0.4 + let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring) + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.prepareImpact(.light) + Queue.mainQueue().after(0.2, { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.hapticFeedback?.impact(.light) + }) + + self.sourceNode.scaleUpdated?(1.0, transition) + + self.sourceNode.contentNode.transform = CATransform3DIdentity + self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0) + self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x / scale, y: offset.y / scale), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in + performCompletion() + }) + + let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) + dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0) + } else { + performCompletion() + } + } +} + +public final class PinchController: ViewController, StandalonePresentableController { + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private let sourceNode: PinchSourceContainerNode + + private var wasDismissed = false + + private var controllerNode: PinchControllerNode { + return self.displayNode as! PinchControllerNode + } + + public init(sourceNode: PinchSourceContainerNode) { + self.sourceNode = sourceNode + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + + self.lockOrientation = true + self.blocksBackgroundWhenInOverlay = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func loadDisplayNode() { + self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode) + + self.displayNodeDidLoad() + + self._ready.set(.single(true)) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) + } + + override public func viewDidAppear(_ animated: Bool) { + if self.ignoreAppearanceMethodInvocations() { + return + } + super.viewDidAppear(animated) + + self.controllerNode.animateIn() + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.wasDismissed { + self.wasDismissed = true + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + } +} diff --git a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift index 48761f12a1..e0b2655301 100644 --- a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift +++ b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift @@ -82,6 +82,10 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { self.validatedGesture = false self.currentAllowedDirections = [] } + + public func cancel() { + self.state = .cancelled + } override public func touchesBegan(_ touches: Set, with event: UIEvent) { let touch = touches.first! diff --git a/submodules/Display/Source/TransformImageArguments.swift b/submodules/Display/Source/TransformImageArguments.swift index fd3b359d7e..d874eb56de 100644 --- a/submodules/Display/Source/TransformImageArguments.swift +++ b/submodules/Display/Source/TransformImageArguments.swift @@ -12,15 +12,15 @@ public protocol TransformImageCustomArguments { } public struct TransformImageArguments: Equatable { - public let corners: ImageCorners + public var corners: ImageCorners - public let imageSize: CGSize - public let boundingSize: CGSize - public let intrinsicInsets: UIEdgeInsets - public let resizeMode: TransformImageResizeMode - public let emptyColor: UIColor? - public let custom: TransformImageCustomArguments? - public let scale: CGFloat? + public var imageSize: CGSize + public var boundingSize: CGSize + public var intrinsicInsets: UIEdgeInsets + public var resizeMode: TransformImageResizeMode + public var emptyColor: UIColor? + public var custom: TransformImageCustomArguments? + public var scale: CGFloat? public init(corners: ImageCorners, imageSize: CGSize, boundingSize: CGSize, intrinsicInsets: UIEdgeInsets, resizeMode: TransformImageResizeMode = .fill(.black), emptyColor: UIColor? = nil, custom: TransformImageCustomArguments? = nil, scale: CGFloat? = nil) { self.corners = corners diff --git a/submodules/Display/Source/WindowPanRecognizer.swift b/submodules/Display/Source/WindowPanRecognizer.swift index d9180edbaa..53ed394912 100644 --- a/submodules/Display/Source/WindowPanRecognizer.swift +++ b/submodules/Display/Source/WindowPanRecognizer.swift @@ -13,6 +13,10 @@ public final class WindowPanRecognizer: UIGestureRecognizer { self.previousPoints.removeAll() } + + public func cancel() { + self.state = .cancelled + } private func addPoint(_ point: CGPoint) { self.previousPoints.append((point, CACurrentMediaTime())) diff --git a/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift b/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift index 7ff05d9c24..5b231df4bd 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageAnchorItem: InstantPageItem { let wantsNode: Bool = false @@ -28,7 +29,7 @@ final class InstantPageAnchorItem: InstantPageItem { func drawInTile(context: CGContext) { } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift b/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift index efc678f687..ac347707b9 100644 --- a/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageArticleItem: InstantPageItem { var frame: CGRect @@ -35,7 +36,7 @@ final class InstantPageArticleItem: InstantPageItem { self.hasRTL = hasRTL } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageArticleNode(context: context, item: self, webPage: self.webPage, strings: strings, theme: theme, contentItems: self.contentItems, contentSize: self.contentSize, cover: self.cover, url: self.url, webpageId: self.webpageId, openUrl: openUrl) } diff --git a/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift b/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift index 8516284d54..6cb3c8f9fb 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageAudioItem: InstantPageItem { var frame: CGRect @@ -24,7 +25,7 @@ final class InstantPageAudioItem: InstantPageItem { self.medias = [media] } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageAudioNode(context: context, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift index be14889554..3c515bafc8 100644 --- a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift @@ -193,7 +193,9 @@ final class InstantPageContentNode : ASDisplayNode { self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) - }, openPeer: { [weak self] peerId in + }, + activatePinchPreview: nil, + openPeer: { [weak self] peerId in self?.openPeer(peerId) }, openUrl: { [weak self] url in self?.openUrl(url) diff --git a/submodules/InstantPageUI/Sources/InstantPageController.swift b/submodules/InstantPageUI/Sources/InstantPageController.swift index 5bbb5f9c17..474edd941a 100644 --- a/submodules/InstantPageUI/Sources/InstantPageController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageController.swift @@ -146,7 +146,7 @@ public final class InstantPageController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageControllerNode(context: self.context, settings: self.settings, themeSettings: self.themeSettings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, autoNightModeTriggered: self.presentationData.autoNightModeTriggered, statusBar: self.statusBar, sourcePeerType: self.sourcePeerType, getNavigationController: { [weak self] in + self.displayNode = InstantPageControllerNode(controller: self, context: self.context, settings: self.settings, themeSettings: self.themeSettings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, autoNightModeTriggered: self.presentationData.autoNightModeTriggered, statusBar: self.statusBar, sourcePeerType: self.sourcePeerType, getNavigationController: { [weak self] in return self?.navigationController as? NavigationController }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a, blockInteraction: true) diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 423e567a5f..cde35cd894 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -16,8 +16,10 @@ import GalleryUI import OpenInExternalAppUI import LocationUI import UndoUI +import ContextUI final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { + private weak var controller: InstantPageController? private let context: AccountContext private var settings: InstantPagePresentationSettings? private var themeSettings: PresentationThemeSettings? @@ -89,7 +91,8 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return InstantPageStoredState(contentOffset: Double(self.scrollNode.view.contentOffset.y), details: details) } - init(context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, autoNightModeTriggered: Bool, statusBar: StatusBar, sourcePeerType: MediaAutoDownloadPeerType, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { + init(controller: InstantPageController, context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, autoNightModeTriggered: Bool, statusBar: StatusBar, sourcePeerType: MediaAutoDownloadPeerType, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { + self.controller = controller self.context = context self.presentationTheme = presentationTheme self.dateTimeFormat = dateTimeFormat @@ -556,6 +559,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) + }, activatePinchPreview: { [weak self] sourceNode in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + let pinchController = PinchController(sourceNode: sourceNode) + controller.window?.presentInGlobalOverlay(pinchController) }, openPeer: { [weak self] peerId in self?.openPeer(peerId) }, openUrl: { [weak self] url in diff --git a/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift b/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift index 4dfef89b24..10d6f883c5 100644 --- a/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift @@ -8,6 +8,7 @@ import Display import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageDetailsItem: InstantPageItem { var frame: CGRect @@ -40,7 +41,7 @@ final class InstantPageDetailsItem: InstantPageItem { self.index = index } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { var expanded: Bool? if let expandedDetails = currentExpandedDetails, let currentlyExpanded = expandedDetails[self.index] { expanded = currentlyExpanded diff --git a/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift b/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift index 4bcf2e92e9..4d0e22b402 100644 --- a/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageFeedbackItem: InstantPageItem { var frame: CGRect @@ -21,7 +22,7 @@ final class InstantPageFeedbackItem: InstantPageItem { self.webPage = webPage } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageFeedbackNode(context: context, strings: strings, theme: theme, webPage: self.webPage, openUrl: openUrl) } diff --git a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift index 6bed10a920..57b529e572 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI protocol InstantPageImageAttribute { } @@ -45,8 +46,8 @@ final class InstantPageImageItem: InstantPageItem { self.fit = fit } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { - return InstantPageImageNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia) + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + return InstantPageImageNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift index 73e634010e..839b401dcf 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift @@ -15,6 +15,7 @@ import LocationResources import LiveLocationPositionNode import AppBundle import TelegramUIPreferences +import ContextUI private struct FetchControls { let fetch: (Bool) -> Void @@ -34,7 +35,8 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private let longPressMedia: (InstantPageMedia) -> Void private var fetchControls: FetchControls? - + + private let pinchContainerNode: PinchSourceContainerNode private let imageNode: TransformImageNode private let statusNode: RadialStatusNode private let linkIconNode: ASImageNode @@ -48,7 +50,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private var themeUpdated: Bool = false - init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void) { + init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?) { self.context = context self.theme = theme self.webPage = webPage @@ -59,15 +61,17 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { self.fit = fit self.openMedia = openMedia self.longPressMedia = longPressMedia - + + self.pinchContainerNode = PinchSourceContainerNode() self.imageNode = TransformImageNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) self.linkIconNode = ASImageNode() self.pinNode = ChatMessageLiveLocationPositionNode() super.init() - - self.addSubnode(self.imageNode) + + self.pinchContainerNode.contentNode.addSubnode(self.imageNode) + self.addSubnode(self.pinchContainerNode) if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) @@ -97,10 +101,10 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { if media.url != nil { self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink") - self.addSubnode(self.linkIconNode) + self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode) } - self.addSubnode(self.statusNode) + self.pinchContainerNode.contentNode.addSubnode(self.statusNode) } } else if let file = media.media as? TelegramMediaFile { let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) @@ -114,16 +118,14 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } if file.isVideo { self.statusNode.transitionToState(.play(.white), animated: false, completion: {}) - self.addSubnode(self.statusNode) + self.pinchContainerNode.contentNode.addSubnode(self.statusNode) } } else if let map = media.media as? TelegramMediaMap { self.addSubnode(self.pinNode) - - var zoom: Int32 = 12 + var dimensions = CGSize(width: 200.0, height: 100.0) for attribute in self.attributes { if let mapAttribute = attribute as? InstantPageMapAttribute { - zoom = mapAttribute.zoom dimensions = mapAttribute.dimensions break } @@ -135,7 +137,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference)) self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) self.statusNode.transitionToState(.play(.white), animated: false, completion: {}) - self.addSubnode(self.statusNode) + self.pinchContainerNode.contentNode.addSubnode(self.statusNode) + } + + if let activatePinchPreview = activatePinchPreview { + self.pinchContainerNode.activate = { sourceNode in + activatePinchPreview(sourceNode) + } } } @@ -198,7 +206,9 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { if self.currentSize != size || self.themeUpdated { self.currentSize = size self.themeUpdated = false - + + self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.pinchContainerNode.update(size: size, transition: .immediate) self.imageNode.frame = CGRect(origin: CGPoint(), size: size) let radialStatusSize: CGFloat = 50.0 diff --git a/submodules/InstantPageUI/Sources/InstantPageItem.swift b/submodules/InstantPageUI/Sources/InstantPageItem.swift index c60bd39cf0..463cad2601 100644 --- a/submodules/InstantPageUI/Sources/InstantPageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI protocol InstantPageItem { var frame: CGRect { get set } @@ -16,7 +17,7 @@ protocol InstantPageItem { func matchesAnchor(_ anchor: String) -> Bool func drawInTile(context: CGContext) - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? func matchesNode(_ node: InstantPageNode) -> Bool func linkSelectionRects(at point: CGPoint) -> [CGRect] diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift index 3b5e71ccc4..2d82943957 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPagePeerReferenceItem: InstantPageItem { var frame: CGRect @@ -27,7 +28,7 @@ final class InstantPagePeerReferenceItem: InstantPageItem { self.rtl = rtl } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPagePeerReferenceNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, transparent: self.transparent, rtl: self.rtl, openPeer: openPeer) } diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift index d43d84cc0c..8157bdb07d 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPagePlayableVideoItem: InstantPageItem { var frame: CGRect @@ -29,7 +30,7 @@ final class InstantPagePlayableVideoItem: InstantPageItem { self.interactive = interactive } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPagePlayableVideoNode(context: context, webPage: self.webPage, theme: theme, media: self.media, interactive: self.interactive, openMedia: openMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift b/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift index 3d22f56dd6..597e9e15ac 100644 --- a/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI enum InstantPageShape { case rect @@ -62,7 +63,7 @@ final class InstantPageShapeItem: InstantPageItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift index 9ce77fc9a6..bd047dc032 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageSlideshowItem: InstantPageItem { var frame: CGRect @@ -21,7 +22,7 @@ final class InstantPageSlideshowItem: InstantPageItem { self.medias = medias } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageSlideshowNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia, longPressMedia: longPressMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift index d545caade5..5bfaf94a6f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift @@ -183,7 +183,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe let media = self.items[index] let contentNode: ASDisplayNode if let _ = media.media as? TelegramMediaImage { - contentNode = InstantPageImageNode(context: self.context, sourcePeerType: self.sourcePeerType, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia) + contentNode = InstantPageImageNode(context: self.context, sourcePeerType: self.sourcePeerType, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: nil) } else if let file = media.media as? TelegramMediaFile { contentNode = ASDisplayNode() } else { diff --git a/submodules/InstantPageUI/Sources/InstantPageTableItem.swift b/submodules/InstantPageUI/Sources/InstantPageTableItem.swift index b496136023..50a5254f6d 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTableItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTableItem.swift @@ -8,6 +8,7 @@ import Display import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI private struct TableSide: OptionSet { var rawValue: Int32 = 0 @@ -200,12 +201,12 @@ final class InstantPageTableItem: InstantPageScrollableItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { var additionalNodes: [InstantPageNode] = [] for cell in self.cells { for item in cell.additionalItems { if item.wantsNode { - if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { + if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { node.frame = item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY) additionalNodes.append(node) } diff --git a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift index c9483f1fba..f1ee5708bc 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift @@ -9,6 +9,7 @@ import TelegramPresentationData import TelegramUIPreferences import TextFormat import AccountContext +import ContextUI public final class InstantPageUrlItem: Equatable { public let url: String @@ -436,7 +437,7 @@ final class InstantPageTextItem: InstantPageItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return nil } @@ -485,11 +486,11 @@ final class InstantPageScrollableTextItem: InstantPageScrollableItem { context.restoreGState() } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { var additionalNodes: [InstantPageNode] = [] for item in additionalItems { if item.wantsNode { - if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { + if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { node.frame = item.frame additionalNodes.append(node) } diff --git a/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift b/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift index f432f660ba..a9868840e2 100644 --- a/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageWebEmbedItem: InstantPageItem { var frame: CGRect @@ -25,7 +26,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { self.enableScrolling = enableScrolling } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: updateWebEmbedHeight) } diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 6202d5678f..76149fcc15 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -565,7 +565,7 @@ public func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReferenc } } -public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) |> map { _, _, generate in return generate @@ -684,7 +684,7 @@ public func chatMessagePhotoInternal(photoData: Signal NumberFormatter { numberFormatter.positiveFormat = result numberFormatter.negativeFormat = "-\(result)" - numberFormatter.currencySymbol = entry.symbol + numberFormatter.currencySymbol = "" numberFormatter.currencyDecimalSeparator = entry.decimalSeparator numberFormatter.currencyGroupingSeparator = entry.thousandsSeparator @@ -164,3 +164,42 @@ public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { return formatter.string(from: (Float(amount) * 0.01) as NSNumber) ?? "" } } + +public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String) { + if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { + var result = "" + if amount < 0 { + result.append("-") + } + /*if entry.symbolOnLeft { + result.append(entry.symbol) + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + }*/ + var integerPart = abs(amount) + var fractional: [Character] = [] + for _ in 0 ..< entry.decimalDigits { + let part = integerPart % 10 + integerPart /= 10 + if let scalar = UnicodeScalar(UInt32(part + 48)) { + fractional.append(Character(scalar)) + } + } + result.append("\(integerPart)") + result.append(entry.decimalSeparator) + for i in 0 ..< fractional.count { + result.append(fractional[fractional.count - i - 1]) + } + /*if !entry.symbolOnLeft { + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + result.append(entry.symbol) + }*/ + + return (result, entry.symbol) + } else { + return ("", "") + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6edc11f355..e72c6a2fae 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -912,6 +912,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(controller) }) } + }, activateMessagePinch: { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + let pinchController = PinchController(sourceNode: sourceNode) + strongSelf.window?.presentInGlobalOverlay(pinchController) }, openMessageContextActions: { message, node, rect, gesture in gesture?.cancel() }, navigateToMessage: { [weak self] fromId, id in diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 2863f406e8..f6997caa90 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -53,6 +53,7 @@ public final class ChatControllerInteraction { let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void + let activateMessagePinch: (PinchSourceContainerNode) -> Void let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void let navigateToMessageStandalone: (MessageId) -> Void @@ -144,6 +145,7 @@ public final class ChatControllerInteraction { openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, + activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, navigateToMessageStandalone: @escaping (MessageId) -> Void, @@ -222,6 +224,7 @@ public final class ChatControllerInteraction { self.openPeer = openPeer self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu + self.activateMessagePinch = activateMessagePinch self.openMessageContextActions = openMessageContextActions self.navigateToMessage = navigateToMessage self.navigateToMessageStandalone = navigateToMessageStandalone @@ -301,7 +304,7 @@ public final class ChatControllerInteraction { static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil }, chatControllerNode: { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index fe7c0e93fa..ebab0dad4c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -143,6 +143,7 @@ class ChatMessageShareButton: HighlightableButtonNode { class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode + private let pinchContainerNode: PinchSourceContainerNode let imageNode: TransformImageNode private var placeholderNode: StickerShimmerEffectNode private var animationNode: GenericAnimatedStickerNode? @@ -195,6 +196,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { required init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() + self.pinchContainerNode = PinchSourceContainerNode() self.imageNode = TransformImageNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode() @@ -262,7 +264,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.imageNode.displaysAsynchronously = false self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode - self.addSubnode(self.containerNode) + self.pinchContainerNode.contentNode.addSubnode(self.containerNode) + self.addSubnode(self.pinchContainerNode) self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.placeholderNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) @@ -278,6 +281,23 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } item.controllerInteraction.openMessageReactions(item.message.id) } + + self.pinchContainerNode.activate = { [weak self] sourceNode in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.activateMessagePinch(sourceNode) + } + + self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in + guard let strongSelf = self else { + return + } + + let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0)) + + transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor) + } } deinit { @@ -976,6 +996,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) @@ -1063,6 +1085,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { imageApply() strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame + strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect if let updatedShareButtonNode = updatedShareButtonNode { diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 12e233aa60..2ddfe11858 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -271,7 +271,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { + func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let textAsyncLayout = TextNode.asyncLayout(self.textNode) let currentImage = self.media as? TelegramMediaImage let imageLayout = self.inlineImageNode.asyncLayout() @@ -284,7 +284,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode - return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in + return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize in let isPreview = presentationData.isPreview let fontSize: CGFloat = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0) @@ -420,11 +420,99 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if case .replyThread = chatLocation { isReplyThread = true } + + var imageMode = false + + var textStatusType: ChatMessageDateAndStatusType? + var imageStatusType: ChatMessageDateAndStatusType? + var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? + + if let (media, flags) = mediaAndFlags { + if let file = media as? TelegramMediaFile { + if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { + imageMode = true + } else if file.isInstantVideo { + imageMode = true + } else if file.isVideo { + imageMode = true + } else if file.isSticker || file.isAnimatedSticker { + imageMode = true + } + } else if let _ = media as? TelegramMediaImage { + if !flags.contains(.preferMediaInline) { + imageMode = true + } + } else if let _ = media as? TelegramMediaWebFile { + imageMode = true + } else if let _ = media as? WallpaperPreviewMedia { + imageMode = true + } + } + + if preferMediaBeforeText { + imageMode = false + } + + let statusInText = !imageMode + + switch preparePosition { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if let count = webpageGalleryMediaCount { + additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").0)) + skipStandardStatus = imageMode + } else if let mediaBadge = mediaBadge { + additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge)) + } + + if !skipStandardStatus { + if message.effectivelyIncoming(context.account.peerId) { + if imageMode { + imageStatusType = .ImageIncoming + } else { + textStatusType = .BubbleIncoming + } + } else { + if message.flags.contains(.Failed) { + if imageMode { + imageStatusType = .ImageOutgoing(.Failed) + } else { + textStatusType = .BubbleOutgoing(.Failed) + } + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { + if imageMode { + imageStatusType = .ImageOutgoing(.Sending) + } else { + textStatusType = .BubbleOutgoing(.Sending) + } + } else { + if imageMode { + imageStatusType = .ImageOutgoing(.Sent(read: messageRead)) + } else { + textStatusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + } + } + default: + break + } + + let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in + ChatMessageDateAndStatus( + type: statusType, + edited: edited, + viewCount: viewCount, + dateReplies: dateReplies, + dateReactions: dateReactions, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + dateText: dateText + ) + } if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isInstantVideo { @@ -455,12 +543,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isSticker || file.isAnimatedSticker { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else { @@ -485,7 +573,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else if let image = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { @@ -497,11 +585,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } else if let image = media as? TelegramMediaWebFile { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let wallpaper = media as? WallpaperPreviewMedia { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, wallpaper, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { @@ -527,60 +615,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { break } - var statusInText = false - var statusSizeAndApply: (CGSize, (Bool) -> Void)? - + let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) - - var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? - - switch position { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): - let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil && contentInstantVideoSizeAndApply == nil) || preferMediaBeforeText) - statusInText = !imageMode - - if let count = webpageGalleryMediaCount { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").0)) - skipStandardStatus = imageMode - } else if let mediaBadge = mediaBadge { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge)) - } - - if !skipStandardStatus { - let statusType: ChatMessageDateAndStatusType - if message.effectivelyIncoming(context.account.peerId) { - if imageMode { - statusType = .ImageIncoming - } else { - statusType = .BubbleIncoming - } - } else { - if message.flags.contains(.Failed) { - if imageMode { - statusType = .ImageOutgoing(.Failed) - } else { - statusType = .BubbleOutgoing(.Failed) - } - } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { - if imageMode { - statusType = .ImageOutgoing(.Sending) - } else { - statusType = .BubbleOutgoing(.Sending) - } - } else { - if imageMode { - statusType = .ImageOutgoing(.Sent(read: messageRead)) - } else { - statusType = .BubbleOutgoing(.Sent(read: messageRead)) - } - } - } - - statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring) - } - default: - break + + if let textStatusType = textStatusType { + statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, textStatusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring) } var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge? @@ -823,6 +863,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let contentImageNode = contentImageApply(transition, synchronousLoads) if strongSelf.contentImageNode !== contentImageNode { strongSelf.contentImageNode = contentImageNode + contentImageNode.activatePinch = { sourceNode in + controllerInteraction.activateMessagePinch(sourceNode) + } strongSelf.addSubnode(contentImageNode) contentImageNode.activateLocalContent = { [weak strongSelf] mode in if let strongSelf = strongSelf { diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 2803acb223..1328a749ad 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -236,7 +236,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let clockMinImage: UIImage? var impressionImage: UIImage? var repliesImage: UIImage? - var selfExpiringImage: UIImage? + let selfExpiringImage: UIImage? = nil let themeUpdated = presentationData.theme != currentTheme || type != currentType diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift index 110dc38a36..5b3cbb1970 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var messageEntities: [MessageTextEntity]? for attribute in item.message.attributes { @@ -44,7 +44,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble } let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift index c0ff0ffa39..c481ff165c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift @@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var messageEntities: [MessageTextEntity]? for attribute in item.message.attributes { @@ -39,7 +39,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent let text: String = item.message.text let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift index 0d638f3fd4..b0e5ba0891 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift @@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var messageEntities: [MessageTextEntity]? for attribute in item.message.attributes { @@ -44,7 +44,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont } let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift index de1c237da2..240ee141fa 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift @@ -45,7 +45,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var game: TelegramMediaGame? var messageEntities: [MessageTextEntity]? @@ -78,7 +78,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(item.message.id.peerId), title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(item.message.id.peerId), title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index fed0355010..58a9228d9d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -22,6 +22,7 @@ import TelegramAnimatedStickerNode import LocalMediaResources import WallpaperResources import ChatMessageInteractiveMediaBadge +import ContextUI private struct FetchControls { let fetch: (Bool) -> Void @@ -64,9 +65,23 @@ enum InteractiveMediaNodePlayWithSoundMode { case loop } +struct ChatMessageDateAndStatus { + var type: ChatMessageDateAndStatusType + var edited: Bool + var viewCount: Int? + var dateReplies: Int + var dateReactions: [MessageReaction] + var isPinned: Bool + var dateText: String +} + final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitionNode { + private let pinchContainerNode: PinchSourceContainerNode private let imageNode: TransformImageNode private var currentImageArguments: TransformImageArguments? + private var currentHighQualityImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + private var highQualityImageNode: TransformImageNode? + private var videoNode: UniversalVideoNode? private var videoContent: NativeVideoContent? private var animatedStickerNode: AnimatedStickerNode? @@ -75,6 +90,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var decoration: UniversalVideoDecoration? { return self.videoNodeDecoration } + let dateAndStatusNode: ChatMessageDateAndStatusNode private var badgeNode: ChatMessageInteractiveMediaBadge? private var tapRecognizer: UITapGestureRecognizer? @@ -134,15 +150,74 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in } + var activatePinch: ((PinchSourceContainerNode) -> Void)? override init() { + self.pinchContainerNode = PinchSourceContainerNode() + + self.dateAndStatusNode = ChatMessageDateAndStatusNode() + self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] super.init() + + self.addSubnode(self.pinchContainerNode) self.imageNode.displaysAsynchronously = false - self.addSubnode(self.imageNode) + self.pinchContainerNode.contentNode.addSubnode(self.imageNode) + + self.pinchContainerNode.activate = { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + strongSelf.activatePinch?(sourceNode) + } + + self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in + guard let strongSelf = self else { + return + } + + let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0)) + + transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor) + + if abs(scale - 1.0) > CGFloat.ulpOfOne { + var highQualityImageNode: TransformImageNode? + if let current = strongSelf.highQualityImageNode { + highQualityImageNode = current + } else if let currentHighQualityImageSignal = strongSelf.currentHighQualityImageSignal, let currentImageArguments = strongSelf.currentImageArguments { + let imageNode = TransformImageNode() + imageNode.frame = strongSelf.imageNode.frame + strongSelf.pinchContainerNode.contentNode.insertSubnode(imageNode, aboveSubnode: strongSelf.imageNode) + + var updatedArguments = currentImageArguments + updatedArguments.scale = 3.0 + let apply = imageNode.asyncLayout()(updatedArguments) + let _ = apply() + imageNode.setSignal(currentHighQualityImageSignal, attemptSynchronously: false) + + highQualityImageNode = imageNode + strongSelf.highQualityImageNode = imageNode + } + if let highQualityImageNode = highQualityImageNode { + transition.updateAlpha(node: highQualityImageNode, alpha: factor) + } + } else if let highQualityImageNode = strongSelf.highQualityImageNode { + strongSelf.highQualityImageNode = nil + transition.updateAlpha(node: highQualityImageNode, alpha: 0.0, completion: { [weak highQualityImageNode] _ in + highQualityImageNode?.removeFromSupernode() + }) + } + + if let badgeNode = strongSelf.badgeNode { + transition.updateAlpha(node: badgeNode, alpha: 1.0 - factor) + } + if let statusNode = strongSelf.statusNode { + transition.updateAlpha(node: statusNode, alpha: 1.0 - factor) + } + } } deinit { @@ -242,10 +317,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - func asyncLayout() -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { + func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { let currentMessage = self.message let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() + let statusLayout = self.dateAndStatusNode.asyncLayout() let currentVideoNode = self.videoNode let currentAnimatedStickerNode = self.animatedStickerNode @@ -255,7 +331,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback - return { [weak self] context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { [weak self] context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var nativeSize: CGSize let isSecretMedia = message.containsSecretMedia @@ -359,6 +435,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio case .unconstrained: nativeSize = unboundSize } + + var statusSize = CGSize() + var statusApply: ((Bool) -> Void)? + + if let dateAndStatus = dateAndStatus { + let (size, apply) = statusLayout(context, presentationData, dateAndStatus.edited, dateAndStatus.viewCount, dateAndStatus.dateText, dateAndStatus.type, CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateAndStatus.dateReactions, dateAndStatus.dateReplies, dateAndStatus.isPinned, message.isSelfExpiring) + statusSize = size + statusApply = apply + } let maxWidth: CGFloat if isSecretMedia { @@ -367,7 +452,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio maxWidth = maxDimensions.width } if isSecretMedia { - let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) + let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(presentationData.theme.theme) } return (nativeSize, maxWidth, { constrainedSize, automaticPlayback, wideLayout, corners in @@ -416,7 +501,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio drawingSize = nativeSize.aspectFilled(boundingSize) } - var updateImageSignal: ((Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)? + var updateImageSignal: ((Bool, Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)? var updatedStatusSignal: Signal<(MediaResourceStatus, MediaResourceStatus?), NoError>? var updatedFetchControls: FetchControls? @@ -453,7 +538,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if isSticker { emptyColor = .clear } else { - emptyColor = message.effectivelyIncoming(context.account.peerId) ? theme.chat.message.incoming.mediaPlaceholderColor : theme.chat.message.outgoing.mediaPlaceholderColor + emptyColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor } if let wallpaper = media as? WallpaperPreviewMedia { if case let .file(_, patternColor, patternBottomColor, rotation, _, _) = wallpaper.content { @@ -475,12 +560,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio replaceAnimatedStickerNode = true } if isSecretMedia { - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatSecretPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image)) } } else { - updateImageSignal = { synchronousLoad in - return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad) + updateImageSignal = { synchronousLoad, highQuality in + return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality) } } @@ -505,7 +590,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if hasCurrentAnimatedStickerNode { replaceAnimatedStickerNode = true } - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatWebFileImage(account: context.account, file: image) } @@ -518,22 +603,22 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio }) } else if let file = media as? TelegramMediaFile { if isSecretMedia { - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatSecretMessageVideo(account: context.account, videoReference: .message(message: MessageReference(message), media: file)) } } else { if file.isAnimatedSticker { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatMessageAnimatedSticker(postbox: context.account.postbox, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) } } else if file.isSticker { - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatMessageSticker(account: context.account, file: file, small: false) } } else { onlyFullSizeVideoThumbnail = isSendingUpdated - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) } } @@ -598,7 +683,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } }) } else if let wallpaper = media as? WallpaperPreviewMedia { - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in switch wallpaper.content { case let .file(file, _, _, _, isTheme, _): if isTheme { @@ -692,27 +777,52 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio strongSelf.attributes = attributes strongSelf.media = media strongSelf.wideLayout = wideLayout - strongSelf.themeAndStrings = (theme, strings, dateTimeFormat.decimalSeparator) + strongSelf.themeAndStrings = (presentationData.theme.theme, presentationData.strings, dateTimeFormat.decimalSeparator) strongSelf.sizeCalculation = sizeCalculation strongSelf.automaticPlayback = automaticPlayback strongSelf.automaticDownload = automaticDownload if let previousArguments = strongSelf.currentImageArguments { if previousArguments.imageSize == arguments.imageSize { - strongSelf.imageNode.frame = imageFrame + strongSelf.pinchContainerNode.frame = imageFrame + strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) } else { - transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) + transition.updateFrame(node: strongSelf.pinchContainerNode, frame: imageFrame) + transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(), size: imageFrame.size)) + strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: transition) + } } else { - strongSelf.imageNode.frame = imageFrame + strongSelf.pinchContainerNode.frame = imageFrame + strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) } strongSelf.currentImageArguments = arguments imageApply() + + if let statusApply = statusApply { + if strongSelf.dateAndStatusNode.supernode == nil { + strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode) + } + var hasAnimation = true + if transition.isAnimated { + hasAnimation = false + } + statusApply(hasAnimation) + + let dateAndStatusFrame = CGRect(origin: CGPoint(x: imageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: imageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + + strongSelf.dateAndStatusNode.frame = dateAndStatusFrame + strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) + } else if strongSelf.dateAndStatusNode.supernode != nil { + strongSelf.dateAndStatusNode.removeFromSupernode() + } if let statusNode = strongSelf.statusNode { var statusFrame = statusNode.frame - statusFrame.origin.x = floor(imageFrame.midX - statusFrame.width / 2.0) - statusFrame.origin.y = floor(imageFrame.midY - statusFrame.height / 2.0) + statusFrame.origin.x = floor(imageFrame.width / 2.0 - statusFrame.width / 2.0) + statusFrame.origin.y = floor(imageFrame.height / 2.0 - statusFrame.height / 2.0) statusNode.frame = statusFrame } @@ -776,7 +886,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let dimensions = updatedAnimatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: updatedAnimatedStickerFile.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) - strongSelf.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode) + strongSelf.pinchContainerNode.contentNode.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode) animatedStickerNode.visibility = strongSelf.visibility } } @@ -807,7 +917,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads), attemptSynchronously: synchronousLoads) + strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads, false), attemptSynchronously: synchronousLoads) + strongSelf.currentHighQualityImageSignal = updateImageSignal(false, true) } if let _ = secretBeginTimeAndTimeout { @@ -837,7 +948,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio |> deliverOnMainQueue).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf, let videoNode = strongSelf.videoNode { - strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) + strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) } } })) @@ -997,10 +1108,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if progressRequired { if self.statusNode == nil { let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.message.mediaOverlayControlColors.fillColor) - let imagePosition = self.imageNode.position - statusNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - radialStatusSize / 2.0), y: floor(imagePosition.y - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) + let imageSize = self.imageNode.bounds.size + statusNode.frame = CGRect(origin: CGPoint(x: floor(imageSize.width / 2.0 - radialStatusSize / 2.0), y: floor(imageSize.height / 2.0 - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) self.statusNode = statusNode - self.addSubnode(statusNode) + self.pinchContainerNode.contentNode.addSubnode(statusNode) } } else { if let statusNode = self.statusNode { @@ -1275,7 +1386,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } self.badgeNode = badgeNode - self.addSubnode(badgeNode) + self.pinchContainerNode.contentNode.addSubnode(badgeNode) animated = false } @@ -1300,12 +1411,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) + var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -1315,7 +1426,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio imageLayout = imageNode.asyncLayout() } - let (unboundSize, initialWidth, continueLayout) = imageLayout(context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners) diff --git a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift index c30749172b..b38c677bcc 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -38,7 +38,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var invoice: TelegramMediaInvoice? for media in item.message.media { if let media = media as? TelegramMediaInvoice { @@ -74,7 +74,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index 9a6d8bd125..d564507543 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -17,7 +17,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } private let interactiveImageNode: ChatMessageInteractiveMediaNode - private let dateAndStatusNode: ChatMessageDateAndStatusNode private var selectionNode: GridMessageSelectionNode? private var highlightedState: Bool = false @@ -32,7 +31,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { required init() { self.interactiveImageNode = ChatMessageInteractiveMediaNode() - self.dateAndStatusNode = ChatMessageDateAndStatusNode() super.init() @@ -54,6 +52,13 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } } + + self.interactiveImageNode.activatePinch = { [weak self] sourceNode in + guard let strongSelf = self, let _ = strongSelf.item else { + return + } + strongSelf.item?.controllerInteraction.activateMessagePinch(sourceNode) + } } required init?(coder aDecoder: NSCoder) { @@ -62,7 +67,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let interactiveImageLayout = self.interactiveImageNode.asyncLayout() - let statusLayout = self.dateAndStatusNode.asyncLayout() return { item, layoutConstants, preparePosition, selection, constrainedSize in var selectedMedia: Media? @@ -142,8 +146,81 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { bubbleInsets = UIEdgeInsets() sizeCalculation = .unconstrained } + + var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } + var viewCount: Int? + var dateReplies = 0 + for attribute in item.message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + if case .mosaic = preparePosition { + } else { + edited = !attribute.isHidden + } + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { + if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { + dateReplies = Int(attribute.count) + } + } + } + + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) + + let statusType: ChatMessageDateAndStatusType? + switch preparePosition { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if item.message.effectivelyIncoming(item.context.account.peerId) { + statusType = .ImageIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .ImageOutgoing(.Failed) + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .ImageOutgoing(.Sent(read: item.read)) + } + } + case .mosaic: + statusType = nil + default: + statusType = nil + } + + var isReplyThread = false + if case .replyThread = item.chatLocation { + isReplyThread = true + } + + let dateAndStatus = statusType.flatMap { statusType -> ChatMessageDateAndStatus in + ChatMessageDateAndStatus( + type: statusType, + edited: edited, + viewCount: viewCount, + dateReplies: dateReplies, + dateReactions: dateReactions, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, + dateText: dateText + ) + } - let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData.theme.theme, item.presentationData.strings, item.presentationData.dateTimeFormat, item.message, item.attributes, selectedMedia!, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode) let forceFullCorners = false let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none) @@ -169,82 +246,9 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return (refinedWidth + bubbleInsets.left + bubbleInsets.right, { boundingWidth in let (imageSize, imageApply) = finishLayout(boundingWidth - bubbleInsets.left - bubbleInsets.right) - var edited = false - if item.attributes.updatingMedia != nil { - edited = true - } - var viewCount: Int? - var dateReplies = 0 - for attribute in item.message.attributes { - if let attribute = attribute as? EditedMessageAttribute { - if case .mosaic = preparePosition { - } else { - edited = !attribute.isHidden - } - } else if let attribute = attribute as? ViewCountMessageAttribute { - viewCount = attribute.count - } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { - if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { - dateReplies = Int(attribute.count) - } - } - } - - var dateReactions: [MessageReaction] = [] - var dateReactionCount = 0 - if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { - for reaction in reactionsAttribute.reactions { - if reaction.isSelected { - dateReactions.insert(reaction, at: 0) - } else { - dateReactions.append(reaction) - } - dateReactionCount += Int(reaction.count) - } - } - - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) - - let statusType: ChatMessageDateAndStatusType? - switch position { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): - if item.message.effectivelyIncoming(item.context.account.peerId) { - statusType = .ImageIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .ImageOutgoing(.Failed) - } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { - statusType = .ImageOutgoing(.Sending) - } else { - statusType = .ImageOutgoing(.Sent(read: item.read)) - } - } - case .mosaic: - statusType = nil - default: - statusType = nil - } - let imageLayoutSize = CGSize(width: imageSize.width + bubbleInsets.left + bubbleInsets.right, height: imageSize.height + bubbleInsets.top + bubbleInsets.bottom) - var statusSize = CGSize() - var statusApply: ((Bool) -> Void)? - - if let statusType = statusType { - var isReplyThread = false - if case .replyThread = item.chatLocation { - isReplyThread = true - } - - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) - statusSize = size - statusApply = apply - } - - var layoutWidth = imageLayoutSize.width - if case .constrained = sizeCalculation { - layoutWidth = max(layoutWidth, statusSize.width + bubbleInsets.left + bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right) - } + let layoutWidth = imageLayoutSize.width let layoutSize = CGSize(width: layoutWidth, height: imageLayoutSize.height) @@ -262,24 +266,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { transition.updateFrame(node: strongSelf.interactiveImageNode, frame: imageFrame) - if let statusApply = statusApply { - if strongSelf.dateAndStatusNode.supernode == nil { - strongSelf.interactiveImageNode.addSubnode(strongSelf.dateAndStatusNode) - } - var hasAnimation = true - if case .None = animation { - hasAnimation = false - } - statusApply(hasAnimation) - - let dateAndStatusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) - - strongSelf.dateAndStatusNode.frame = dateAndStatusFrame - strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) - } else if strongSelf.dateAndStatusNode.supernode != nil { - strongSelf.dateAndStatusNode.removeFromSupernode() - } - imageApply(transition, synchronousLoads) if let selection = selection { @@ -310,14 +296,14 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { - strongSelf.dateAndStatusNode.pressed = { + strongSelf.interactiveImageNode.dateAndStatusNode.pressed = { guard let strongSelf = self else { return } - item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode) + item.controllerInteraction.displayImportedMessageTooltip(strongSelf.interactiveImageNode.dateAndStatusNode) } } else { - strongSelf.dateAndStatusNode.pressed = nil + strongSelf.interactiveImageNode.dateAndStatusNode.pressed = nil } } }) @@ -356,7 +342,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveImageNode.isHidden = mediaHidden self.interactiveImageNode.updateIsHidden(mediaHidden) - if let automaticPlayback = self.automaticPlayback { + /*if let automaticPlayback = self.automaticPlayback { if !automaticPlayback { self.dateAndStatusNode.isHidden = false } else if self.dateAndStatusNode.isHidden != mediaHidden { @@ -367,7 +353,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - } + }*/ return mediaHidden } @@ -416,9 +402,9 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { - if !self.dateAndStatusNode.isHidden { + /*if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionNode(value: value) - } + }*/ return nil } } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 3a5ee9c215..98068bd74c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -21,6 +21,7 @@ private let inlineBotNameFont = nameFont class ChatMessageStickerItemNode: ChatMessageItemView { private let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode + private let pinchContainerNode: PinchSourceContainerNode let imageNode: TransformImageNode private var placeholderNode: StickerShimmerEffectNode var textNode: TextNode? @@ -53,6 +54,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { required init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() + self.pinchContainerNode = PinchSourceContainerNode() self.imageNode = TransformImageNode() self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode.isUserInteractionEnabled = false @@ -119,7 +121,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.imageNode.displaysAsynchronously = false self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode - self.addSubnode(self.containerNode) + self.pinchContainerNode.contentNode.addSubnode(self.containerNode) + self.addSubnode(self.pinchContainerNode) self.contextSourceNode.contentNode.addSubnode(self.placeholderNode) self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) @@ -135,6 +138,23 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } item.controllerInteraction.openMessageReactions(item.message.id) } + + self.pinchContainerNode.activate = { [weak self] sourceNode in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.activateMessagePinch(sourceNode) + } + + self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in + guard let strongSelf = self else { + return + } + + let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0)) + + transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor) + } } required init?(coder aDecoder: NSCoder) { @@ -655,9 +675,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame + strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect dateAndStatusApply(false) diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 560ab504f6..229a9f0d58 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -86,7 +86,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var webPage: TelegramMediaWebpage? var webPageContent: TelegramMediaWebpageLoadedContent? for media in item.message.media { @@ -301,7 +301,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index d641c88270..505570d638 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -260,6 +260,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) + }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 3e53710d0a..85da7030df 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -109,7 +109,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in + }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 193982ef4b..d4316a4d95 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -70,6 +70,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in + }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index c2b7d1dc83..643e2e8cbe 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1848,6 +1848,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) + }, activateMessagePinch: { _ in }, openMessageContextActions: { [weak self] message, node, rect, gesture in guard let strongSelf = self else { gesture?.cancel() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e314d79971..7f551fcd19 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1220,7 +1220,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { let controllerInteraction: ChatControllerInteraction if tapMessage != nil || clickThroughMessage != nil { controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in + }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: { message in tapMessage?(message) }, clickThroughMessage: { diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index e8e949c07c..bf31994075 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit e8e949c07cf1bc0b345a566c381a25c17c1e2ad0 +Subproject commit bf319940759582f51749de28545113a864cfd161 From af87c34a625d3b5c9c99d988d03c435f7e46a44f Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 19:17:56 +0400 Subject: [PATCH 2/9] Update tgcalls --- submodules/TgVoipWebrtc/tgcalls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index bf31994075..cb39db6020 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit bf319940759582f51749de28545113a864cfd161 +Subproject commit cb39db6020dee2e33b28d251c00cdab7db1b608b From 761fd23c4085ba034080f7302ada4e9be1f51a6b Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 19:18:09 +0400 Subject: [PATCH 3/9] Fix apply payment method --- .../BotPaymentsUI/Sources/BotCheckoutControllerNode.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 2933684211..81e9b20aa9 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -595,7 +595,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } })]), nil) default: - break + applyPaymentMethod(method) } } else { applyPaymentMethod(method) From f57fee0f927c23fa14c36d25037adef6ab30580d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 19:45:54 +0400 Subject: [PATCH 4/9] Fix build --- .../Sources/TelegramBaseController.swift | 2 +- .../TelegramCallsUI/Sources/PresentationGroupCall.swift | 8 +++++--- .../TelegramCallsUI/Sources/VoiceChatJoinScreen.swift | 2 +- submodules/TelegramUI/Sources/ChatController.swift | 6 +++--- .../TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift | 4 ++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 534023efba..1e6690eedb 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -406,7 +406,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { strongSelf.joinGroupCall( peerId: groupCallPanelData.peerId, invite: nil, - activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title) + activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title, scheduleTimestamp: nil, subscribed: false) ) }) if let navigationBar = self.navigationBar { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 7e259cedc8..6c8fe69220 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -77,6 +77,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { clientParams: nil, streamDcId: nil, title: call.title, + scheduleTimestamp: nil, recordingStartTimestamp: nil, sortAscending: true ), @@ -120,7 +121,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { } return GroupCallPanelData( peerId: peerId, - info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, recordingStartTimestamp: nil, sortAscending: state.sortAscending), + info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, scheduleTimestamp: nil, recordingStartTimestamp: nil, sortAscending: state.sortAscending), topParticipants: topParticipants, participantCount: state.totalCount, activeSpeakers: activeSpeakers, @@ -761,7 +762,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { }) if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in - impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title)) + impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title, scheduleTimestamp: nil, subscribed: false)) }) { self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId) } else { @@ -1638,6 +1639,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { clientParams: nil, streamDcId: nil, title: state.title, + scheduleTimestamp: nil, recordingStartTimestamp: state.recordingStartTimestamp, sortAscending: state.sortAscending )))) @@ -2207,7 +2209,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if let value = value { - strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title) + strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title, scheduleTimestamp: nil, subscribed: false) strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl) } else { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index b6086120ad..c518582468 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -145,7 +145,7 @@ public final class VoiceChatJoinScreen: ViewController { defaultJoinAsPeerId = cachedData.callJoinPeerId } - let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title) + let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title, scheduleTimestamp: nil, subscribed: false) if availablePeers.count > 0 && defaultJoinAsPeerId == nil { strongSelf.dismiss() strongSelf.join(activeCall) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e72c6a2fae..0c6ca47a1c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -535,7 +535,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } case .groupPhoneCall, .inviteToGroupPhoneCall: if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall { - strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title)) + strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: nil, subscribed: false)) } else { var canManageGroupCalls = false if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel { @@ -564,12 +564,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G statusController?.dismiss() } strongSelf.present(statusController, in: .window(.root)) - strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId) + strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId, title: nil, scheduleDate: nil) |> deliverOnMainQueue).start(next: { [weak self] info in guard let strongSelf = self else { return } - strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title)) + strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: nil, subscribed: false)) }, error: { [weak self] error in dismissStatus?() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 643e2e8cbe..31b42287fc 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -4023,14 +4023,14 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD statusController?.dismiss() } strongSelf.controller?.present(statusController, in: .window(.root)) - strongSelf.activeActionDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: peerId) + strongSelf.activeActionDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: peerId, title: nil, scheduleDate: nil) |> deliverOnMainQueue).start(next: { [weak self] info in guard let strongSelf = self else { return } strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in result(joinAsPeerId) - }, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title)) + }, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: nil, subscribed: false)) }, error: { [weak self] error in dismissStatus?() From e1c22bb90212d3c9ca887bb897d3f527150edc4f Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 19:48:30 +0400 Subject: [PATCH 5/9] Fix tips layout --- .../Sources/BotCheckoutTipItem.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift index bd199b9122..f2f184efad 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift @@ -315,16 +315,15 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { index += 1 } - let sideInset: CGFloat = 16.0 + let sideInset: CGFloat = params.leftInset + 16.0 var scaleFactor: CGFloat = 1.0 let availableWidth = params.width - sideInset * 2.0 - CGFloat(max(0, item.availableVariants.count - 1)) * 12.0 if totalMinWidth < availableWidth { scaleFactor = availableWidth / totalMinWidth } - index = 0 - var variantsOffset: CGFloat = 16.0 - for _ in item.availableVariants { + var variantsOffset: CGFloat = sideInset + for index in 0 ..< item.availableVariants.count { if index != 0 { variantsOffset += 12.0 } @@ -334,10 +333,14 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { let nodeWidth = floor(scaleFactor * minWidth) - valueNode.frame = CGRect(origin: CGPoint(x: variantsOffset, y: 0.0), size: CGSize(width: nodeWidth, height: valueHeight)) + var valueFrame = CGRect(origin: CGPoint(x: variantsOffset, y: 0.0), size: CGSize(width: nodeWidth, height: valueHeight)) + if scaleFactor > 1.0 && index == item.availableVariants.count - 1 { + valueFrame.size.width = params.width - sideInset - valueFrame.minX + } + + valueNode.frame = valueFrame nodeApply(nodeWidth) variantsOffset += nodeWidth - index += 1 } variantsOffset += 16.0 From 8b1020826426ad4a44cb9573ebbff0f99cfeb89f Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 20:30:13 +0400 Subject: [PATCH 6/9] Fix pinch image quality --- .../ChatMessageInteractiveMediaNode.swift | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 58a9228d9d..67f5880c87 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -79,7 +79,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio private let pinchContainerNode: PinchSourceContainerNode private let imageNode: TransformImageNode private var currentImageArguments: TransformImageArguments? - private var currentHighQualityImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + private var currentHighQualityImageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize)? private var highQualityImageNode: TransformImageNode? private var videoNode: UniversalVideoNode? @@ -187,14 +187,43 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var highQualityImageNode: TransformImageNode? if let current = strongSelf.highQualityImageNode { highQualityImageNode = current - } else if let currentHighQualityImageSignal = strongSelf.currentHighQualityImageSignal, let currentImageArguments = strongSelf.currentImageArguments { + } else if let (currentHighQualityImageSignal, nativeImageSize) = strongSelf.currentHighQualityImageSignal, let currentImageArguments = strongSelf.currentImageArguments { let imageNode = TransformImageNode() imageNode.frame = strongSelf.imageNode.frame + + let corners = currentImageArguments.corners + if isRoundEqualCorners(corners) { + imageNode.cornerRadius = corners.topLeft.radius + imageNode.layer.mask = nil + } else { + imageNode.cornerRadius = 0 + + let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius)) + let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom) + let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) + let context = DrawingContext(size: size, clear: true) + context.withContext { ctx in + ctx.setFillColor(UIColor.black.cgColor) + ctx.fill(arguments.drawingRect) + } + addCorners(context, arguments: arguments) + + if let maskImage = context.generateImage() { + let mask = CALayer() + mask.contents = maskImage.cgImage + mask.contentsScale = maskImage.scale + mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height) + + imageNode.layer.mask = mask + imageNode.layer.mask?.frame = imageNode.bounds + } + } + strongSelf.pinchContainerNode.contentNode.insertSubnode(imageNode, aboveSubnode: strongSelf.imageNode) - var updatedArguments = currentImageArguments - updatedArguments.scale = 3.0 - let apply = imageNode.asyncLayout()(updatedArguments) + let scaleFactor = nativeImageSize.height / currentImageArguments.imageSize.height + + let apply = imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: currentImageArguments.imageSize.width * scaleFactor, height: currentImageArguments.imageSize.height * scaleFactor), boundingSize: CGSize(width: currentImageArguments.boundingSize.width * scaleFactor, height: currentImageArguments.boundingSize.height * scaleFactor), intrinsicInsets: UIEdgeInsets(top: currentImageArguments.intrinsicInsets.top * scaleFactor, left: currentImageArguments.intrinsicInsets.left * scaleFactor, bottom: currentImageArguments.intrinsicInsets.bottom * scaleFactor, right: currentImageArguments.intrinsicInsets.right * scaleFactor))) let _ = apply() imageNode.setSignal(currentHighQualityImageSignal, attemptSynchronously: false) @@ -918,7 +947,19 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let updateImageSignal = updateImageSignal { strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads, false), attemptSynchronously: synchronousLoads) - strongSelf.currentHighQualityImageSignal = updateImageSignal(false, true) + + var imageDimensions: CGSize? + if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { + imageDimensions = dimensions.cgSize + } else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions { + imageDimensions = dimensions.cgSize + } + + if let imageDimensions = imageDimensions { + strongSelf.currentHighQualityImageSignal = (updateImageSignal(false, true), imageDimensions) + } } if let _ = secretBeginTimeAndTimeout { From df3cef34ff5e07afd674cfc7be31d31310c18942 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 20:30:21 +0400 Subject: [PATCH 7/9] Update tgcalls --- submodules/TgVoipWebrtc/tgcalls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index cb39db6020..40fc820cc9 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit cb39db6020dee2e33b28d251c00cdab7db1b608b +Subproject commit 40fc820cc9bb433d5c95f1976aed396bcf30a690 From 66fc6f0055fc7f790b8ccc56fe410e295b0d7aa5 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 20:37:48 +0400 Subject: [PATCH 8/9] Sort by raised hands if can speak --- submodules/TelegramCore/Sources/GroupCalls.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index 7e8c864861..11969e3dbd 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -1132,11 +1132,24 @@ public final class GroupCallParticipantsContext { public var state: Signal { let accountPeerId = self.account.peerId + let myPeerId = self.myPeerId return self.statePromise.get() |> map { state -> State in var publicState = state.state var sortAgain = false - let canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId) + var canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId) + for participant in publicState.participants { + if participant.peer.id == myPeerId { + if let muteState = participant.muteState { + if muteState.canUnmute { + canSeeHands = true + } + } else { + canSeeHands = true + } + break + } + } for i in 0 ..< publicState.participants.count { if let pendingMuteState = state.overlayState.pendingMuteStateChanges[publicState.participants[i].peer.id] { publicState.participants[i].muteState = pendingMuteState.state From 0e997621d8b83a4c20a8cff339b4e488da026244 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 6 Apr 2021 20:41:36 +0400 Subject: [PATCH 9/9] Fix pinch dismiss --- submodules/ContextUI/Sources/PinchController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/ContextUI/Sources/PinchController.swift b/submodules/ContextUI/Sources/PinchController.swift index 38be8faf46..9cb033b06b 100644 --- a/submodules/ContextUI/Sources/PinchController.swift +++ b/submodules/ContextUI/Sources/PinchController.swift @@ -316,7 +316,7 @@ private final class PinchControllerNode: ViewControllerTracingNode { self.sourceNode.contentNode.transform = CATransform3DIdentity self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0) - self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x / scale, y: offset.y / scale), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in + self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x, y: offset.y), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in performCompletion() })