Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2024-05-28 18:13:26 +04:00
commit 9ea80ff9dc
9 changed files with 333 additions and 57 deletions

View File

@ -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<Impl>
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<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?, globalPeerSearchContext: GlobalPeerSearchContext?) {
self.context = context
self.animationCache = animationCache
self.animationRenderer = animationRenderer
@ -1184,6 +1306,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.location = location
self.navigationController = navigationController
let globalPeerSearchContext = globalPeerSearchContext ?? GlobalPeerSearchContext()
self.globalPeerSearchContext = globalPeerSearchContext
var peersFilter = peersFilter
if case .forum = location {
//peersFilter.insert(.excludeRecent)
@ -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 {

View File

@ -127,10 +127,11 @@ private final class ChatListSearchPendingPane {
location: ChatListControllerLocation,
searchQuery: Signal<String?, NoError>,
searchOptions: Signal<ChatListSearchOptions?, NoError>,
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<String?, NoError>
private let searchOptions: Signal<ChatListSearchOptions?, NoError>
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 = {

View File

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

View File

@ -111,7 +111,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private var expandedBlockIds: Set<Int> = Set()
private var appliedExpandedBlockIds: Set<Int>?
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)
}

View File

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

View File

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

View File

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

View File

@ -552,7 +552,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
transition.updateAlpha(node: self.regularContentNode, alpha: (state.isEditing || 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

View File

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