From e37edd6319f33207a85fde10909902b9cf42c0e6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 22 Oct 2020 01:01:16 +0400 Subject: [PATCH] [WIP] Pinned message improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 +- submodules/AnimatedNavigationStripeNode/BUCK | 16 + submodules/AnimatedNavigationStripeNode/BUILD | 16 + .../AnimatedNavigationStripeNode.swift | 281 ++++++++++++++++++ .../ChatListFilterTabContainerNode.swift | 6 +- .../ContainedViewLayoutTransition.swift | 6 +- .../Display/Source/Nodes/ASImageNode.swift | 38 ++- submodules/TelegramUI/BUCK | 1 + submodules/TelegramUI/BUILD | 1 + .../TelegramUI/Sources/ChatController.swift | 63 +++- .../ChatMessageAttachedContentNode.swift | 4 +- .../ChatMessageBubbleContentNode.swift | 4 +- .../Sources/ChatMessageBubbleItemNode.swift | 12 +- .../ChatMessageFileBubbleContentNode.swift | 2 +- .../ChatMessageInstantVideoItemNode.swift | 2 +- .../ChatMessageInteractiveFileNode.swift | 14 +- .../ChatPanelInterfaceInteraction.swift | 4 +- .../ChatPinnedMessageTitlePanelNode.swift | 35 ++- .../Sources/ChatRecentActionsController.swift | 2 +- .../Sources/EditAccessoryPanelNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 2 +- .../Sources/ReplyAccessoryPanelNode.swift | 2 +- .../Sources/UndoOverlayControllerNode.swift | 2 +- 23 files changed, 454 insertions(+), 65 deletions(-) create mode 100644 submodules/AnimatedNavigationStripeNode/BUCK create mode 100644 submodules/AnimatedNavigationStripeNode/BUILD create mode 100644 submodules/AnimatedNavigationStripeNode/Sources/AnimatedNavigationStripeNode.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f207870674..f611d242b5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -5874,7 +5874,7 @@ Any member of this group will be able to see messages in the channel."; "Chat.PanelHidePinnedMessages" = "Don't Show Pinned Messages"; "Chat.PanelUnpinAllMessages_1" = "Unpin Message"; -"Chat.PanelUnpinAllMessages_any" = "Unpin All %@ Messages"; +"Chat.PanelUnpinAllMessages_any" = "Unpin All Messages"; "Chat.UnpinAllMessagesConfirmation_1" = "Do you want to unpin 1 message in this chat?"; "Chat.UnpinAllMessagesConfirmation_any" = "Do you want to unpin all %@ messages in this chat?"; @@ -5883,3 +5883,5 @@ Any member of this group will be able to see messages in the channel."; "Chat.PinnedMessagesHiddenTitle" = "Pinned Messages Hidden"; "Chat.PinnedMessagesHiddenText" = "You will see the bar with pinned messages only if a new message is pinned."; + +"OpenFile.PotentiallyDangerousContentAlert" = "Previewing this file can potentially expose your IP address to its sender. Continue?"; diff --git a/submodules/AnimatedNavigationStripeNode/BUCK b/submodules/AnimatedNavigationStripeNode/BUCK new file mode 100644 index 0000000000..1dd8c4bbb4 --- /dev/null +++ b/submodules/AnimatedNavigationStripeNode/BUCK @@ -0,0 +1,16 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "AnimatedNavigationStripeNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/Display:Display#shared", + "//submodules/AsyncDisplayKit:AsyncDisplayKit#shared", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], +) diff --git a/submodules/AnimatedNavigationStripeNode/BUILD b/submodules/AnimatedNavigationStripeNode/BUILD new file mode 100644 index 0000000000..b0de90d445 --- /dev/null +++ b/submodules/AnimatedNavigationStripeNode/BUILD @@ -0,0 +1,16 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AnimatedNavigationStripeNode", + module_name = "AnimatedNavigationStripeNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/Display:Display", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/AnimatedNavigationStripeNode/Sources/AnimatedNavigationStripeNode.swift b/submodules/AnimatedNavigationStripeNode/Sources/AnimatedNavigationStripeNode.swift new file mode 100644 index 0000000000..f802576d83 --- /dev/null +++ b/submodules/AnimatedNavigationStripeNode/Sources/AnimatedNavigationStripeNode.swift @@ -0,0 +1,281 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit + +public final class AnimatedNavigationStripeNode: ASDisplayNode { + public struct Colors: Equatable { + public var foreground: UIColor + public var background: UIColor + public var clearBackground: UIColor + + public init( + foreground: UIColor, + background: UIColor, + clearBackground: UIColor + ) { + self.foreground = foreground + self.background = background + self.clearBackground = clearBackground + } + + public static func ==(lhs: Colors, rhs: Colors) -> Bool { + if !lhs.foreground.isEqual(rhs.foreground) { + return false + } + if !lhs.background.isEqual(rhs.background) { + return false + } + if !lhs.clearBackground.isEqual(rhs.clearBackground) { + return false + } + return true + } + } + + public struct Configuration: Equatable { + public var height: CGFloat + public var index: Int + public var count: Int + + public init(height: CGFloat, index: Int, count: Int) { + self.height = height + self.index = index + self.count = count + } + } + + private final class BackgroundLineNode { + let lineNode: ASImageNode + let overlayNode: ASImageNode + + init() { + self.lineNode = ASImageNode() + self.overlayNode = ASImageNode() + } + } + + private var currentColors: Colors? + private var currentConfiguration: Configuration? + + private let foregroundLineNode: ASImageNode + private var backgroundLineNodes: [Int: BackgroundLineNode] = [:] + private var removingBackgroundLineNodes: [BackgroundLineNode] = [] + + private let topShadowNode: ASImageNode + private let bottomShadowNode: ASImageNode + + private var currentForegroundImage: UIImage? + private var currentBackgroundImage: UIImage? + private var currentClearBackgroundImage: UIImage? + + override public init() { + self.foregroundLineNode = ASImageNode() + self.topShadowNode = ASImageNode() + self.bottomShadowNode = ASImageNode() + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.foregroundLineNode) + self.addSubnode(self.topShadowNode) + self.addSubnode(self.bottomShadowNode) + } + + public func update(colors: Colors, configuration: Configuration, transition: ContainedViewLayoutTransition) { + var transition = transition + + let segmentSpacing: CGFloat = 2.0 + + if self.currentColors != colors { + self.currentColors = colors + self.currentForegroundImage = generateFilledCircleImage(diameter: 2.0, color: colors.foreground)?.resizableImage(withCapInsets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0), resizingMode: .stretch) + self.currentBackgroundImage = generateFilledCircleImage(diameter: 2.0, color: colors.background)?.resizableImage(withCapInsets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0), resizingMode: .stretch) + self.currentClearBackgroundImage = generateImage(CGSize(width: 2.0, height: 4.0 + segmentSpacing * 2.0 + 1.0 * 2.0), contextGenerator: { size, context in + context.setFillColor(colors.clearBackground.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + + let ellipseFudge: CGFloat = 0.02 + + let topEllipse = CGRect(origin: CGPoint(x: -ellipseFudge, y: 1.0 + segmentSpacing), size: CGSize(width: 2.0 + ellipseFudge * 2.0, height: 2.0)) + let bottomEllipse = CGRect(origin: CGPoint(x: -ellipseFudge, y: size.height - (1.0 + segmentSpacing) - 2.0), size: CGSize(width: 2.0 + ellipseFudge * 2.0, height: 2.0)) + + context.fillEllipse(in: topEllipse) + context.fillEllipse(in: bottomEllipse) + + context.fill(CGRect(origin: CGPoint(x: 0.0, y: topEllipse.midY), size: CGSize(width: 2.0, height: bottomEllipse.midY - topEllipse.midY))) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: 2.0, height: 2.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - 1.0), size: CGSize(width: 2.0, height: 2.0))) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 1.0 + segmentSpacing + 2.0, left: 1.0, bottom: 1.0 + segmentSpacing + 2.0, right: 1.0), resizingMode: .stretch) + + self.foregroundLineNode.image = self.currentForegroundImage + for (_, itemNode) in self.backgroundLineNodes { + itemNode.lineNode.image = self.currentBackgroundImage + itemNode.overlayNode.image = self.currentClearBackgroundImage + } + + self.topShadowNode.image = generateImage(CGSize(width: 2.0, height: 7.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] = [colors.clearBackground.cgColor, colors.clearBackground.withAlphaComponent(0.0).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + + self.bottomShadowNode.image = generateImage(CGSize(width: 2.0, height: 7.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] = [colors.clearBackground.cgColor, colors.clearBackground.withAlphaComponent(0.0).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } + + if self.currentConfiguration == nil { + transition = .immediate + } + + if self.currentConfiguration != configuration { + self.currentConfiguration = configuration + + let defaultVerticalInset: CGFloat = 7.0 + let minSegmentHeight: CGFloat = 8.0 + + transition.updateFrame(node: self.topShadowNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: defaultVerticalInset))) + transition.updateFrame(node: self.bottomShadowNode, frame: CGRect(origin: CGPoint(x: 0.0, y: configuration.height - defaultVerticalInset), size: CGSize(width: 2.0, height: defaultVerticalInset))) + + let availableVerticalHeight: CGFloat = configuration.height - defaultVerticalInset * 2.0 + + let proposedSegmentHeight: CGFloat = (availableVerticalHeight - segmentSpacing * CGFloat(configuration.count) + segmentSpacing) / CGFloat(configuration.count) + let segmentHeight = max(proposedSegmentHeight, minSegmentHeight) + + let allItemsHeight = CGFloat(configuration.count) * segmentHeight + max(0.0, CGFloat(configuration.count - 1)) * segmentSpacing + + var verticalInset = defaultVerticalInset + if allItemsHeight > availableVerticalHeight && allItemsHeight - 2.0 <= availableVerticalHeight { + verticalInset -= 2.0 + } + + let topItemsHeight = CGFloat(configuration.index) * (segmentHeight + segmentSpacing) + let bottomItemsHeight = allItemsHeight - topItemsHeight - segmentHeight + + var itemScreenOffset = floorToScreenPixels((configuration.height - segmentHeight) / 2.0) + + if itemScreenOffset - topItemsHeight > verticalInset { + itemScreenOffset = topItemsHeight + verticalInset + } + if itemScreenOffset + segmentHeight + bottomItemsHeight < configuration.height - verticalInset { + itemScreenOffset = configuration.height - verticalInset - (segmentHeight + bottomItemsHeight) + } + + var backgroundItemNodesToOffset: [BackgroundLineNode] = [] + var resolvedOffset: CGFloat = 0.0 + + func updateBackgroundLine(index: Int) -> Bool { + let indexDifference = index - configuration.index + let offsetDistance = CGFloat(indexDifference) * (segmentHeight + segmentSpacing) + + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemScreenOffset + offsetDistance), size: CGSize(width: 2.0, height: segmentHeight)) + + if itemFrame.maxY <= 0.0 || itemFrame.minY > configuration.height { + return false + } + + var itemNodeTransition = transition + let itemNode: BackgroundLineNode + if let current = self.backgroundLineNodes[index] { + itemNode = current + let offset = itemFrame.minY - itemNode.lineNode.frame.minY + if abs(offset) > abs(resolvedOffset) { + resolvedOffset = offset + } + } else { + itemNodeTransition = .immediate + itemNode = BackgroundLineNode() + itemNode.lineNode.image = self.currentBackgroundImage + itemNode.overlayNode.image = self.currentClearBackgroundImage + self.backgroundLineNodes[index] = itemNode + self.insertSubnode(itemNode.lineNode, belowSubnode: self.foregroundLineNode) + self.insertSubnode(itemNode.overlayNode, belowSubnode: self.topShadowNode) + backgroundItemNodesToOffset.append(itemNode) + } + itemNodeTransition.updateFrame(node: itemNode.lineNode, frame: itemFrame, beginWithCurrentState: true) + itemNodeTransition.updateFrame(node: itemNode.overlayNode, frame: itemFrame.insetBy(dx: 0.0, dy: -(1.0 + segmentSpacing)), beginWithCurrentState: true) + + return true + } + + var validIndices = Set() + if configuration.index >= 0 { + for i in (0 ... configuration.index).reversed() { + if updateBackgroundLine(index: i) { + validIndices.insert(i) + } else { + break + } + } + } + if configuration.index < configuration.count { + for i in configuration.index + 1 ..< configuration.count { + if updateBackgroundLine(index: i) { + validIndices.insert(i) + } else { + break + } + } + } + + if !resolvedOffset.isZero { + for itemNode in backgroundItemNodesToOffset { + transition.animatePositionAdditive(node: itemNode.lineNode, offset: CGPoint(x: 0.0, y: -resolvedOffset)) + transition.animatePositionAdditive(node: itemNode.overlayNode, offset: CGPoint(x: 0.0, y: -resolvedOffset)) + } + for itemNode in self.removingBackgroundLineNodes { + transition.animatePosition(node: itemNode.lineNode, to: CGPoint(x: 0.0, y: resolvedOffset), removeOnCompletion: false, additive: true) + transition.animatePosition(node: itemNode.overlayNode, to: CGPoint(x: 0.0, y: resolvedOffset), removeOnCompletion: false, additive: true) + } + } + + var removeIndices: [Int] = [] + for (index, itemNode) in self.backgroundLineNodes { + if !validIndices.contains(index) { + removeIndices.append(index) + + if transition.isAnimated { + removingBackgroundLineNodes.append(itemNode) + transition.animatePosition(node: itemNode.overlayNode, to: CGPoint(x: 0.0, y: resolvedOffset), removeOnCompletion: false, additive: true) + transition.animatePosition(node: itemNode.lineNode, to: CGPoint(x: 0.0, y: resolvedOffset), removeOnCompletion: false, additive: true, completion: { [weak self, weak itemNode] _ in + guard let strongSelf = self, let itemNode = itemNode else { + return + } + strongSelf.removingBackgroundLineNodes.removeAll(where: { $0 === itemNode }) + itemNode.lineNode.removeFromSupernode() + itemNode.overlayNode.removeFromSupernode() + }) + } else { + itemNode.lineNode.removeFromSupernode() + itemNode.overlayNode.removeFromSupernode() + } + } + } + for index in removeIndices { + self.backgroundLineNodes.removeValue(forKey: index) + } + + transition.updateFrame(node: self.foregroundLineNode, frame: CGRect(origin: CGPoint(x: 0.0, y: itemScreenOffset), size: CGSize(width: 2.0, height: segmentHeight)), beginWithCurrentState: true) + } + } +} diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 0606a777f8..2f8c6cb13d 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -628,9 +628,11 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { self.selectedLineNode.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 1.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 3.0, height: 3.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 3.0, y: 0.0), size: CGSize(width: 3.0, height: 3.0))) + context.fill(CGRect(x: 1.5, y: 0.0, width: size.width - 3.0, height: 3.0)) context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0)) - })?.stretchableImage(withLeftCapWidth: 2, topCapHeight: 2) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 3.0, left: 2.5, bottom: 0.0, right: 2.5), resizingMode: .stretch) } if isReordering { diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 053f6ad60b..b9801c5320 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -276,8 +276,8 @@ public extension ContainedViewLayoutTransition { } } - func animatePosition(node: ASDisplayNode, to position: CGPoint, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { - if node.position.equalTo(position) { + func animatePosition(node: ASDisplayNode, to position: CGPoint, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + if !additive && node.position.equalTo(position) { completion?(true) } else { switch self { @@ -286,7 +286,7 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - node.layer.animatePosition(from: node.position, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { result in + node.layer.animatePosition(from: additive ? CGPoint() : node.position, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in if let completion = completion { completion(result) } diff --git a/submodules/Display/Source/Nodes/ASImageNode.swift b/submodules/Display/Source/Nodes/ASImageNode.swift index 8baa7359e2..2ebf0e9987 100644 --- a/submodules/Display/Source/Nodes/ASImageNode.swift +++ b/submodules/Display/Source/Nodes/ASImageNode.swift @@ -5,19 +5,21 @@ import AsyncDisplayKit open class ASImageNode: ASDisplayNode { public var image: UIImage? { didSet { - if let image = self.image { - let capInsets = image.capInsets - if capInsets.left.isZero && capInsets.top.isZero { - self.contentsScale = image.scale - self.contents = image.cgImage + if self.isNodeLoaded { + if let image = self.image { + let capInsets = image.capInsets + if capInsets.left.isZero && capInsets.top.isZero { + self.contentsScale = image.scale + self.contents = image.cgImage + } else { + ASDisplayNodeSetResizableContents(self.layer, image) + } } else { - ASDisplayNodeSetResizableContents(self, image) + self.contents = nil + } + if self.image?.size != oldValue?.size { + self.invalidateCalculatedLayout() } - } else { - self.contents = nil - } - if self.image?.size != oldValue?.size { - self.invalidateCalculatedLayout() } } } @@ -28,6 +30,20 @@ open class ASImageNode: ASDisplayNode { super.init() } + override open func didLoad() { + super.didLoad() + + if let image = self.image { + let capInsets = image.capInsets + if capInsets.left.isZero && capInsets.top.isZero { + self.contentsScale = image.scale + self.contents = image.cgImage + } else { + ASDisplayNodeSetResizableContents(self.layer, image) + } + } + } + override public func calculateSizeThatFits(_ contrainedSize: CGSize) -> CGSize { return self.image?.size ?? CGSize() } diff --git a/submodules/TelegramUI/BUCK b/submodules/TelegramUI/BUCK index 18655fa29a..8831168ba1 100644 --- a/submodules/TelegramUI/BUCK +++ b/submodules/TelegramUI/BUCK @@ -212,6 +212,7 @@ framework( "//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode", "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode", + "//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 264c75d6be..0cbe694529 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -209,6 +209,7 @@ swift_library( "//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode", "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode", + "//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5dc393360d..dbd6cbba32 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4206,8 +4206,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { if case .known = strongSelf.chatDisplayNode.historyNode.visibleContentOffset() { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - } else if case let .peer(peerId) = strongSelf.chatLocation { - strongSelf.navigateToMessage(messageLocation: .upperBound(peerId), animated: true) + } else if case .peer = strongSelf.chatLocation { + strongSelf.scrollToEndOfHistory() } else if case .replyThread = strongSelf.chatLocation { strongSelf.scrollToEndOfHistory() } else { @@ -4751,8 +4751,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) strongSelf.updateItemNodesSearchTextHighlightStates() } - }, navigateToMessage: { [weak self] messageId, dropStack in - self?.navigateToMessage(from: nil, to: .id(messageId), dropStack: dropStack) + }, navigateToMessage: { [weak self] messageId, dropStack, forceInCurrentChat in + self?.navigateToMessage(from: nil, to: .id(messageId), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack) }, navigateToChat: { [weak self] peerId in guard let strongSelf = self else { return @@ -5294,16 +5294,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if canManagePin { let count = strongSelf.presentationInterfaceState.pinnedMessage?.totalCount ?? 1 - let _ = (requestUnpinAllMessages(account: strongSelf.context.account, peerId: strongSelf.chatLocation.peerId) - |> deliverOnMainQueue).start(error: { _ in - - }, completed: { - guard let strongSelf = self else { - return - } - strongSelf.dismiss() - strongSelf.updatedUnpinnedAllMessages?(count) - }) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_UnpinAllMessagesConfirmation(Int32(count)), actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: { + guard let strongSelf = self else { + return + } + let _ = (requestUnpinAllMessages(account: strongSelf.context.account, peerId: strongSelf.chatLocation.peerId) + |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + guard let strongSelf = self else { + return + } + strongSelf.dismiss() + strongSelf.updatedUnpinnedAllMessages?(count) + }) + }) + ], parseMarkdown: true), in: .window(.root)) } else { let topPinnedMessage: Signal = strongSelf.topPinnedMessageSignal(latest: true) |> take(1) @@ -9337,7 +9346,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(nil) }) }) } - if case .peer(peerId) = strongSelf.chatLocation, strongSelf.parentController == nil { + var isPinnedMessages = false + if case .pinnedMessages = strongSelf.presentationInterfaceState.subject { + isPinnedMessages = true + } + + if case .peer(peerId) = strongSelf.chatLocation, strongSelf.parentController == nil, !isPinnedMessages { strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messages.map { $0.id }).withoutSelectionState() }) }) strongController.dismiss() } else if peerId == strongSelf.context.account.peerId { @@ -9375,6 +9389,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) strongController.dismiss() } else { + if let navigationController = strongSelf.navigationController as? NavigationController { + for controller in navigationController.viewControllers { + if let maybeChat = controller as? ChatControllerImpl { + if case .peer(peerId) = maybeChat.chatLocation { + var isChatPinnedMessages = false + if case .pinnedMessages = maybeChat.presentationInterfaceState.subject { + isChatPinnedMessages = true + } + if !isChatPinnedMessages { + maybeChat.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messages.map { $0.id }).withoutSelectionState() }) }) + strongSelf.dismiss() + strongController.dismiss() + return + } + } + } + } + } + let _ = (strongSelf.context.account.postbox.transaction({ transaction -> Void in transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 813b15acae..70098a9b63 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -424,7 +424,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { refineContentImageLayout = refineLayout } else if file.isInstantVideo { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, CGSize(width: 212.0, height: 212.0), .bubble, automaticDownload) + let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned)), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, CGSize(width: 212.0, height: 212.0), .bubble, automaticDownload) initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight contentInstantVideoSizeAndApply = (videoLayout, apply) } else if file.isVideo { @@ -474,7 +474,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let (_, refineLayout) = contentFileLayout(context, presentationData, message, associatedData, chatLocation, attributes, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)) + let (_, refineLayout) = contentFileLayout(context, presentationData, message, associatedData, chatLocation, attributes, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)) refineContentFileLayout = refineLayout } } else if let image = media as? TelegramMediaImage { diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift index 15f0570286..6b34b6a484 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift @@ -108,8 +108,9 @@ final class ChatMessageBubbleContentItem { let presentationData: ChatPresentationData let associatedData: ChatMessageItemAssociatedData let attributes: ChatMessageEntryAttributes + let isItemPinned: Bool - init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes) { + init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool) { self.context = context self.controllerInteraction = controllerInteraction self.message = message @@ -118,6 +119,7 @@ final class ChatMessageBubbleContentItem { self.presentationData = presentationData self.associatedData = associatedData self.attributes = attributes + self.isItemPinned = isItemPinned } } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 464932e748..3bc43cbaf5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -1208,11 +1208,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let contentNodeCount = contentPropertiesAndPrepareLayouts.count let read: Bool + var isItemPinned = false + switch item.content { - case let .message(_, value, _, _): + case let .message(message, value, _, _): read = value + isItemPinned = message.tags.contains(.pinned) case let .group(messages): read = messages[0].1 + for message in messages { + if message.0.tags.contains(.pinned) { + isItemPinned = true + } + } } var mosaicStartIndex: Int? @@ -1267,7 +1275,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode prepareContentPosition = .linear(top: topPosition, bottom: refinedBottomPosition) } - let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes) + let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes, isItemPinned: isItemPinned) var itemSelection: Bool? switch content { diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 544f6a9dbf..d34e1745f7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -89,7 +89,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) - let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.associatedData, item.chatLocation, item.attributes, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, item.message.groupingKey != nil ? selection : nil, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.associatedData, item.chatLocation, item.attributes, item.isItemPinned, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, item.message.groupingKey != nil ? selection : nil, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 8eb72804df..7c05cda0cc 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -276,7 +276,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } - let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, .free, automaticDownload) + let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned)), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, .free, automaticDownload) let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 787b8a51f0..2d37212ee6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -208,7 +208,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) { + func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) @@ -218,7 +218,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let currentMessage = self.message - return { context, presentationData, message, associatedData, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in + return { context, presentationData, message, associatedData, chatLocation, attributes, isPinned, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0)) let descriptionFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) @@ -327,7 +327,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, reactionCount: dateReactionCount) - let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode) + let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions, dateReplies, isPinned && !associatedData.isInPinnedListMode) statusSize = size statusApply = apply } @@ -1032,12 +1032,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize) } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, presentationData, message, associatedData, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in + return { context, presentationData, message, associatedData, chatLocation, attributes, isPinned, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) + var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -1047,7 +1047,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, associatedData, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, associatedData, chatLocation, attributes, isPinned, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift index 6c6b56957a..ceb913e1ef 100644 --- a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift @@ -73,7 +73,7 @@ final class ChatPanelInterfaceInteraction { let openSearchResults: () -> Void let openCalendarSearch: () -> Void let toggleMembersSearch: (Bool) -> Void - let navigateToMessage: (MessageId, Bool) -> Void + let navigateToMessage: (MessageId, Bool, Bool) -> Void let navigateToChat: (PeerId) -> Void let navigateToProfile: (PeerId) -> Void let openPeerInfo: () -> Void @@ -151,7 +151,7 @@ final class ChatPanelInterfaceInteraction { navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, - navigateToMessage: @escaping (MessageId, Bool) -> Void, + navigateToMessage: @escaping (MessageId, Bool, Bool) -> Void, navigateToChat: @escaping (PeerId) -> Void, navigateToProfile: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 1befb2cce1..46e6e2207b 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -13,6 +13,7 @@ import StickerResources import PhotoResources import TelegramStringFormatting import AnimatedCountLabelNode +import AnimatedNavigationStripeNode private enum PinnedMessageAnimation { case slideToTop @@ -28,7 +29,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let clippingContainer: ASDisplayNode private let contentContainer: ASDisplayNode private let contentTextContainer: ASDisplayNode - private let lineNode: ASImageNode + private let lineNode: AnimatedNavigationStripeNode private let titleNode: AnimatedCountLabelNode private let textNode: TextNode @@ -69,9 +70,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.contentContainer = ASDisplayNode() self.contentTextContainer = ASDisplayNode() - self.lineNode = ASImageNode() - self.lineNode.displayWithoutProcessing = true - self.lineNode.displaysAsynchronously = false + self.lineNode = AnimatedNavigationStripeNode() self.titleNode = AnimatedCountLabelNode() self.titleNode.isUserInteractionEnabled = false @@ -145,7 +144,6 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.theme = interfaceState.theme self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(interfaceState.theme), for: []) self.listButton.setImage(PresentationResourcesChat.chatInputPanelPinnedListIconImage(interfaceState.theme), for: []) - self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(interfaceState.theme) self.backgroundColor = interfaceState.theme.chat.historyNavigation.fillColor self.separatorNode.backgroundColor = interfaceState.theme.chat.historyNavigation.strokeColor } @@ -180,7 +178,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.currentMessage = interfaceState.pinnedMessage if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout { - self.enqueueTransition(width: currentLayout.0, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread) + self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread) } } @@ -200,11 +198,8 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.closeButton.isHidden = true } - let contentLeftInset: CGFloat = 10.0 + leftInset let rightInset: CGFloat = 18.0 + rightInset - transition.updateFrame(node: self.lineNode, frame: CGRect(origin: CGPoint(x: contentLeftInset, y: 7.0), size: CGSize(width: 2.0, height: panelHeight - 14.0))) - let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize)) @@ -221,14 +216,14 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.currentLayout = (width, leftInset, rightInset) if let currentMessage = self.currentMessage { - self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread) + self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread) } } return panelHeight } - private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) { + private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) { let message = pinnedMessage.message var animationTransition: ContainedViewLayoutTransition = .immediate @@ -390,6 +385,22 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) + let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight)) + animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame) + strongSelf.lineNode.update( + colors: AnimatedNavigationStripeNode.Colors( + foreground: theme.chat.inputPanel.panelControlAccentColor, + background: theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.5), + clearBackground: theme.chat.inputPanel.panelBackgroundColor + ), + configuration: AnimatedNavigationStripeNode.Configuration( + height: panelHeight, + index: pinnedMessage.index, + count: pinnedMessage.totalCount + ), + transition: animationTransition + ) + strongSelf.imageNodeContainer.frame = CGRect(origin: CGPoint(x: contentLeftInset + 9.0, y: 7.0), size: CGSize(width: 35.0, height: 35.0)) strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 35.0, height: 35.0)) @@ -419,7 +430,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if self.isReplyThread { interfaceInteraction.scrollToTop() } else { - interfaceInteraction.navigateToMessage(message.message.id, false) + interfaceInteraction.navigateToMessage(message.message.id, false, true) } } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index cc49d39a28..e63d6b023d 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -76,7 +76,7 @@ final class ChatRecentActionsController: TelegramBaseController { }, navigateMessageSearch: { _ in }, openCalendarSearch: { }, toggleMembersSearch: { _ in - }, navigateToMessage: { _, _ in + }, navigateToMessage: { _, _, _ in }, navigateToChat: { _ in }, navigateToProfile: { _ in }, openPeerInfo: { diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index 87c6738995..197c2d71c9 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -347,7 +347,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { @objc func contentTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state, let message = self.currentMessage { - self.interfaceInteraction?.navigateToMessage(message.id, false) + self.interfaceInteraction?.navigateToMessage(message.id, false, true) } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index ae68b1b7a3..66207c5aa1 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -382,7 +382,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, navigateMessageSearch: { _ in }, openCalendarSearch: { }, toggleMembersSearch: { _ in - }, navigateToMessage: { _, _ in + }, navigateToMessage: { _, _, _ in }, navigateToChat: { _ in }, navigateToProfile: { _ in }, openPeerInfo: { diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 14d0be64c1..499f13a547 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -256,7 +256,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.interfaceInteraction?.navigateToMessage(self.messageId, false) + self.interfaceInteraction?.navigateToMessage(self.messageId, false, true) } } } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index c35acf1101..a460dd456d 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -218,7 +218,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } displayUndo = undo - self.originalRemainingSeconds = undo ? 5 : 3 + self.originalRemainingSeconds = undo ? 5 : 5 case let .emoji(path, text): self.iconNode = nil self.iconCheckNode = nil