From 78e12ad45e21200ec9353f434f6fb06288b89173 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 7 Apr 2020 17:55:39 +0400 Subject: [PATCH] Sticker pack preview updates --- Telegram/BUILD | 6 +- .../Sources/ChatListController.swift | 5 +- submodules/Display/Source/GridNode.swift | 41 ++- .../SettingsUI/Sources/DebugController.swift | 2 + .../StickerPackPreviewControllerNode.swift | 12 +- .../Sources/StickerPackPreviewGridItem.swift | 126 +++++---- .../Sources/StickerPackScreen.swift | 240 +++++++++++++++--- .../Sources/StickerResources.swift | 8 + submodules/TelegramUI/BUCK | 1 + submodules/TelegramUI/BUILD | 1 + .../TelegramUI/Sources/ChatController.swift | 18 +- .../Sources/ChatControllerInteraction.swift | 6 +- .../Sources/ChatMessageBubbleItemNode.swift | 2 +- .../ChatMessagePollBubbleContentNode.swift | 10 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- .../Sources/OverlayPlayerControllerNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 2 +- .../PeerMediaCollectionController.swift | 2 +- .../Sources/PollResultsController.swift | 4 +- .../Sources/SharedAccountContext.swift | 2 +- .../TooltipUI/Sources/TooltipScreen.swift | 94 +++++-- 22 files changed, 459 insertions(+), 129 deletions(-) diff --git a/Telegram/BUILD b/Telegram/BUILD index c1bf60ae83..f72f1025f2 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -864,10 +864,10 @@ ios_application( ":AppStringResources", ], extensions = [ - ":ShareExtension", - ":NotificationServiceExtension", + #":ShareExtension", + #":NotificationServiceExtension", ], - watch_application = ":TelegramWatchApp", + #watch_application = ":TelegramWatchApp", deps = [ ":Main", ":Lib", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 53d94cc6cd..9c963b8f08 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1197,7 +1197,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } else { text = strongSelf.presentationData.strings.ChatList_TabIconFoldersTooltipEmptyFolders } - parentController.present(TooltipScreen(text: text, location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0), shouldDismissOnTouch: { point in + + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0), size: CGSize()) + + parentController.present(TooltipScreen(text: text, icon: .chatListPress, location: location, shouldDismissOnTouch: { point in guard let strongSelf = self, let parentController = strongSelf.parent as? TabBarController else { return true } diff --git a/submodules/Display/Source/GridNode.swift b/submodules/Display/Source/GridNode.swift index 916d9d194b..6b52651874 100644 --- a/submodules/Display/Source/GridNode.swift +++ b/submodules/Display/Source/GridNode.swift @@ -222,9 +222,11 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { public var scrollingInitiated: (() -> Void)? public var scrollingCompleted: (() -> Void)? public var interactiveScrollingEnded: (() -> Void)? - public var interactiveScrollingWillBeEnded: ((CGPoint, CGPoint) -> Void)? + public var interactiveScrollingWillBeEnded: ((CGPoint, CGPoint, CGPoint) -> CGPoint)? public var visibleContentOffsetChanged: (GridNodeVisibleContentOffset) -> Void = { _ in } + private var autoscrollingAnimator: DisplayLinkAnimator? + public final var floatingSections = false public final var initialOffset: CGFloat = 0.0 @@ -256,6 +258,10 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { fatalError("init(coder:) has not been implemented") } + deinit { + self.autoscrollingAnimator?.invalidate() + } + public func transaction(_ transaction: GridNodeTransaction, completion: (GridNodeDisplayedItemRange) -> Void) { if let updateOpaqueState = transaction.updateOpaqueState { self.opaqueState = updateOpaqueState @@ -365,14 +371,45 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { self.applyPresentationLayoutTransition(self.generatePresentationLayoutTransition(stationaryItems: transaction.stationaryItems, layoutTransactionOffset: layoutTransactionOffset, scrollToItem: generatedScrollToItem), removedNodes: removedNodes, updateLayoutTransition: updateLayoutTransition, customScrollToItem: transaction.scrollToItem != nil, itemTransition: transaction.itemTransition, synchronousLoads: transaction.synchronousLoads, updatingLayout: transaction.updateLayout != nil, completion: completion) } + public func autoscroll(toOffset: CGPoint, duration: Double) { + if let autoscrollingAnimator = self.autoscrollingAnimator { + self.autoscrollingAnimator = nil + autoscrollingAnimator.invalidate() + } + + let fromOffset: CGPoint = self.scrollView.contentOffset + + self.autoscrollingAnimator = DisplayLinkAnimator(duration: duration, from: 0.0, to: 1.0, update: { [weak self] t in + guard let strongSelf = self else { + return + } + let mappedT = bezierPoint(0.23, 1.0, 0.32, 1.0, t) + let offset = CGPoint(x: 0.0, y: (1.0 - mappedT) * fromOffset.y + mappedT * toOffset.y) + strongSelf.scrollView.setContentOffset(offset, animated: false) + }, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.autoscrollingAnimator?.invalidate() + strongSelf.autoscrollingAnimator = nil + }) + } + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if let autoscrollingAnimator = self.autoscrollingAnimator { + self.autoscrollingAnimator = nil + autoscrollingAnimator.invalidate() + } + self.updateItemNodeVisibilititesAndScrolling() self.updateVisibleContentOffset() self.scrollingInitiated?() } public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - self.interactiveScrollingWillBeEnded?(velocity, targetContentOffset.pointee) + if let interactiveScrollingWillBeEnded = self.interactiveScrollingWillBeEnded { + targetContentOffset.pointee = interactiveScrollingWillBeEnded(scrollView.contentOffset, velocity, targetContentOffset.pointee) + } } public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { diff --git a/submodules/SettingsUI/Sources/DebugController.swift b/submodules/SettingsUI/Sources/DebugController.swift index 3033fa358a..0a52cf7055 100644 --- a/submodules/SettingsUI/Sources/DebugController.swift +++ b/submodules/SettingsUI/Sources/DebugController.swift @@ -395,6 +395,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { let _ = (context.account.postbox.transaction { transaction -> Void in transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedPollResults) unmarkChatListFeaturedFiltersAsSeen(transaction: transaction) + + transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks) }).start() } }) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index 2230f24959..823b28822b 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -25,8 +25,8 @@ private struct StickerPackPreviewGridEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem { - return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction) + func item(account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem { + return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isEmpty: false) } } @@ -35,12 +35,12 @@ private struct StickerPackPreviewGridTransaction { let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] - init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) { + init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices - self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) } - self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) } + self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction, theme: theme), previousIndex: $0.2) } + self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) } } } @@ -385,7 +385,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.contentTitleNode.attributedText = stringWithAppliedEntities(info.title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font) animateIn = true } - transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction) + transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme) self.currentItems = updatedItems } } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index ba51dfe0f4..c7f4db7fbc 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -10,6 +10,7 @@ import StickerResources import AccountContext import AnimatedStickerNode import TelegramAnimatedStickerNode +import TelegramPresentationData final class StickerPackPreviewInteraction { var previewedItem: StickerPreviewPeekItem? @@ -22,20 +23,24 @@ final class StickerPackPreviewInteraction { final class StickerPackPreviewGridItem: GridItem { let account: Account - let stickerItem: StickerPackItem + let stickerItem: StickerPackItem? let interaction: StickerPackPreviewInteraction + let theme: PresentationTheme + let isEmpty: Bool let section: GridSection? = nil - init(account: Account, stickerItem: StickerPackItem, interaction: StickerPackPreviewInteraction) { + init(account: Account, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) { self.account = account self.stickerItem = stickerItem self.interaction = interaction + self.theme = theme + self.isEmpty = isEmpty } func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = StickerPackPreviewGridItemNode() - node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction) + node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isEmpty: self.isEmpty) return node } @@ -44,16 +49,18 @@ final class StickerPackPreviewGridItem: GridItem { assertionFailure() return } - node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction) + node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isEmpty: self.isEmpty) } } private let textFont = Font.regular(20.0) final class StickerPackPreviewGridItemNode: GridItemNode { - private var currentState: (Account, StickerPackItem, CGSize)? + private var currentState: (Account, StickerPackItem?)? + private var isEmpty: Bool? private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? + private let placeholderNode: ASDisplayNode override var isVisibleInGrid: Bool { didSet { @@ -76,10 +83,26 @@ final class StickerPackPreviewGridItemNode: GridItemNode { override init() { self.imageNode = TransformImageNode() self.imageNode.isLayerBacked = !smartInvertColorsEnabled() + self.placeholderNode = ASDisplayNode() super.init() self.addSubnode(self.imageNode) + self.addSubnode(self.placeholderNode) + + var firstTime = true + self.imageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil, !strongSelf.placeholderNode.alpha.isZero { + strongSelf.placeholderNode.alpha = 0.0 + if !firstTime { + strongSelf.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + firstTime = false + } } deinit { @@ -92,41 +115,55 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) } - func setup(account: Account, stickerItem: StickerPackItem, interaction: StickerPackPreviewInteraction) { + func setup(account: Account, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) { self.interaction = interaction - if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem { - if let dimensions = stickerItem.file.dimensions { - if stickerItem.file.isAnimatedSticker { - let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))) - - if self.animationNode == nil { - let animationNode = AnimatedStickerNode() - self.animationNode = animationNode - self.addSubnode(animationNode) - animationNode.started = { [weak self] in - self?.imageNode.isHidden = true + self.placeholderNode.backgroundColor = theme.list.mediaPlaceholderColor + + if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem || self.isEmpty != isEmpty { + if let stickerItem = stickerItem { + if let _ = stickerItem.file.dimensions { + if stickerItem.file.isAnimatedSticker { + let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))) + + if self.animationNode == nil { + let animationNode = AnimatedStickerNode() + self.animationNode = animationNode + self.addSubnode(animationNode) + animationNode.started = { [weak self] in + self?.imageNode.isHidden = true + self?.placeholderNode.alpha = 0.0 + } } + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) + self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) + } else { + if let animationNode = self.animationNode { + animationNode.visibility = false + self.animationNode = nil + animationNode.removeFromSupernode() + } + self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) } - let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) - self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) - self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) - } else { - if let animationNode = self.animationNode { - animationNode.visibility = false - self.animationNode = nil - animationNode.removeFromSupernode() - } - self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) } - - self.currentState = (account, stickerItem, dimensions.cgSize) - self.setNeedsLayout() + } else { + if isEmpty { + if !self.placeholderNode.alpha.isZero { + self.placeholderNode.alpha = 0.0 + self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } else { + self.placeholderNode.alpha = 1.0 + } } + self.currentState = (account, stickerItem) + self.setNeedsLayout() } + self.isEmpty = isEmpty //self.updateSelectionState(animated: false) //self.updateHiddenMedia() @@ -139,13 +176,17 @@ final class StickerPackPreviewGridItemNode: GridItemNode { let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0) let boundingSize = CGSize(width: boundsSide, height: boundsSide) - if let (_, _, mediaDimensions) = self.currentState { - let imageSize = mediaDimensions.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) - if let animationNode = self.animationNode { - animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) - animationNode.updateLayout(size: imageSize) + self.placeholderNode.frame = CGRect(origin: CGPoint(x: floor((bounds.width - boundingSize.width) / 2.0), y: floor((bounds.height - boundingSize.height) / 2.0)), size: boundingSize) + + if let (_, item) = self.currentState { + if let item = item, let dimensions = item.file.dimensions?.cgSize { + let imageSize = dimensions.aspectFitted(boundingSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + if let animationNode = self.animationNode { + animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.updateLayout(size: imageSize) + } } } } @@ -155,14 +196,11 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { - if let interaction = self.interaction, let (_, item, _) = self.currentState, case .ended = recognizer.state { - //interaction.sendSticker(item) - } } func updatePreviewing(animated: Bool) { var isPreviewing = false - if let (_, item, _) = self.currentState, let interaction = self.interaction { + if let (_, maybeItem) = self.currentState, let interaction = self.interaction, let item = maybeItem { isPreviewing = interaction.previewedItem == .pack(item) } if self.currentIsPreviewing != isPreviewing { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 38e9a5ce70..01c2e277aa 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -13,18 +13,16 @@ import MergeLists private struct StickerPackPreviewGridEntry: Comparable, Identifiable { let index: Int - let stickerItem: StickerPackItem - - var stableId: MediaId { - return self.stickerItem.file.fileId - } + let stableId: Int + let stickerItem: StickerPackItem? + let isEmpty: Bool static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem { - return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction) + func item(account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem { + return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isEmpty: self.isEmpty) } } @@ -32,13 +30,16 @@ private struct StickerPackPreviewGridTransaction { let deletions: [Int] let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] + let scrollToItem: GridNodeScrollToItem? - init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) { + init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, scrollToItem: GridNodeScrollToItem?) { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices - self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) } - self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) } + self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction, theme: theme), previousIndex: $0.2) } + self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) } + + self.scrollToItem = scrollToItem } } @@ -67,11 +68,13 @@ private final class StickerPackContainer: ASDisplayNode { private let actionAreaSeparatorNode: ASDisplayNode private let buttonNode: HighlightableButtonNode private let titleNode: ImmediateTextNode + private let titlePlaceholderNode: ASDisplayNode private let titleContainer: ASDisplayNode private let titleSeparatorNode: ASDisplayNode private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)? + private var nextStableId: Int = 1 private var currentEntries: [StickerPackPreviewGridEntry] = [] private var enqueuedTransactions: [StickerPackPreviewGridTransaction] = [] @@ -85,6 +88,7 @@ private final class StickerPackContainer: ASDisplayNode { } var expandProgress: CGFloat = 0.0 + var expandScrollProgress: CGFloat = 0.0 var modalProgress: CGFloat = 0.0 let expandProgressUpdated: (StickerPackContainer, ContainedViewLayoutTransition) -> Void @@ -120,6 +124,9 @@ private final class StickerPackContainer: ASDisplayNode { self.buttonNode = HighlightableButtonNode() self.titleNode = ImmediateTextNode() + self.titlePlaceholderNode = ASDisplayNode() + self.titlePlaceholderNode.alpha = 0.0 + self.titlePlaceholderNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor self.titleContainer = ASDisplayNode() self.titleSeparatorNode = ASDisplayNode() self.titleSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor @@ -135,6 +142,7 @@ private final class StickerPackContainer: ASDisplayNode { self.addSubnode(self.buttonNode) self.titleContainer.addSubnode(self.titleNode) + self.titleContainer.addSubnode(self.titlePlaceholderNode) self.addSubnode(self.titleContainer) self.addSubnode(self.titleSeparatorNode) @@ -157,32 +165,75 @@ private final class StickerPackContainer: ASDisplayNode { } } - self.gridNode.interactiveScrollingWillBeEnded = { [weak self] velocity, targetOffset in + self.gridNode.interactiveScrollingWillBeEnded = { [weak self] contentOffset, velocity, targetOffset -> CGPoint in guard let strongSelf = self, !strongSelf.isDismissed else { - return + return targetOffset } - DispatchQueue.main.async { - let contentOffset = targetOffset - let insets = strongSelf.gridNode.scrollView.contentInset - var modalProgress: CGFloat = 0.0 - - if contentOffset.y < 0.0 && contentOffset.y >= -insets.top { - strongSelf.gridNode.scrollView.stopScrollingAnimation() - if contentOffset.y > -insets.top / 2.0 || velocity.y <= -100.0 { - strongSelf.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true) - modalProgress = 1.0 - } else { - strongSelf.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -insets.top), animated: true) - } - } else if contentOffset.y >= 0.0 { + + let insets = strongSelf.gridNode.scrollView.contentInset + var modalProgress: CGFloat = 0.0 + + var updatedOffset = targetOffset + var resetOffset = false + + if targetOffset.y < 0.0 && targetOffset.y >= -insets.top { + if contentOffset.y > 0.0 { + updatedOffset = CGPoint(x: 0.0, y: 0.0) modalProgress = 1.0 + } else { + if targetOffset.y > -insets.top / 2.0 || velocity.y <= -100.0 { + modalProgress = 1.0 + resetOffset = true + } else { + modalProgress = 0.0 + if contentOffset.y > -insets.top { + resetOffset = true + } + } + } + } else if targetOffset.y >= 0.0 { + modalProgress = 1.0 + } + + if abs(strongSelf.modalProgress - modalProgress) > CGFloat.ulpOfOne { + if contentOffset.y > 0.0 && targetOffset.y > 0.0 { + } else { + resetOffset = true + } + strongSelf.modalProgress = modalProgress + strongSelf.expandProgressUpdated(strongSelf, .animated(duration: 0.4, curve: .spring)) + } + + if resetOffset { + let offset: CGPoint + let isVelocityAligned: Bool + if modalProgress.isZero { + offset = CGPoint(x: 0.0, y: -insets.top) + isVelocityAligned = velocity.y < 0.0 + } else { + offset = CGPoint(x: 0.0, y: 0.0) + isVelocityAligned = velocity.y > 0.0 } - if abs(strongSelf.modalProgress - modalProgress) > CGFloat.ulpOfOne { - strongSelf.modalProgress = modalProgress - strongSelf.expandProgressUpdated(strongSelf, .animated(duration: 0.4, curve: .spring)) + DispatchQueue.main.async { + let duration: Double + if isVelocityAligned { + let minVelocity: CGFloat = 400.0 + let maxVelocity: CGFloat = 1000.0 + let clippedVelocity = max(minVelocity, min(maxVelocity, abs(velocity.y * 500.0))) + + let distance = abs(offset.y - contentOffset.y) + duration = Double(distance / clippedVelocity) + } else { + duration = 0.5 + } + + strongSelf.gridNode.autoscroll(toOffset: offset, duration: duration) } + updatedOffset = contentOffset } + + return updatedOffset } self.itemsDisposable = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: stickerPack, forceActualized: false) @@ -276,6 +327,7 @@ private final class StickerPackContainer: ASDisplayNode { @objc func buttonPressed() { guard let (info, items, installed) = currentStickerPack else { + self.requestDismiss() return } @@ -310,12 +362,57 @@ private final class StickerPackContainer: ASDisplayNode { var updateLayout = false + var scrollToItem: GridNodeScrollToItem? + switch contents { case .fetching: entries = [] + self.buttonNode.setTitle(self.presentationData.strings.Channel_NotificationLoading.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemDisabledTextColor, for: .normal) + self.buttonNode.setBackgroundImage(nil, for: []) + + for _ in 0 ..< 16 { + var stableId: Int? + inner: for entry in self.currentEntries { + if entry.stickerItem == nil, entry.index == entries.count { + stableId = entry.stableId + break inner + } + } + + let resolvedStableId: Int + if let stableId = stableId { + resolvedStableId = stableId + } else { + resolvedStableId = self.nextStableId + self.nextStableId += 1 + } + + self.nextStableId += 1 + entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: false)) + } + self.titlePlaceholderNode.alpha = 1.0 case .none: entries = [] + self.buttonNode.setTitle(self.presentationData.strings.Common_Close.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) + self.buttonNode.setBackgroundImage(nil, for: []) + + for _ in 0 ..< 16 { + let resolvedStableId = self.nextStableId + self.nextStableId += 1 + entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: true)) + } + if !self.titlePlaceholderNode.alpha.isZero { + self.titlePlaceholderNode.alpha = 0.0 + self.titlePlaceholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } case let .result(info, items, installed): + if !items.isEmpty && self.currentStickerPack == nil { + if let (_, _, _, gridInsets) = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { + scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: false) + } + } + self.currentStickerPack = (info, items, installed) if installed { @@ -341,6 +438,16 @@ private final class StickerPackContainer: ASDisplayNode { context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25) self.buttonNode.setBackgroundImage(roundedAccentBackground, for: []) + if self.titleNode.attributedText == nil { + self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + + if self.titleNode.attributedText == nil { + if !self.titlePlaceholderNode.alpha.isZero { + self.titlePlaceholderNode.alpha = 0.0 + self.titlePlaceholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } } self.titleNode.attributedText = NSAttributedString(string: info.title, font: Font.semibold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) @@ -350,7 +457,21 @@ private final class StickerPackContainer: ASDisplayNode { guard let item = item as? StickerPackItem else { continue } - entries.append(StickerPackPreviewGridEntry(index: entries.count, stickerItem: item)) + var stableId: Int? + inner: for entry in self.currentEntries { + if let stickerItem = entry.stickerItem, stickerItem.file.fileId == item.file.fileId { + stableId = entry.stableId + break inner + } + } + let resolvedStableId: Int + if let stableId = stableId { + resolvedStableId = stableId + } else { + resolvedStableId = self.nextStableId + self.nextStableId += 1 + } + entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: item, isEmpty: false)) } } let previousEntries = self.currentEntries @@ -363,7 +484,11 @@ private final class StickerPackContainer: ASDisplayNode { self.updateLayout(layout: layout, transition: .immediate) } - let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction) + let fakeTitleSize = CGSize(width: 160.0, height: 22.0) + self.titlePlaceholderNode.frame = CGRect(origin: CGPoint(x: floor((-fakeTitleSize.width) / 2.0), y: floor((-fakeTitleSize.height) / 2.0)), size: fakeTitleSize) + + + let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme, scrollToItem: scrollToItem) self.enqueueTransaction(transaction) } @@ -374,6 +499,19 @@ private final class StickerPackContainer: ASDisplayNode { return min(self.backgroundNode.frame.minY, gridFrame.minY + gridInsets.top - titleAreaInset) } + func syncExpandProgress(expandScrollProgress: CGFloat, expandProgress: CGFloat, modalProgress: CGFloat, transition: ContainedViewLayoutTransition) { + guard let (_, _, _, gridInsets) = self.validLayout else { + return + } + + let contentOffset = (1.0 - expandScrollProgress) * (-gridInsets.top) + self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) + + self.expandScrollProgress = expandScrollProgress + self.expandProgress = expandProgress + self.modalProgress = modalProgress + } + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { var insets = layout.insets(options: [.statusBar]) insets.top += 10.0 @@ -448,10 +586,16 @@ private final class StickerPackContainer: ASDisplayNode { let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top let expandHeight: CGFloat = 100.0 let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight)) + let expandScrollProgress = 1.0 - max(0.0, min(1.0, presentationLayout.contentOffset.y / (-gridInsets.top))) - var expandProgressTransition = transition + let expandProgressTransition = transition var expandUpdated = false + if abs(self.expandScrollProgress - expandScrollProgress) > CGFloat.ulpOfOne { + self.expandScrollProgress = expandScrollProgress + expandUpdated = true + } + if abs(self.expandProgress - expandProgress) > CGFloat.ulpOfOne { self.expandProgress = expandProgress expandUpdated = true @@ -482,7 +626,7 @@ private final class StickerPackContainer: ASDisplayNode { } let transaction = self.enqueuedTransactions.removeFirst() - self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: transaction.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -640,6 +784,11 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { let modalProgress = container.modalProgress strongSelf.modalProgressUpdated(modalProgress, transition) strongSelf.containerLayoutUpdated(layout, transition: .immediate) + for (otherIndex, otherContainer) in strongSelf.containers { + if otherContainer !== container { + otherContainer.syncExpandProgress(expandScrollProgress: container.expandScrollProgress, expandProgress: container.expandProgress, modalProgress: container.modalProgress, transition: .immediate) + } + } } }, presentInGlobalOverlay: presentInGlobalOverlay, sendSticker: sendSticker) @@ -647,11 +796,18 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { self.containers[i] = container } - containerTransition.updateFrame(node: container, frame: CGRect(origin: CGPoint(x: CGFloat(indexOffset) * layout.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: layout.size), beginWithCurrentState: true) + let containerFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * layout.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: layout.size) + containerTransition.updateFrame(node: container, frame: containerFrame, beginWithCurrentState: true) containerTransition.updateSublayerTransformScaleAndOffset(node: container, scale: containerScale, offset: CGPoint(), beginWithCurrentState: true) if container.validLayout?.0 != layout { container.updateLayout(layout: layout, transition: containerTransition) } + + if let selectedContainer = self.containers[self.selectedStickerPackIndex] { + if selectedContainer !== container { + container.syncExpandProgress(expandScrollProgress: selectedContainer.expandScrollProgress, expandProgress: selectedContainer.expandProgress, modalProgress: selectedContainer.modalProgress, transition: .immediate) + } + } } else { if let container = self.containers[i] { container.removeFromSupernode() @@ -703,9 +859,17 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { let deltaIndex = velocity.x > 0 ? -1 : 1 self.selectedStickerPackIndex = max(0, min(self.stickerPacks.count - 1, Int(self.selectedStickerPackIndex + deltaIndex))) } + let deltaOffset = self.relativeToSelectedStickerPackTransition self.relativeToSelectedStickerPackTransition = 0.0 if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.35, curve: .spring)) + let existingIndices = Array(self.containers.keys) + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + self.containerLayoutUpdated(layout, transition: transition) + for (key, container) in self.containers { + if !existingIndices.contains(key) { + transition.animatePositionAdditive(node: container, offset: CGPoint(x: -deltaOffset, y: 0.0)) + } + } } default: break @@ -717,12 +881,12 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let minInset: CGFloat = (self.containers.map { (_, container) -> CGFloat in container.topContentInset }).max() ?? 0.0 - self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } func animateOut(completion: @escaping () -> Void) { self.dimNode.alpha = 0.0 - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) let minInset: CGFloat = (self.containers.map { (_, container) -> CGFloat in container.topContentInset }).max() ?? 0.0 self.containerContainingNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in @@ -848,6 +1012,8 @@ public enum StickerPackScreenPerformedAction { } public func StickerPackScreen(context: AccountContext, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? = nil) -> ViewController { + return StickerPackScreenImpl(context: context, stickerPacks: stickerPacks, selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, parentNavigationController: parentNavigationController, sendSticker: sendSticker) + let controller = StickerPackPreviewController(context: context, stickerPack: mainStickerPack, mode: mode, parentNavigationController: parentNavigationController, actionPerformed: actionPerformed) controller.sendSticker = sendSticker return controller diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index 4543cc95d2..dc70e68749 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -392,6 +392,10 @@ public func chatMessageSticker(postbox: Postbox, file: TelegramMediaFile, small: let fullSizeData = value._1 let fullSizeComplete = value._2 return { arguments in + if thumbnailData == nil && fullSizeData == nil { + return nil + } + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) let drawingRect = arguments.drawingRect @@ -483,6 +487,10 @@ public func chatMessageAnimatedSticker(postbox: Postbox, file: TelegramMediaFile let fullSizeData = value._1 let fullSizeComplete = value._2 return { arguments in + if thumbnailData == nil && fullSizeData == nil { + return nil + } + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) let drawingRect = arguments.drawingRect diff --git a/submodules/TelegramUI/BUCK b/submodules/TelegramUI/BUCK index 4b24c7c120..0037548b08 100644 --- a/submodules/TelegramUI/BUCK +++ b/submodules/TelegramUI/BUCK @@ -204,6 +204,7 @@ framework( "//submodules/Svg:Svg", "//submodules/StatisticsUI:StatisticsUI", "//submodules/ManagedAnimationNode:ManagedAnimationNode", + "//submodules/TooltipUI:TooltipUI", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 896f395291..585f4d3d3e 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -202,6 +202,7 @@ swift_library( "//submodules/AccountUtils:AccountUtils", "//submodules/Svg:Svg", "//submodules/ManagedAnimationNode:ManagedAnimationNode", + "//submodules/TooltipUI:TooltipUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f563c41019..477ce7cfa6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -58,6 +58,7 @@ import PhoneNumberFormat import SettingsUI import UrlWhitelist import TelegramIntents +import TooltipUI public enum ChatControllerPeekActions { case standard @@ -1692,7 +1693,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G itemNode.animateQuizInvalidOptionSelected() if let solution = resultPoll.results.solution { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: solution), elevatedLayout: true, action: { _ in return false }), in: .window(.root)) + for contentNode in itemNode.contentNodes { + if let contentNode = contentNode as? ChatMessagePollBubbleContentNode, let sourceNode = contentNode.solutionTipSourceNode { + let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view) + + strongSelf.present(TooltipScreen(text: solution, icon: .info, location: absoluteFrame, shouldDismissOnTouch: { _ in + return true + }), in: .current) + } + } } } } @@ -1938,11 +1947,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } strongSelf.presentPollCreation(isQuiz: isQuiz) - }, displayPollSolution: { [weak self] text in + }, displayPollSolution: { [weak self] text, sourceNode in guard let strongSelf = self else { return } - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: text), elevatedLayout: true, action: { _ in return false }), in: .window(.root)) + let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -12.0, dy: 0.0) + strongSelf.present(TooltipScreen(text: text, icon: .info, location: absoluteFrame, shouldDismissOnTouch: { _ in + return true + }), in: .current) }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 7468326661..0d7c39a17f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -107,7 +107,7 @@ public final class ChatControllerInteraction { let dismissReplyMarkupMessage: (Message) -> Void let openMessagePollResults: (MessageId, Data) -> Void let openPollCreation: (Bool?) -> Void - let displayPollSolution: (String) -> Void + let displayPollSolution: (String, ASDisplayNode) -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -122,7 +122,7 @@ public final class ChatControllerInteraction { var searchTextHighightState: (String, [MessageIndex])? var seenOneTimeAnimatedMedia = Set() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (String) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (String, ASDisplayNode) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -222,7 +222,7 @@ public final class ChatControllerInteraction { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in - }, displayPollSolution: { _ in + }, displayPollSolution: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 182d48cffd..65ff6a9910 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -175,7 +175,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } private var replyInfoNode: ChatMessageReplyInfoNode? - private var contentNodes: [ChatMessageBubbleContentNode] = [] + private(set) var contentNodes: [ChatMessageBubbleContentNode] = [] private var mosaicStatusNode: ChatMessageDateAndStatusNode? private var actionButtonsNode: ChatMessageActionButtonsNode? diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index b1cf8fb78b..b82a91f689 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -851,6 +851,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { private let statusNode: ChatMessageDateAndStatusNode private var optionNodes: [ChatMessagePollOptionNode] = [] + var solutionTipSourceNode: ASDisplayNode? { + return self.solutionButtonNode + } + private var poll: TelegramMediaPoll? required init() { @@ -1452,16 +1456,16 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { timerTransition.updateTransformScale(node: timerNode, scale: 0.1) } - if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .anonymous = poll.publicity, case .quiz = poll.kind, let solution = poll.results.solution, !solution.isEmpty, (isClosed || hasSelected) { + if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .quiz = poll.kind, let solution = poll.results.solution, !solution.isEmpty, (isClosed || hasSelected) { let solutionButtonNode: SolutionButtonNode if let current = strongSelf.solutionButtonNode { solutionButtonNode = current } else { solutionButtonNode = SolutionButtonNode(pressed: { - guard let strongSelf = self, let item = strongSelf.item else { + guard let strongSelf = self, let solutionButtonNode = strongSelf.solutionButtonNode, let item = strongSelf.item else { return } - item.controllerInteraction.displayPollSolution(solution) + item.controllerInteraction.displayPollSolution(solution, solutionButtonNode) }) strongSelf.solutionButtonNode = solutionButtonNode strongSelf.addSubnode(solutionButtonNode) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index a2e5de3fd0..06599a75d5 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -423,7 +423,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in - }, displayPollSolution: { _ in + }, displayPollSolution: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 989bbcc9c5..55fa62ae71 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -90,7 +90,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur openPeer(peerId, .chat(textInputState: nil, subject: .message(messageId))) case let .stickerPack(name): dismissInput() - if false { + if true { var mainStickerPack: StickerPackReference? var stickerPacks: [StickerPackReference] = [] if let message = contentContext as? Message { diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift index a29ce96c7e..63bb0924de 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift @@ -122,7 +122,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in - }, displayPollSolution: { _ in + }, displayPollSolution: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 2d118d7917..7048bf4b94 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1527,7 +1527,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in - }, displayPollSolution: { _ in + }, displayPollSolution: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift b/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift index 9ffc1cec19..b96bf76684 100644 --- a/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift @@ -427,7 +427,7 @@ public class PeerMediaCollectionController: TelegramBaseController { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in - }, displayPollSolution: { _ in + }, displayPollSolution: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index 0dec75d964..adaf58152f 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -219,11 +219,11 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po entries.append(.text(poll.text)) - if let solution = poll.results.solution, !solution.isEmpty { + /*if let solution = poll.results.solution, !solution.isEmpty { //TODO:localize entries.append(.solutionHeader("EXPLANATION")) entries.append(.solutionText(solution)) - } + }*/ var optionVoterCount: [Int: Int32] = [:] let totalVoterCount = poll.results.totalVoters ?? 0 diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 2ae89b4e0e..23a9513b19 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1135,7 +1135,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in - }, displayPollSolution: { _ in + }, displayPollSolution: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 456e98ab58..1a4272918b 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -7,17 +7,20 @@ import AnimatedStickerNode import AppBundle private final class TooltipScreenNode: ViewControllerTracingNode { - private let location: CGPoint + private let icon: TooltipScreen.Icon + private let location: CGRect private let shouldDismissOnTouch: (CGPoint) -> Bool private let requestDismiss: () -> Void private let containerNode: ASDisplayNode private let backgroundNode: ASImageNode private let arrowNode: ASImageNode + private let arrowContainer: ASDisplayNode private let animatedStickerNode: AnimatedStickerNode private let textNode: ImmediateTextNode - init(text: String, location: CGPoint, shouldDismissOnTouch: @escaping (CGPoint) -> Bool, requestDismiss: @escaping () -> Void) { + init(text: String, icon: TooltipScreen.Icon, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> Bool, requestDismiss: @escaping () -> Void) { + self.icon = icon self.location = location self.shouldDismissOnTouch = shouldDismissOnTouch self.requestDismiss = requestDismiss @@ -39,20 +42,31 @@ private final class TooltipScreenNode: ViewControllerTracingNode { context.fillPath() }) + self.arrowContainer = ASDisplayNode() + self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 0 self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) self.animatedStickerNode = AnimatedStickerNode() - if let path = getAppBundle().path(forResource: "ChatListFoldersTooltip", ofType: "json") { - self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct) - self.animatedStickerNode.automaticallyLoadFirstFrame = true + switch icon { + case .chatListPress: + if let path = getAppBundle().path(forResource: "ChatListFoldersTooltip", ofType: "json") { + self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct) + self.animatedStickerNode.automaticallyLoadFirstFrame = true + } + case .info: + if let path = getAppBundle().path(forResource: "anim_infotip", ofType: "json") { + self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct) + self.animatedStickerNode.automaticallyLoadFirstFrame = true + } } super.init() - self.backgroundNode.addSubnode(self.arrowNode) + self.arrowContainer.addSubnode(self.arrowNode) + self.backgroundNode.addSubnode(self.arrowContainer) self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.textNode) self.containerNode.addSubnode(self.animatedStickerNode) @@ -64,19 +78,48 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let bottomInset: CGFloat = 10.0 let contentInset: CGFloat = 9.0 let contentVerticalInset: CGFloat = 11.0 - let animationSize = CGSize(width: 32.0, height: 32.0) - let animationInset: CGFloat = (70 - animationSize.width) / 2.0 + let animationSize: CGSize + let animationInset: CGFloat + + switch self.icon { + case .chatListPress: + animationSize = CGSize(width: 32.0, height: 32.0) + animationInset = (70.0 - animationSize.width) / 2.0 + case .info: + animationSize = CGSize(width: 32.0, height: 32.0) + animationInset = 0.0 + } + let animationSpacing: CGFloat = 8.0 let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - contentInset * 2.0 - sideInset * 2.0 - animationSize.width - animationSpacing, height: .greatestFiniteMagnitude)) let backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0 - let backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: self.location.y - bottomInset - backgroundHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: backgroundHeight)) + var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: self.location.minY - bottomInset - backgroundHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: backgroundHeight)) + var invertArrow = false + if backgroundFrame.minY < layout.insets(options: .statusBar).top { + backgroundFrame.origin.y = self.location.maxY + bottomInset + invertArrow = true + } + transition.updateFrame(node: self.containerNode, frame: backgroundFrame) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) if let image = self.arrowNode.image { let arrowSize = image.size - let arrowCenterX = self.location.x - transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: backgroundFrame.height), size: arrowSize)) + let arrowCenterX = self.location.midX + + let arrowFrame: CGRect + + if invertArrow { + arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: -arrowSize.height), size: arrowSize) + } else { + arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: backgroundFrame.height), size: arrowSize) + } + + transition.updateFrame(node: self.arrowContainer, frame: arrowFrame) + + ContainedViewLayoutTransition.immediate.updateTransformScale(node: self.arrowContainer, scale: CGPoint(x: 1.0, y: invertArrow ? -1.0 : 1.0)) + + self.arrowNode.frame = CGRect(origin: CGPoint(), size: arrowFrame.size) } transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize)) @@ -103,10 +146,18 @@ private final class TooltipScreenNode: ViewControllerTracingNode { func animateIn() { self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.01)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) - self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowNode.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowNode.frame.maxY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true) + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.maxY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true) self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in + let animationDelay: Double + switch self.icon { + case .chatListPress: + animationDelay = 0.6 + case .info: + animationDelay = 0.2 + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + animationDelay, execute: { [weak self] in self?.animatedStickerNode.visibility = true }) } @@ -116,13 +167,19 @@ private final class TooltipScreenNode: ViewControllerTracingNode { completion() }) self.containerNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.arrowNode.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowNode.frame.maxY - self.containerNode.bounds.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true) + self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.maxY - self.containerNode.bounds.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true) } } public final class TooltipScreen: ViewController { + public enum Icon { + case info + case chatListPress + } + private let text: String - private let location: CGPoint + private let icon: TooltipScreen.Icon + private let location: CGRect private let shouldDismissOnTouch: (CGPoint) -> Bool private var controllerNode: TooltipScreenNode { @@ -132,8 +189,9 @@ public final class TooltipScreen: ViewController { private var validLayout: ContainerViewLayout? private var isDismissed: Bool = false - public init(text: String, location: CGPoint, shouldDismissOnTouch: @escaping (CGPoint) -> Bool) { + public init(text: String, icon: TooltipScreen.Icon, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> Bool) { self.text = text + self.icon = icon self.location = location self.shouldDismissOnTouch = shouldDismissOnTouch @@ -151,13 +209,13 @@ public final class TooltipScreen: ViewController { self.controllerNode.animateIn() - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: { [weak self] in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 8.0, execute: { [weak self] in self?.dismiss() }) } override public func loadDisplayNode() { - self.displayNode = TooltipScreenNode(text: self.text, location: self.location, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in + self.displayNode = TooltipScreenNode(text: self.text, icon: self.icon, location: self.location, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in guard let strongSelf = self else { return }