diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 1ab62f620f..3c8642e60f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -1091,6 +1091,127 @@ private struct DownloadItem: Equatable { } } +private func filteredPeerSearchQueryResults(value: ([FoundPeer], [FoundPeer]), scope: TelegramSearchPeersScope) -> ([FoundPeer], [FoundPeer]) { + switch scope { + case .everywhere: + return value + case .channels: + return ( + value.0.filter { peer in + if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + }, + value.1.filter { peer in + if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + } + ) + } +} + +final class GlobalPeerSearchContext { + private struct SearchKey: Hashable { + var query: String + + init(query: String) { + self.query = query + } + } + + private final class QueryContext { + var value: ([FoundPeer], [FoundPeer])? + let subscribers = Bag<(TelegramSearchPeersScope, (([FoundPeer], [FoundPeer])) -> Void)>() + let disposable = MetaDisposable() + + init() { + } + + deinit { + self.disposable.dispose() + } + } + + private final class Impl { + private let queue: Queue + private var queryContexts: [SearchKey: QueryContext] = [:] + + init(queue: Queue) { + self.queue = queue + } + + func searchRemotePeers(engine: TelegramEngine, query: String, scope: TelegramSearchPeersScope, onNext: @escaping (([FoundPeer], [FoundPeer])) -> Void) -> Disposable { + let searchKey = SearchKey(query: query) + let queryContext: QueryContext + if let current = self.queryContexts[searchKey] { + queryContext = current + + if let value = queryContext.value { + onNext(filteredPeerSearchQueryResults(value: value, scope: scope)) + } + } else { + queryContext = QueryContext() + self.queryContexts[searchKey] = queryContext + queryContext.disposable.set((engine.contacts.searchRemotePeers( + query: query, + scope: .everywhere + ) + |> delay(0.4, queue: Queue.mainQueue()) + |> deliverOn(self.queue)).start(next: { [weak queryContext] value in + guard let queryContext else { + return + } + queryContext.value = value + for (scope, f) in queryContext.subscribers.copyItems() { + f(filteredPeerSearchQueryResults(value: value, scope: scope)) + } + })) + } + + let index = queryContext.subscribers.add((scope, onNext)) + + let queue = self.queue + return ActionDisposable { [weak self, weak queryContext] in + queue.async { + guard let self, let queryContext else { + return + } + guard let currentContext = self.queryContexts[searchKey], queryContext === queryContext else { + return + } + currentContext.subscribers.remove(index) + if currentContext.subscribers.isEmpty { + currentContext.disposable.dispose() + self.queryContexts.removeValue(forKey: searchKey) + } + } + } + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + init() { + let queue = Queue.mainQueue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue) + }) + } + + func searchRemotePeers(engine: TelegramEngine, query: String, scope: TelegramSearchPeersScope = .everywhere) -> Signal<([FoundPeer], [FoundPeer]), NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.searchRemotePeers(engine: engine, query: query, scope: scope, onNext: subscriber.putNext) + } + } +} + final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let context: AccountContext private let animationCache: AnimationCache @@ -1099,6 +1220,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let peersFilter: ChatListNodePeersFilter private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? private var presentationData: PresentationData + private let globalPeerSearchContext: GlobalPeerSearchContext? private let key: ChatListSearchPaneKey private let tagMask: EngineMessage.Tags? private let location: ChatListControllerLocation @@ -1175,7 +1297,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryDisposable: Disposable? private var searchOptionsDisposable: Disposable? - init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, globalPeerSearchContext: GlobalPeerSearchContext?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -1183,6 +1305,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.key = key self.location = location self.navigationController = navigationController + + let globalPeerSearchContext = globalPeerSearchContext ?? GlobalPeerSearchContext() + + self.globalPeerSearchContext = globalPeerSearchContext var peersFilter = peersFilter if case .forum = location { @@ -1788,18 +1914,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( - context.engine.contacts.searchRemotePeers(query: query) + globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) |> map { ($0.0, $0.1, false) } - |> delay(0.4, queue: Queue.concurrentDefaultQueue()) ) ) } else if let query = query, case .channels = key { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( - context.engine.contacts.searchRemotePeers(query: query, scope: .channels) + globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query, scope: .channels) |> map { ($0.0, $0.1, false) } - |> delay(0.4, queue: Queue.concurrentDefaultQueue()) ) ) } else { diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 6da789c482..0b32ff7113 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -127,10 +127,11 @@ private final class ChatListSearchPendingPane { location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, + globalPeerSearchContext: GlobalPeerSearchContext?, key: ChatListSearchPaneKey, hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void ) { - let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, globalPeerSearchContext: globalPeerSearchContext) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady @@ -156,6 +157,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD private let location: ChatListControllerLocation private let searchQuery: Signal private let searchOptions: Signal + private let globalPeerSearchContext: GlobalPeerSearchContext private let navigationController: NavigationController? var interaction: ChatListSearchInteraction? @@ -198,6 +200,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD self.searchQuery = searchQuery self.searchOptions = searchOptions self.navigationController = navigationController + self.globalPeerSearchContext = GlobalPeerSearchContext() super.init() } @@ -432,6 +435,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD location: self.location, searchQuery: self.searchQuery, searchOptions: self.searchOptions, + globalPeerSearchContext: self.globalPeerSearchContext, key: key, hasBecomeReady: { [weak self] key in let apply: () -> Void = { diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index e46a510fd9..d4dfa2809e 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -474,7 +474,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } public func update(revealed: Bool, animated: Bool = true) { - guard self.isRevealed != revealed, let textNode = self.textNode else { + guard self.isRevealed != revealed else { return } @@ -483,11 +483,15 @@ public class InvisibleInkDustNode: ASDisplayNode { if revealed { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate transition.updateAlpha(node: self, alpha: 0.0) - transition.updateAlpha(node: textNode, alpha: 1.0) + if let textNode = self.textNode { + transition.updateAlpha(node: textNode, alpha: 1.0) + } } else { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .linear) : .immediate transition.updateAlpha(node: self, alpha: 1.0) - transition.updateAlpha(node: textNode, alpha: 0.0) + if let textNode = self.textNode { + transition.updateAlpha(node: textNode, alpha: 0.0) + } if self.isExploding { self.isExploding = false @@ -497,7 +501,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } public func revealAtLocation(_ location: CGPoint) { - guard let (_, _, textColor, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else { + guard let (_, _, textColor, _, _) = self.currentParams, !self.isRevealed else { return } @@ -507,7 +511,7 @@ public class InvisibleInkDustNode: ASDisplayNode { self.isExploding = true self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") - self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position") + self.emitterLayer?.setValue(location, forKeyPath: "emitterBehaviors.fingerAttractor.position") let maskSize = self.emitterNode.frame.size Queue.concurrentDefaultQueue().async { @@ -520,10 +524,15 @@ public class InvisibleInkDustNode: ASDisplayNode { } } - Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { - textNode.alpha = 1.0 + Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { [weak self] in + guard let self else { + return + } - textNode.view.mask = self.textMaskNode.view + if let textNode = self.textNode { + textNode.alpha = 1.0 + textNode.view.mask = self.textMaskNode.view + } self.textSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0) let xFactor = (location.x / self.emitterNode.frame.width - 0.5) * 2.0 @@ -539,8 +548,13 @@ public class InvisibleInkDustNode: ASDisplayNode { self.textSpotNode.layer.anchorPoint = CGPoint(x: location.x / self.emitterMaskNode.frame.width, y: location.y / self.emitterMaskNode.frame.height) self.textSpotNode.position = location - self.textSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { _ in - textNode.view.mask = nil + self.textSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + if let textNode = self.textNode { + textNode.view.mask = nil + } }) self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) @@ -567,8 +581,10 @@ public class InvisibleInkDustNode: ASDisplayNode { self.emitterMaskFillNode.layer.removeAllAnimations() } } else { - textNode.alpha = 1.0 - textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + if let textNode = self.textNode { + textNode.alpha = 1.0 + textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } self.staticNode?.alpha = 0.0 self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index ee47fa2bc2..2f6ed085d3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -111,7 +111,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var expandedBlockIds: Set = Set() private var appliedExpandedBlockIds: Set? - private var displayContentsUnderSpoilers: Bool = false + private var displayContentsUnderSpoilers: (value: Bool, location: CGPoint?) = (false, nil) override public var visibility: ListViewItemNodeVisibility { didSet { @@ -162,11 +162,15 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.requestMessageUpdate(item.message.id, false) } - self.textNode.textNode.requestDisplayContentsUnderSpoilers = { [weak self] in + self.textNode.textNode.requestDisplayContentsUnderSpoilers = { [weak self] location in guard let self else { return } - self.updateDisplayContentsUnderSpoilers(value: true) + var mappedLocation: CGPoint? + if let location { + mappedLocation = self.textNode.textNode.layer.convert(location, to: self.layer) + } + self.updateDisplayContentsUnderSpoilers(value: true, at: mappedLocation) } self.textNode.textNode.canHandleTapAtPoint = { [weak self] point in guard let self else { @@ -586,7 +590,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor, - displayContentsUnderSpoilers: displayContentsUnderSpoilers, + displayContentsUnderSpoilers: displayContentsUnderSpoilers.value, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlockIds )) @@ -677,6 +681,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.appliedExpandedBlockIds = strongSelf.expandedBlockIds + var spoilerExpandRect: CGRect? + if let location = strongSelf.displayContentsUnderSpoilers.location { + spoilerExpandRect = textFrame.size.centered(around: CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) + } + let _ = textApply(InteractiveTextNodeWithEntities.Arguments( context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, @@ -685,7 +694,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { attemptSynchronous: synchronousLoads, textColor: messageTheme.primaryTextColor, spoilerEffectColor: messageTheme.secondaryTextColor, - animation: animation + animation: animation, + animationArguments: InteractiveTextNode.AnimationArguments( + spoilerExpandRect: spoilerExpandRect + ) )) animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil) @@ -853,7 +865,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let textNodeFrame = self.textNode.textNode.frame let textLocalPoint = CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY) if let (index, attributes) = self.textNode.textNode.attributesAtPoint(textLocalPoint) { - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers { + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !self.displayContentsUnderSpoilers.value { return ChatMessageBubbleContentTapAction(content: .none) } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true @@ -1045,7 +1057,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, !self.displayContentsUnderSpoilers { + if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, !self.displayContentsUnderSpoilers.value { } else if let rects = rects { let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { @@ -1315,11 +1327,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { guard let strongSelf = self else { return } - if !strongSelf.displayContentsUnderSpoilers, let textLayout = strongSelf.textNode.textNode.cachedLayout, textLayout.segments.contains(where: { !$0.spoilers.isEmpty }), let selectionRange { + if !strongSelf.displayContentsUnderSpoilers.value, let textLayout = strongSelf.textNode.textNode.cachedLayout, textLayout.segments.contains(where: { !$0.spoilers.isEmpty }), let selectionRange { for segment in textLayout.segments { for (spoilerRange, _) in segment.spoilers { if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 { - strongSelf.updateDisplayContentsUnderSpoilers(value: true) + strongSelf.updateDisplayContentsUnderSpoilers(value: true, at: nil) return } } @@ -1375,17 +1387,18 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { }) } - if self.displayContentsUnderSpoilers { - self.updateDisplayContentsUnderSpoilers(value: false) + if self.displayContentsUnderSpoilers.value { + self.updateDisplayContentsUnderSpoilers(value: false, at: nil) } } } - private func updateDisplayContentsUnderSpoilers(value: Bool) { - if self.displayContentsUnderSpoilers == value { + private func updateDisplayContentsUnderSpoilers(value: Bool, at location: CGPoint?) { + if self.displayContentsUnderSpoilers.value == value { return } - self.displayContentsUnderSpoilers = value + self.displayContentsUnderSpoilers = (value, location) + self.displayContentsUnderSpoilers.location = nil if let item = self.item { item.controllerInteraction.requestMessageUpdate(item.message.id, false) } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 6c5d3bb50a..151c338855 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -769,7 +769,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess let messagesContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) self.messagesContainer.frame = messagesContainerFrame - return messagesContainerFrame.size + // 4.0 is a magic number to compensate for offset in other types of content + return CGSize(width: messagesContainerFrame.width, height: messagesContainerFrame.height - 4.0) } } } diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 78b09b1597..bfd9029500 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -1073,6 +1073,14 @@ private func addAttachment(attachment: UIImage, line: InteractiveTextNodeLine, a } open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecognizerDelegate { + public final class AnimationArguments { + public let spoilerExpandRect: CGRect? + + public init(spoilerExpandRect: CGRect?) { + self.spoilerExpandRect = spoilerExpandRect + } + } + public struct RenderContentTypes: OptionSet { public var rawValue: Int @@ -1106,7 +1114,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn public var canHandleTapAtPoint: ((CGPoint) -> Bool)? public var requestToggleBlockCollapsed: ((Int) -> Void)? - public var requestDisplayContentsUnderSpoilers: (() -> Void)? + public var requestDisplayContentsUnderSpoilers: ((CGPoint?) -> Void)? private var tapRecognizer: UITapGestureRecognizer? public var currentText: NSAttributedString? { @@ -1676,7 +1684,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlocks) } - private func updateContentItems(animation: ListViewItemUpdateAnimation) { + private func updateContentItems(animation: ListViewItemUpdateAnimation, animationArguments: AnimationArguments?) { guard let cachedLayout = self.cachedLayout else { return } @@ -1729,8 +1737,15 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn var contentItemAnimation = animation let contentItemLayer: TextContentItemLayer + var itemSpoilerExpandRect: CGRect? + var itemAnimateContents = animateContents && contentItemAnimation.isAnimated if let current = self.contentItemLayers[itemId] { contentItemLayer = current + + if animation.isAnimated, let spoilerExpandRect = animationArguments?.spoilerExpandRect { + itemSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -contentItemFrame.minX, dy: -contentItemFrame.minY) + itemAnimateContents = true + } } else { contentItemAnimation = .None contentItemLayer = TextContentItemLayer() @@ -1738,7 +1753,13 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn self.layer.addSublayer(contentItemLayer) } - contentItemLayer.update(item: contentItem, animation: contentItemAnimation, synchronously: synchronous, animateContents: animateContents && contentItemAnimation.isAnimated) + contentItemLayer.update( + item: contentItem, + animation: contentItemAnimation, + synchronously: synchronous, + animateContents: itemAnimateContents, + spoilerExpandRect: itemSpoilerExpandRect + ) contentItemAnimation.animator.updateFrame(layer: contentItemLayer, frame: contentItemFrame, completion: nil) } @@ -1779,7 +1800,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let point = recognizer.location(in: self.view) if let cachedLayout = self.cachedLayout, !cachedLayout.displayContentsUnderSpoilers, let (_, attributes) = self.attributesAtPoint(point) { if attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil { - self.requestDisplayContentsUnderSpoilers?() + self.requestDisplayContentsUnderSpoilers?(point) return } } @@ -1789,7 +1810,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } } - public static func asyncLayout(_ maybeNode: InteractiveTextNode?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (ListViewItemUpdateAnimation) -> InteractiveTextNode) { + public static func asyncLayout(_ maybeNode: InteractiveTextNode?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (ListViewItemUpdateAnimation, AnimationArguments?) -> InteractiveTextNode) { let existingLayout: InteractiveTextNodeLayout? = maybeNode?.cachedLayout return { arguments in @@ -1831,10 +1852,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let node = maybeNode ?? InteractiveTextNode() - return (layout, { animation in + return (layout, { animation, animationArguments in if node.cachedLayout !== layout { node.cachedLayout = layout - node.updateContentItems(animation: animation) + node.updateContentItems(animation: animation, animationArguments: animationArguments) } return node @@ -2202,7 +2223,13 @@ final class TextContentItemLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } - func update(item: TextContentItem, animation: ListViewItemUpdateAnimation, synchronously: Bool = false, animateContents: Bool = false) { + func update( + item: TextContentItem, + animation: ListViewItemUpdateAnimation, + synchronously: Bool, + animateContents: Bool, + spoilerExpandRect: CGRect? + ) { self.item = item let contentFrame = CGRect(origin: CGPoint(), size: item.size) @@ -2241,7 +2268,13 @@ final class TextContentItemLayer: SimpleLayer { return } self.isAnimating = false - self.update(item: item, animation: .None, synchronously: true) + self.update( + item: item, + animation: .None, + synchronously: true, + animateContents: false, + spoilerExpandRect: nil + ) }) } else { blockBackgroundView.layer.frame = blockBackgroundFrame @@ -2362,13 +2395,61 @@ final class TextContentItemLayer: SimpleLayer { self.renderNode.params = RenderParams(size: contentFrame.size, item: item, mask: staticContentMask) if synchronously { - let previousContents = self.renderNode.layer.contents - self.renderNode.displayImmediately() - if animateContents, let previousContents { - animation.transition.animateContents(layer: self.renderNode.layer, from: previousContents) + if let spoilerExpandRect { + let _ = spoilerExpandRect + + self.renderNode.displayImmediately() + + let maskFrame = self.renderNode.frame + + let maskLayer = SimpleLayer() + maskLayer.frame = maskFrame + self.addSublayer(maskLayer) + + let maskGradientLayer = SimpleGradientLayer() + maskGradientLayer.frame = CGRect(origin: CGPoint(), size: maskFrame.size) + setupSpoilerExpansionMaskGradient( + gradientLayer: maskGradientLayer, + centerLocation: CGPoint( + x: 0.5, + y: 0.5 + ), + radius: CGSize( + width: 1.5, + height: 1.5 + ), + inverse: false + ) + } else { + let previousContents = self.renderNode.layer.contents + self.renderNode.displayImmediately() + if animateContents, let previousContents { + animation.transition.animateContents(layer: self.renderNode.layer, from: previousContents) + } } } else { self.renderNode.setNeedsDisplay() } } } + +private func setupSpoilerExpansionMaskGradient(gradientLayer: SimpleGradientLayer, centerLocation: CGPoint, radius: CGSize, inverse: Bool) { + let startAlpha: CGFloat = inverse ? 0.0 : 1.0 + let endAlpha: CGFloat = inverse ? 1.0 : 0.0 + + let locations: [CGFloat] = [0.0, 0.7, 0.95, 1.0] + let colors: [CGColor] = [ + UIColor(rgb: 0xff0000, alpha: startAlpha).cgColor, + UIColor(rgb: 0xff0000, alpha: startAlpha).cgColor, + UIColor(rgb: 0xff0000, alpha: endAlpha).cgColor, + UIColor(rgb: 0xff0000, alpha: endAlpha).cgColor + ] + + gradientLayer.type = .radial + gradientLayer.colors = colors + gradientLayer.locations = locations.map { $0 as NSNumber } + gradientLayer.startPoint = centerLocation + + let endEndPoint = CGPoint(x: (gradientLayer.startPoint.x + radius.width) * 1.0, y: (gradientLayer.startPoint.y + radius.height) * 1.0) + gradientLayer.endPoint = endEndPoint +} diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift index 77fd404cb1..97ae6c2c1f 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift @@ -65,6 +65,7 @@ public final class InteractiveTextNodeWithEntities { public let textColor: UIColor public let spoilerEffectColor: UIColor public let animation: ListViewItemUpdateAnimation + public let animationArguments: InteractiveTextNode.AnimationArguments? public init( context: AccountContext, @@ -74,7 +75,8 @@ public final class InteractiveTextNodeWithEntities { attemptSynchronous: Bool, textColor: UIColor, spoilerEffectColor: UIColor, - animation: ListViewItemUpdateAnimation + animation: ListViewItemUpdateAnimation, + animationArguments: InteractiveTextNode.AnimationArguments? ) { self.context = context self.cache = cache @@ -84,6 +86,7 @@ public final class InteractiveTextNodeWithEntities { self.textColor = textColor self.spoilerEffectColor = spoilerEffectColor self.animation = animation + self.animationArguments = animationArguments } public func withUpdatedPlaceholderColor(_ color: UIColor) -> Arguments { @@ -95,7 +98,8 @@ public final class InteractiveTextNodeWithEntities { attemptSynchronous: self.attemptSynchronous, textColor: self.textColor, spoilerEffectColor: self.spoilerEffectColor, - animation: self.animation + animation: self.animation, + animationArguments: self.animationArguments ) } } @@ -113,6 +117,7 @@ public final class InteractiveTextNodeWithEntities { private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayerData] = [:] private var dustEffectNodes: [Int: InvisibleInkDustNode] = [:] + private var displayContentsUnderSpoilers: Bool? private var enableLooping: Bool = true @@ -215,11 +220,22 @@ public final class InteractiveTextNodeWithEntities { return (layout, { applyArguments in let animation: ListViewItemUpdateAnimation = applyArguments?.animation ?? .None - let result = apply(animation) + let result = apply(animation, applyArguments?.animationArguments) if let maybeNode = maybeNode { if let applyArguments = applyArguments { - maybeNode.updateInteractiveContents(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, textColor: applyArguments.textColor, spoilerEffectColor: applyArguments.spoilerEffectColor, animation: animation) + maybeNode.updateInteractiveContents( + context: applyArguments.context, + cache: applyArguments.cache, + renderer: applyArguments.renderer, + textLayout: layout, + placeholderColor: applyArguments.placeholderColor, + attemptSynchronousLoad: false, + textColor: applyArguments.textColor, + spoilerEffectColor: applyArguments.spoilerEffectColor, + animation: animation, + animationArguments: applyArguments.animationArguments + ) } return maybeNode @@ -227,7 +243,18 @@ public final class InteractiveTextNodeWithEntities { let resultNode = InteractiveTextNodeWithEntities(textNode: result) if let applyArguments = applyArguments { - resultNode.updateInteractiveContents(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false, textColor: applyArguments.textColor, spoilerEffectColor: applyArguments.spoilerEffectColor, animation: .None) + resultNode.updateInteractiveContents( + context: applyArguments.context, + cache: applyArguments.cache, + renderer: applyArguments.renderer, + textLayout: layout, + placeholderColor: applyArguments.placeholderColor, + attemptSynchronousLoad: false, + textColor: applyArguments.textColor, + spoilerEffectColor: applyArguments.spoilerEffectColor, + animation: .None, + animationArguments: nil + ) } return resultNode @@ -253,7 +280,8 @@ public final class InteractiveTextNodeWithEntities { attemptSynchronousLoad: Bool, textColor: UIColor, spoilerEffectColor: UIColor, - animation: ListViewItemUpdateAnimation + animation: ListViewItemUpdateAnimation, + animationArguments: InteractiveTextNode.AnimationArguments? ) { self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji @@ -261,6 +289,8 @@ public final class InteractiveTextNodeWithEntities { if let textLayout { displayContentsUnderSpoilers = textLayout.displayContentsUnderSpoilers } + let previousDisplayContentsUnderSpoilers = self.displayContentsUnderSpoilers + self.displayContentsUnderSpoilers = displayContentsUnderSpoilers var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] @@ -345,7 +375,12 @@ public final class InteractiveTextNodeWithEntities { wordRects: segment.spoilerWords.map { $0.1.offsetBy(dx: segmentItem.contentOffset.x + 3.0, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) } ) - animation.transition.updateAlpha(node: dustEffectNode, alpha: displayContentsUnderSpoilers ? 0.0 : 1.0) + if let previousDisplayContentsUnderSpoilers, previousDisplayContentsUnderSpoilers != displayContentsUnderSpoilers, displayContentsUnderSpoilers, let currentSpoilerExpandRect = animationArguments?.spoilerExpandRect { + let spoilerLocalPosition = self.textNode.layer.convert(currentSpoilerExpandRect.center, to: dustEffectNode.layer) + dustEffectNode.revealAtLocation(spoilerLocalPosition) + } else { + dustEffectNode.update(revealed: displayContentsUnderSpoilers, animated: previousDisplayContentsUnderSpoilers != nil && animation.isAnimated) + } } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index e1580cd240..4e0574cab9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -552,7 +552,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { } transition.updateAlpha(node: self.regularContentNode, alpha: (state.isEditing || self.customNavigationContentNode != nil) ? 0.0 : 1.0) - transition.updateAlpha(node: self.navigationButtonContainer, alpha: self.customNavigationContentNode != nil ? 0.0 : 1.0) + if self.navigationTransition == nil { + transition.updateAlpha(node: self.navigationButtonContainer, alpha: self.customNavigationContentNode != nil ? 0.0 : 1.0) + } self.editingContentNode.alpha = state.isEditing ? 1.0 : 0.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 2e0e110acd..c51462b0a2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -11043,7 +11043,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive, animateHeader: transition.isAnimated) + let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: transition.isAnimated && self.headerNode.navigationTransition == nil) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight)) if additive { transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) @@ -11416,7 +11416,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive, animateHeader: animateHeader) + let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: animateHeader && self.headerNode.navigationTransition == nil) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -12946,7 +12946,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } let headerInset = sectionInset - topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: true) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: false) } let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height