@@ -1679,7 +1679,11 @@ public class ChatControllerView : View, ChatInputDelegate {
16791679 return frame.focusX(size, y: y)
16801680 }
16811681
1682- func showToaster(text: NSAttributedString, action: (() -> Void)? = nil, timeout: Double, animated: Bool) {
1682+ var presentationTextColor: NSColor {
1683+ return presentation.colors.text
1684+ }
1685+
1686+ func showToaster(text: NSAttributedString, animation: LocalAnimatedSticker? = nil, context: (any AccountContextProtocol)? = nil, action: (() -> Void)? = nil, timeout: Double, animated: Bool) {
16831687 // Tear down any in-flight toaster before installing the new one —
16841688 // `dismissImmediately` only kills the timer, so we also have to
16851689 // remove the view from the hierarchy or it sticks around forever.
@@ -1691,7 +1695,7 @@ public class ChatControllerView : View, ChatInputDelegate {
16911695
16921696 let view = ChatToasterView(frame: .zero)
16931697 let maxWidth = max(80, frame.width - 24)
1694- view.update(text: text, maxWidth: maxWidth)
1698+ view.update(text: text, animation: animation, context: context, maxWidth: maxWidth)
16951699 let size = view.contentSize(maxWidth: maxWidth)
16961700 let y = ChatHeaderBubblesView.totalHeight + header.state.height + 8
16971701 view.frame = frame.focusX(size, y: y)
@@ -1737,6 +1741,10 @@ public class ChatControllerView : View, ChatInputDelegate {
17371741final class ChatToasterView: Control {
17381742 private let glass: LiquidGlassView
17391743 private let textView = TextView()
1744+ private let animationView: MediaAnimatedStickerView = MediaAnimatedStickerView(frame: NSMakeRect(0, 0, 26, 26))
1745+ private var hasAnimation: Bool = false
1746+ private let animationSize: CGFloat = 26
1747+ private let animationSpacing: CGFloat = 8
17401748 /// Auto-dismiss timer owned by the view itself, so the view manages its
17411749 /// own lifecycle. Previously a separate `ChatToaster` model held both a
17421750 /// strong reference to the view AND the disposable, which made the
@@ -1752,6 +1760,9 @@ final class ChatToasterView: Control {
17521760 super.init(frame: frameRect)
17531761 addSubview(glass)
17541762 addSubview(textView)
1763+ addSubview(animationView)
1764+ animationView.userInteractionEnabled = false
1765+ animationView.isHidden = true
17551766 textView.isSelectable = false
17561767 textView.userInteractionEnabled = false
17571768 textView.disableBackgroundDrawing = true
@@ -1766,9 +1777,22 @@ final class ChatToasterView: Control {
17661777 dismissDisposable.dispose()
17671778 }
17681779
1769- func update(text: NSAttributedString, maxWidth: CGFloat) {
1780+ func update(text: NSAttributedString, animation: LocalAnimatedSticker? = nil, context: (any AccountContextProtocol)? = nil, maxWidth: CGFloat) {
1781+ let iconWidth: CGFloat
1782+ if let animation, let context {
1783+ hasAnimation = true
1784+ animationView.isHidden = false
1785+ let color = (text.length > 0 ? text.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor : nil) ?? NSColor.white
1786+ let parameters = ChatAnimatedStickerMediaLayoutParameters(playPolicy: .once, alwaysAccept: true, cache: .temporaryLZ4(.thumb), hidePlayer: false, media: animation.file, colors: [.init(keyPath: "", color: color)], shimmer: false)
1787+ animationView.update(with: animation.file, size: NSMakeSize(animationSize, animationSize), context: context, table: nil, parameters: parameters, animated: false)
1788+ iconWidth = animationSize + animationSpacing
1789+ } else {
1790+ hasAnimation = false
1791+ animationView.isHidden = true
1792+ iconWidth = 0
1793+ }
17701794 let layout = TextViewLayout(text, maximumNumberOfLines: 3, truncationType: .end, alignment: .center)
1771- layout.measure(width: max(40, maxWidth - horizontalPadding * 2))
1795+ layout.measure(width: max(40, maxWidth - horizontalPadding * 2 - iconWidth ))
17721796 textView.update(layout)
17731797 }
17741798
@@ -1788,7 +1812,8 @@ final class ChatToasterView: Control {
17881812 guard let layout = textView.textLayout else {
17891813 return NSMakeSize(120, minHeight)
17901814 }
1791- let width = min(maxWidth, layout.layoutSize.width + horizontalPadding * 2)
1815+ let iconWidth: CGFloat = hasAnimation ? animationSize + animationSpacing : 0
1816+ let width = min(maxWidth, layout.layoutSize.width + iconWidth + horizontalPadding * 2)
17921817 let height = max(minHeight, layout.layoutSize.height + verticalPadding * 2)
17931818 return NSMakeSize(width, height)
17941819 }
@@ -1797,7 +1822,15 @@ final class ChatToasterView: Control {
17971822 super.layout()
17981823 glass.frame = bounds
17991824 glass.cornerRadius = bounds.height / 2
1800- textView.center()
1825+ if hasAnimation {
1826+ let iconWidth = animationSize + animationSpacing
1827+ let contentWidth = iconWidth + textView.frame.width
1828+ let startX = floor((bounds.width - contentWidth) / 2)
1829+ animationView.setFrameOrigin(NSMakePoint(startX, floor((bounds.height - animationSize) / 2)))
1830+ textView.setFrameOrigin(NSMakePoint(startX + iconWidth, floor((bounds.height - textView.frame.height) / 2)))
1831+ } else {
1832+ textView.center()
1833+ }
18011834 }
18021835}
18031836
@@ -7826,6 +7859,11 @@ open class ChatController<Context: AccountContextProtocol>: TelegramGenericViewC
78267859 self?.navigationController?.push(ChatController(context: context, chatLocation: .thread(result.message), mode: .preview, chatLocationContextHolder: result.contextHolder))
78277860 })
78287861 }
7862+ chatInteraction.showAnimatedToaster = { [weak self] animation, text in
7863+ guard let self else { return }
7864+ let attributed = NSAttributedString.initialize(string: text, color: self.genericView.presentationTextColor, font: .normal(.text))
7865+ self.genericView.showToaster(text: attributed, animation: animation, context: self.context, timeout: 3.0, animated: true)
7866+ }
78297867 chatInteraction.removeToaster = { [weak self] in
78307868 self?.genericView.removeToaster(animated: true)
78317869 }
0 commit comments