Bot forums

This commit is contained in:
Isaac
2025-08-19 17:24:33 +02:00
parent bdaf5f5a02
commit ee749050f0
35 changed files with 990 additions and 144 deletions

View File

@@ -113,6 +113,35 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private var appliedExpandedBlockIds: Set<Int>?
private var displayContentsUnderSpoilers: (value: Bool, location: CGPoint?) = (false, nil)
private final class TextRevealAnimationState {
let fromCount: Int
let toCount: Int
let startTimestamp: Double
let duration: Double
init(fromCount: Int, toCount: Int, startTimestamp: Double, duration: Double) {
self.fromCount = fromCount
self.toCount = toCount
self.startTimestamp = startTimestamp
self.duration = duration
}
func fraction(timestamp: Double) -> CGFloat {
var animationFraction = (timestamp - self.startTimestamp) / self.duration
animationFraction = max(0.0, min(1.0, animationFraction))
return animationFraction
}
func glyphCount(timestamp: Double) -> Int {
let animationFraction = self.fraction(timestamp: timestamp)
let glyphCount = (1.0 - animationFraction) * Double(self.fromCount) + animationFraction * Double(self.toCount)
return Int(glyphCount)
}
}
private var textRevealLink: SharedDisplayLinkDriver.Link?
private var textRevealAnimationState: TextRevealAnimationState?
override public var visibility: ListViewItemNodeVisibility {
didSet {
if oldValue != self.visibility {
@@ -131,6 +160,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
required public init() {
self.containerNode = ASDisplayNode()
self.containerNode.clipsToBounds = true
self.textNode = InteractiveTextNodeWithEntities()
@@ -200,12 +230,22 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let previousItem = self.item
let textLayout = InteractiveTextNodeWithEntities.asyncLayout(self.textNode)
let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode)
let currentCachedChatMessageText = self.cachedChatMessageText
let expandedBlockIds = self.expandedBlockIds
let displayContentsUnderSpoilers = self.displayContentsUnderSpoilers
let currentMaxGlyphCount: Int?
if let textRevealAnimationState = self.textRevealAnimationState {
currentMaxGlyphCount = textRevealAnimationState.glyphCount(timestamp: CACurrentMediaTime())
//print("currentMaxGlyphCount(\(textRevealAnimationState.fromCount) -> \(textRevealAnimationState.toCount)) fraction: \(textRevealAnimationState.fraction(timestamp: CACurrentMediaTime()))")
} else {
currentMaxGlyphCount = nil
}
let previousGlyphCount = self.textNode.textNode.getGlyphCount()
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@@ -673,9 +713,32 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size)
var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom))
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset)
let realTextFrame = textFrame
var hasDraft = false
if item.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) {
hasDraft = true
}
var hadDraft = false
if let previousItem, previousItem.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) {
hadDraft = true
}
var maxGlyphCount = currentMaxGlyphCount
if maxGlyphCount == nil && (hasDraft || hadDraft) {
maxGlyphCount = previousGlyphCount
}
if let maxGlyphCount {
textFrame.size = textLayout.sizeForGlyphCount(glyphCount: maxGlyphCount)
//print("currentMaxGlyphCount: \(currentMaxGlyphCount), size: \(textFrame.size.height)")
textFrameWithoutInsets.size = CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)
}
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset)
var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width
@@ -706,8 +769,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.cachedChatMessageText = updatedCachedChatMessageText
}
var previousAnimateGlyphCount: Int?
if hasDraft || hadDraft {
previousAnimateGlyphCount = strongSelf.textNode.textNode.getGlyphCount()
}
strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize)
animation.animator.updateFrame(layer: strongSelf.containerNode.layer, frame: CGRect(origin: CGPoint(), size: boundingSize), completion: nil)
if strongSelf.appliedExpandedBlockIds != nil && strongSelf.appliedExpandedBlockIds != strongSelf.expandedBlockIds {
itemApply?.setInvertOffsetDirection()
@@ -764,7 +832,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
)
))
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil)
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: realTextFrame, completion: nil)
switch strongSelf.visibility {
case .none:
@@ -785,8 +853,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
strongSelf.textAccessibilityOverlayNode.frame = textFrame
//TODO:release
//strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
strongSelf.updateIsTranslating(isTranslating)
@@ -897,6 +963,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.codeHighlightState = nil
codeHighlightState.disposable.dispose()
}
if previousAnimateGlyphCount != nil || strongSelf.textRevealAnimationState != nil || hadDraft {
strongSelf.updateTextRevealAnimation(previousGlyphCount: previousAnimateGlyphCount ?? 0)
}
}
})
})
@@ -904,6 +974,74 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
private func updateTextRevealAnimation(previousGlyphCount: Int) {
var fromCount = previousGlyphCount
let toCount = self.textNode.textNode.getGlyphCount()
let timestamp = CACurrentMediaTime()
if let textRevealAnimationState = self.textRevealAnimationState {
if textRevealAnimationState.toCount == toCount {
return
}
fromCount = textRevealAnimationState.glyphCount(timestamp: timestamp)
}
if fromCount == toCount {
if self.textRevealAnimationState != nil {
self.textRevealAnimationState = nil
self.textRevealLink = nil
self.textNode.textNode.updateRevealGlyphCount(count: nil)
}
return
}
var duration: Double = Double(toCount - fromCount) / 20.0
duration = max(0.1, min(duration, 5.0))
self.textRevealAnimationState = TextRevealAnimationState(
fromCount: fromCount,
toCount: toCount,
startTimestamp: timestamp,
duration: duration
)
if self.textRevealLink == nil, self.textRevealAnimationState != nil {
var lastLineUpdateTimestamp = timestamp
self.textRevealLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
guard let self else {
return
}
guard let textRevealAnimationState = self.textRevealAnimationState else {
self.textRevealLink = nil
return
}
let timestamp = CACurrentMediaTime()
if textRevealAnimationState.fraction(timestamp: timestamp) >= 1.0 {
self.textRevealAnimationState = nil
self.textRevealLink = nil
self.textNode.textNode.updateRevealGlyphCount(count: nil)
self.requestFullUpdate?()
} else {
let lineUpdateTimeout = timestamp - lastLineUpdateTimestamp
var requestUpdate = false
let glyphCount = textRevealAnimationState.glyphCount(timestamp: timestamp)
if let revealGlyphCount = self.textNode.textNode.revealGlyphCount, let cachedLayout = self.textNode.textNode.cachedLayout {
if cachedLayout.sizeForGlyphCount(glyphCount: revealGlyphCount).height != cachedLayout.sizeForGlyphCount(glyphCount: glyphCount).height {
if lineUpdateTimeout >= 0.1 {
lastLineUpdateTimestamp = timestamp
requestUpdate = true
}
}
}
self.textNode.textNode.updateRevealGlyphCount(count: glyphCount)
if requestUpdate {
self.requestFullUpdate?()
}
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.statusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
@@ -1569,7 +1707,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
guard let self, completed else {
return
}
self.containerNode.clipsToBounds = false
self.containerNode.clipsToBounds = true
})
}
}