mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Implement textDate autoformatter for InstantPage V2 rich bubbles
Render RichText.textDate (API constructor textDate(flags, text, date)) as a
locale/timezone-correct, relative-aware, tappable date inside V2 rich-message
bubbles, reusing the existing stringForEntityFormattedDate autoformatter.
- Model: add RichText.textDate(text📅format:) with Postbox coding,
FlatBuffers schema (RichText.fbs; flatc regenerates), Equatable and plainText;
parse flags->DateTimeFormat? at the API boundary (flags==0 => nil, matching
the messageEntityFormattedDate convention) with a symmetric apiRichText()
round-trip. The new case is handled in every exhaustive RichText switch
(SyncCore, ApiUtils, InstantPageTextItem, InstantPageAnchorPath,
InstantPagePreviewText, BrowserReadability, BrowserMarkdown).
- Render: attributedStringForRichText gains an optional formatDate closure;
the V2 layout builds it from the message's strings/dateTimeFormat and threads
it to every text-building call site, formatting the timestamp and tagging the
run with TelegramTextAttributes.Date.
- Tappable: the Date attribute is added to linkSelectionRects hit-testing and
mapped in the rich bubble's entityTapContent to the existing .date tap action
(opens the date context menu).
- Live update: the V2 layout accumulates a page-wide formattedDateUpdatePeriod
(>=10s) for relative dates; the rich bubble schedules a refresh timer on it,
recreating it only when the period changes. The currentPageLayout cache is
bypassed for relative-date pages so the timer-driven relayout actually
re-formats the date.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b912f26de
commit
a731f8bdc0
10 changed files with 172 additions and 59 deletions
|
|
@ -1091,6 +1091,8 @@ private func richTextIsEntityExpressible(_ text: RichText) -> Bool {
|
|||
return richTextIsEntityExpressible(inner)
|
||||
case .textMentionName(let inner, _):
|
||||
return richTextIsEntityExpressible(inner)
|
||||
case .textDate:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2153,7 +2155,7 @@ private func markdownDroppingPrefixLength(_ length: Int, from text: RichText) ->
|
|||
return dropped == .empty ? .empty : .anchor(text: dropped, name: name)
|
||||
case .textCustomEmoji:
|
||||
return text
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler, .textDate:
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
|
@ -2186,7 +2188,7 @@ private func markdownHasDisplayableContent(_ richText: RichText) -> Bool {
|
|||
return !latex.isEmpty
|
||||
case .textCustomEmoji:
|
||||
return true
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler, .textDate:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -2219,7 +2221,7 @@ private func markdownIsWhitespaceOnly(_ richText: RichText) -> Bool {
|
|||
return latex.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
case .textCustomEmoji:
|
||||
return false
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler, .textDate:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ private func trimStart(_ input: RichText) -> RichText {
|
|||
break
|
||||
case .textCustomEmoji:
|
||||
break
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler, .textDate:
|
||||
break
|
||||
}
|
||||
return text
|
||||
|
|
@ -454,7 +454,7 @@ private func trimEnd(_ input: RichText) -> RichText {
|
|||
break
|
||||
case .textCustomEmoji:
|
||||
break
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler, .textDate:
|
||||
break
|
||||
}
|
||||
return text
|
||||
|
|
@ -503,7 +503,7 @@ private func trim(_ input: RichText) -> RichText {
|
|||
break
|
||||
case .textCustomEmoji:
|
||||
break
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler, .textDate:
|
||||
break
|
||||
}
|
||||
return text
|
||||
|
|
@ -551,7 +551,7 @@ private func addNewLine(_ input: RichText) -> RichText {
|
|||
text = .concat([.formula(latex: latex), .plain("\n")])
|
||||
case .textCustomEmoji:
|
||||
break
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler, .textDate:
|
||||
break
|
||||
}
|
||||
return text
|
||||
|
|
|
|||
|
|
@ -149,5 +149,7 @@ private func richTextContainsAnchor(_ text: RichText, name: String) -> Bool {
|
|||
return richTextContainsAnchor(inner, name: name)
|
||||
case let .textMentionName(inner, _):
|
||||
return richTextContainsAnchor(inner, name: name)
|
||||
case let .textDate(inner, _, _):
|
||||
return richTextContainsAnchor(inner, name: name)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -474,7 +474,8 @@ public final class InstantPageTextItem: InstantPageItem {
|
|||
TelegramTextAttributes.PeerTextMention,
|
||||
TelegramTextAttributes.BotCommand,
|
||||
TelegramTextAttributes.Hashtag,
|
||||
TelegramTextAttributes.BankCard
|
||||
TelegramTextAttributes.BankCard,
|
||||
TelegramTextAttributes.Date
|
||||
]
|
||||
for key in interactiveKeys {
|
||||
let attrKey = NSAttributedString.Key(rawValue: key)
|
||||
|
|
@ -727,7 +728,7 @@ final class InstantPageScrollableTextItem: InstantPageScrollableItem {
|
|||
}
|
||||
}
|
||||
|
||||
func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack, url: InstantPageUrlItem? = nil, boundingWidth: CGFloat? = nil) -> NSAttributedString {
|
||||
func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack, url: InstantPageUrlItem? = nil, boundingWidth: CGFloat? = nil, formatDate: ((Int32, MessageTextEntityType.DateTimeFormat) -> String)? = nil) -> NSAttributedString {
|
||||
switch text {
|
||||
case .empty:
|
||||
return NSAttributedString(string: "", attributes: styleStack.textAttributes())
|
||||
|
|
@ -739,67 +740,67 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
return NSAttributedString(string: string, attributes: attributes)
|
||||
case let .bold(text):
|
||||
styleStack.push(.bold)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .italic(text):
|
||||
styleStack.push(.italic)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .underline(text):
|
||||
styleStack.push(.underline)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .strikethrough(text):
|
||||
styleStack.push(.strikethrough)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .fixed(text):
|
||||
styleStack.push(.fontFixed(true))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .url(text, url, webpageId):
|
||||
styleStack.push(.link(webpageId != nil))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: url, webpageId: webpageId))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: url, webpageId: webpageId), formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .email(text, email):
|
||||
styleStack.push(.bold)
|
||||
styleStack.push(.underline)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "mailto:\(email)", webpageId: nil))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "mailto:\(email)", webpageId: nil), formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .concat(texts):
|
||||
let string = NSMutableAttributedString()
|
||||
for text in texts {
|
||||
let substring = attributedStringForRichText(text, styleStack: styleStack, url: url, boundingWidth: boundingWidth)
|
||||
let substring = attributedStringForRichText(text, styleStack: styleStack, url: url, boundingWidth: boundingWidth, formatDate: formatDate)
|
||||
string.append(substring)
|
||||
}
|
||||
return string
|
||||
case let .subscript(text):
|
||||
styleStack.push(.subscript)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .superscript(text):
|
||||
styleStack.push(.superscript)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .marked(text):
|
||||
styleStack.push(.marker)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .phone(text, phone):
|
||||
styleStack.push(.bold)
|
||||
styleStack.push(.underline)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "tel:\(phone)", webpageId: nil))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "tel:\(phone)", webpageId: nil), formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
styleStack.pop()
|
||||
return result
|
||||
|
|
@ -829,7 +830,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
})
|
||||
let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)
|
||||
let attrDictionaryDelegate = [(kCTRunDelegateAttributeName as NSAttributedString.Key): (delegate as Any), NSAttributedString.Key(rawValue: InstantPageMediaIdAttribute): id.id, NSAttributedString.Key(rawValue: InstantPageMediaDimensionsAttribute): dimensions]
|
||||
let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString
|
||||
let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url, formatDate: formatDate).mutableCopy() as! NSMutableAttributedString
|
||||
mutableAttributedString.addAttributes(attrDictionaryDelegate, range: NSMakeRange(0, mutableAttributedString.length))
|
||||
return mutableAttributedString
|
||||
case let .formula(latex):
|
||||
|
|
@ -864,7 +865,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
return data.pointee.width
|
||||
})
|
||||
let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)
|
||||
let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString
|
||||
let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url, formatDate: formatDate).mutableCopy() as! NSMutableAttributedString
|
||||
mutableAttributedString.addAttributes([
|
||||
kCTRunDelegateAttributeName as NSAttributedString.Key: delegate as Any,
|
||||
NSAttributedString.Key(rawValue: InstantPageFormulaAttribute): attachment
|
||||
|
|
@ -877,29 +878,29 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
empty = true
|
||||
text = .plain("\u{200b}")
|
||||
}
|
||||
let anchorText = !empty ? attributedStringForRichText(text, styleStack: styleStack, url: url) : nil
|
||||
let anchorText = !empty ? attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate) : nil
|
||||
styleStack.push(.anchor(name, anchorText, empty))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .textAutoUrl(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: text.plainText, webpageId: nil))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: text.plainText, webpageId: nil), formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .textAutoEmail(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "mailto:\(text.plainText)", webpageId: nil))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "mailto:\(text.plainText)", webpageId: nil), formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .textAutoPhone(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "tel:\(text.plainText)", webpageId: nil))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "tel:\(text.plainText)", webpageId: nil), formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .textMention(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
|
|
@ -908,7 +909,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
return mutable
|
||||
case let .textMentionName(text, peerId):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
|
|
@ -918,7 +919,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
return mutable
|
||||
case let .textHashtag(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
|
|
@ -927,7 +928,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
return mutable
|
||||
case let .textCashtag(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
|
|
@ -936,7 +937,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
return mutable
|
||||
case let .textBotCommand(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
|
|
@ -945,7 +946,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
return mutable
|
||||
case let .textBankCard(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
|
|
@ -981,19 +982,31 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
})
|
||||
let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)
|
||||
let emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil)
|
||||
let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString
|
||||
let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url, formatDate: formatDate).mutableCopy() as! NSMutableAttributedString
|
||||
mutableAttributedString.addAttributes([
|
||||
kCTRunDelegateAttributeName as NSAttributedString.Key: delegate as Any,
|
||||
ChatTextInputAttributes.customEmoji: emojiAttribute
|
||||
], range: NSMakeRange(0, mutableAttributedString.length))
|
||||
return mutableAttributedString
|
||||
case let .textSpoiler(text):
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler), value: true, range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
case let .textDate(text, date, format):
|
||||
if let format, let formatDate {
|
||||
let formatted = formatDate(date, format)
|
||||
let result = attributedStringForRichText(.plain(formatted), styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Date), value: date, range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
} else {
|
||||
return attributedStringForRichText(text, styleStack: styleStack, url: url, formatDate: formatDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ public struct InstantPageV2Layout {
|
|||
/// but no fetch signal can be bound (image view simply isn't created).
|
||||
public let webpage: TelegramMediaWebpage?
|
||||
|
||||
/// Set by `layoutInstantPageV2` when the page contains at least one `.relative` `textDate`.
|
||||
/// The minimum refresh period (seconds, >=10) across all relative dates; the rich-data bubble
|
||||
/// schedules a timer on it to keep "N minutes ago" fresh. nil => no relative date => no timer.
|
||||
public var formattedDateUpdatePeriod: Int32? = nil
|
||||
|
||||
public init(contentSize: CGSize, items: [InstantPageV2LaidOutItem], detailsIndices: [Int], media: [EngineMedia.Id: EngineMedia] = [:], webpage: TelegramMediaWebpage? = nil) {
|
||||
self.contentSize = contentSize
|
||||
self.items = items
|
||||
|
|
@ -395,10 +400,23 @@ public func layoutInstantPageV2(
|
|||
media[id] = .file(video)
|
||||
}
|
||||
|
||||
let dateAccumulator = DateUpdateAccumulator()
|
||||
let formatDate: (Int32, MessageTextEntityType.DateTimeFormat) -> String = { timestamp, format in
|
||||
if case .relative = format {
|
||||
let now = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
let age = abs(now - timestamp)
|
||||
// Cap the fastest bucket at 10s (the message-entity reference uses 1s for <120s).
|
||||
let period: Int32 = age < 120 ? 10 : (age <= 60 * 60 ? 60 : 30 * 60)
|
||||
dateAccumulator.period = dateAccumulator.period.map { min($0, period) } ?? period
|
||||
}
|
||||
return stringForEntityFormattedDate(timestamp: timestamp, format: format, strings: strings, dateTimeFormat: dateTimeFormat)
|
||||
}
|
||||
|
||||
var context = LayoutContext(
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
dateTimeFormat: dateTimeFormat,
|
||||
formatDate: formatDate,
|
||||
userLocation: userLocation,
|
||||
webpage: webpage,
|
||||
media: media,
|
||||
|
|
@ -411,13 +429,15 @@ public func layoutInstantPageV2(
|
|||
expandedDetails: expandedDetails
|
||||
)
|
||||
|
||||
return layoutBlockSequence(
|
||||
var result = layoutBlockSequence(
|
||||
instantPage.blocks,
|
||||
boundingWidth: boundingWidth,
|
||||
horizontalInset: horizontalInset,
|
||||
kind: .topLevel,
|
||||
context: &context
|
||||
)
|
||||
result.formattedDateUpdatePeriod = dateAccumulator.period
|
||||
return result
|
||||
}
|
||||
|
||||
/// Used by `ChatMessageRichDataBubbleContentNode` to anchor the date/checks status node at the
|
||||
|
|
@ -503,10 +523,15 @@ public func lastTextLineFrameIfLastItemIsText(in layout: InstantPageV2Layout) ->
|
|||
|
||||
// MARK: - Layout context
|
||||
|
||||
private final class DateUpdateAccumulator {
|
||||
var period: Int32?
|
||||
}
|
||||
|
||||
private struct LayoutContext {
|
||||
let theme: InstantPageTheme
|
||||
let strings: PresentationStrings
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let formatDate: (Int32, MessageTextEntityType.DateTimeFormat) -> String
|
||||
let userLocation: MediaResourceUserLocation
|
||||
let webpage: TelegramMediaWebpage
|
||||
let media: [EngineMedia.Id: EngineMedia]
|
||||
|
|
@ -1069,7 +1094,7 @@ private func layoutDetails(
|
|||
let titleStyleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(titleStyleStack, theme: context.theme, category: .paragraph, link: false)
|
||||
let (titleTextItem, _, _) = layoutTextItem(
|
||||
attributedStringForRichText(title, styleStack: titleStyleStack),
|
||||
attributedStringForRichText(title, styleStack: titleStyleStack, formatDate: context.formatDate),
|
||||
boundingWidth: boundingWidth - horizontalInset * 2.0 - 32.0, // reserve right edge for chevron
|
||||
offset: CGPoint(x: 0.0, y: 0.0),
|
||||
fitToWidth: context.fitToWidth,
|
||||
|
|
@ -1186,7 +1211,7 @@ private func layoutTable(
|
|||
// boundingWidth sizes inline attachments to `cellWidthLimit - totalCellPadding`, while
|
||||
// the line-break budget passed to `layoutTextItem` is the full `cellWidthLimit`. (V1
|
||||
// subtracts `totalCellPadding` only on the attribute-string arg, not the layout arg.)
|
||||
let attrStr = attributedStringForRichText(text, styleStack: styleStack, boundingWidth: cellWidthLimit - totalCellPadding)
|
||||
let attrStr = attributedStringForRichText(text, styleStack: styleStack, boundingWidth: cellWidthLimit - totalCellPadding, formatDate: context.formatDate)
|
||||
if let shortestItem = layoutTextItem(
|
||||
attrStr,
|
||||
boundingWidth: cellWidthLimit,
|
||||
|
|
@ -1672,7 +1697,7 @@ private func layoutCaptionAndCredit(
|
|||
y += 14.0
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: .caption, link: false)
|
||||
let attributedString = attributedStringForRichText(caption.text, styleStack: styleStack)
|
||||
let attributedString = attributedStringForRichText(caption.text, styleStack: styleStack, formatDate: context.formatDate)
|
||||
let (textItem, captionItems, captionSize) = layoutTextItem(
|
||||
attributedString,
|
||||
boundingWidth: boundingWidth - horizontalInset * 2.0,
|
||||
|
|
@ -1698,7 +1723,7 @@ private func layoutCaptionAndCredit(
|
|||
}
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: .credit, link: false)
|
||||
let attributedString = attributedStringForRichText(caption.credit, styleStack: styleStack)
|
||||
let attributedString = attributedStringForRichText(caption.credit, styleStack: styleStack, formatDate: context.formatDate)
|
||||
let (_, creditItems, creditSize) = layoutTextItem(
|
||||
attributedString,
|
||||
boundingWidth: boundingWidth - horizontalInset * 2.0,
|
||||
|
|
@ -2009,7 +2034,7 @@ private func layoutSimpleText(
|
|||
) -> [InstantPageV2LaidOutItem] {
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: category, link: false)
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack)
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack, formatDate: context.formatDate)
|
||||
let (_, items, _) = layoutTextItem(
|
||||
attributedString,
|
||||
boundingWidth: boundingWidth - horizontalInset * 2.0,
|
||||
|
|
@ -2029,7 +2054,7 @@ private func layoutHeading(
|
|||
) -> [InstantPageV2LaidOutItem] {
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, attributes: context.theme.headingTextAttributes(level: level, link: false))
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack)
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack, formatDate: context.formatDate)
|
||||
let (_, items, _) = layoutTextItem(
|
||||
attributedString,
|
||||
boundingWidth: boundingWidth - horizontalInset * 2.0,
|
||||
|
|
@ -2053,7 +2078,7 @@ private func layoutParagraph(
|
|||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: kind == .cell ? .table : .paragraph, link: false)
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack)
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack, formatDate: context.formatDate)
|
||||
|
||||
let (_, items, _) = layoutTextItem(
|
||||
attributedString,
|
||||
|
|
@ -2125,7 +2150,7 @@ private func layoutAuthorDate(
|
|||
let alignment: NSTextAlignment = (context.rtl || previousItemHasRTL) ? .right : .natural
|
||||
|
||||
let (_, items, _) = layoutTextItem(
|
||||
attributedStringForRichText(resolvedText, styleStack: styleStack),
|
||||
attributedStringForRichText(resolvedText, styleStack: styleStack, formatDate: context.formatDate),
|
||||
boundingWidth: boundingWidth - horizontalInset * 2.0,
|
||||
alignment: alignment,
|
||||
offset: CGPoint(x: horizontalInset, y: 0.0),
|
||||
|
|
@ -2175,7 +2200,7 @@ private func layoutCodeBlock(
|
|||
// V1 lines 335–338: fall back to plain paragraph style when no language.
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: .codeBlock, link: false)
|
||||
attributedString = attributedStringForRichText(text, styleStack: styleStack)
|
||||
attributedString = attributedStringForRichText(text, styleStack: styleStack, formatDate: context.formatDate)
|
||||
}
|
||||
|
||||
// V1 line 341: text bounding width excludes horizontalInset×2 and backgroundInset×2.
|
||||
|
|
@ -2236,7 +2261,7 @@ private func layoutThinking(
|
|||
)
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, attributes: dimmedAttributes)
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack)
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack, formatDate: context.formatDate)
|
||||
|
||||
// Mirror a normal `.text` item's sizing: lay the text out flush (offset .zero) and put the page
|
||||
// inset onto the BLOCK frame, so the `.thinking` item's frame == a `.text` item's frame
|
||||
|
|
@ -2315,7 +2340,7 @@ private func layoutBlockQuote(
|
|||
contentHeight += 14.0
|
||||
let captionStyleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(captionStyleStack, theme: context.theme, category: .caption, link: false)
|
||||
let attributedCaption = attributedStringForRichText(caption, styleStack: captionStyleStack)
|
||||
let attributedCaption = attributedStringForRichText(caption, styleStack: captionStyleStack, formatDate: context.formatDate)
|
||||
let (_, captionItems, captionSize) = layoutTextItem(
|
||||
attributedCaption,
|
||||
boundingWidth: innerBoundingWidth,
|
||||
|
|
@ -2386,7 +2411,7 @@ private func layoutQuoteText(
|
|||
let textX: CGFloat = horizontalInset + lineInset
|
||||
let textAlignment: NSTextAlignment = isPull ? .center : .natural
|
||||
|
||||
let attributedBody = attributedStringForRichText(text, styleStack: styleStack)
|
||||
let attributedBody = attributedStringForRichText(text, styleStack: styleStack, formatDate: context.formatDate)
|
||||
let (_, bodyItems, bodySize) = layoutTextItem(
|
||||
attributedBody,
|
||||
boundingWidth: textBoundingWidth,
|
||||
|
|
@ -2407,7 +2432,7 @@ private func layoutQuoteText(
|
|||
let captionStyleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(captionStyleStack, theme: context.theme, category: .caption, link: false)
|
||||
|
||||
let attributedCaption = attributedStringForRichText(caption, styleStack: captionStyleStack)
|
||||
let attributedCaption = attributedStringForRichText(caption, styleStack: captionStyleStack, formatDate: context.formatDate)
|
||||
let (_, captionItems, captionSize) = layoutTextItem(
|
||||
attributedCaption,
|
||||
boundingWidth: textBoundingWidth,
|
||||
|
|
@ -2522,7 +2547,7 @@ private func layoutList(
|
|||
// Measure using a UILabel to get the expected label width.
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: .paragraph, link: false)
|
||||
let attrStr = attributedStringForRichText(.plain(value), styleStack: styleStack)
|
||||
let attrStr = attributedStringForRichText(.plain(value), styleStack: styleStack, formatDate: context.formatDate)
|
||||
let (textItem, _, _) = layoutTextItem(
|
||||
attrStr,
|
||||
boundingWidth: boundingWidth - horizontalInset * 2.0,
|
||||
|
|
@ -2586,7 +2611,7 @@ private func layoutList(
|
|||
// Layout text content.
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: .paragraph, link: false)
|
||||
let attrStr = attributedStringForRichText(text, styleStack: styleStack)
|
||||
let attrStr = attributedStringForRichText(text, styleStack: styleStack, formatDate: context.formatDate)
|
||||
let textX = horizontalInset + indexSpacing + maxIndexWidth
|
||||
let textWidth = boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth
|
||||
let (textItem, textLaidOutItems, textSize) = layoutTextItem(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ union RichText_Value {
|
|||
RichText_Hashtag,
|
||||
RichText_Mention,
|
||||
RichText_MentionName,
|
||||
RichText_Spoiler
|
||||
RichText_Spoiler,
|
||||
RichText_Date
|
||||
}
|
||||
|
||||
table RichText {
|
||||
|
|
@ -155,3 +156,9 @@ table RichText_MentionName {
|
|||
table RichText_Spoiler {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_Date {
|
||||
text:RichText (id: 0, required);
|
||||
date:int (id: 1);
|
||||
format:int = -1 (id: 2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,9 +70,8 @@ extension RichText {
|
|||
case let .textCashtag(textCashtagData):
|
||||
self = .textCashtag(text: RichText(apiText: textCashtagData.text))
|
||||
case let .textDate(value):
|
||||
let _ = value
|
||||
//TODO:localize
|
||||
self = .plain("")
|
||||
let format: MessageTextEntityType.DateTimeFormat? = value.flags == 0 ? nil : MessageTextEntityType.DateTimeFormat(rawValue: value.flags)
|
||||
self = .textDate(text: RichText(apiText: value.text), date: value.date, format: format)
|
||||
case let .textHashtag(textHashtagData):
|
||||
self = .textHashtag(text: RichText(apiText: textHashtagData.text))
|
||||
case let .textMention(textMentionData):
|
||||
|
|
@ -142,6 +141,8 @@ extension RichText {
|
|||
return .textMentionName(Api.RichText.Cons_textMentionName(text: text.apiRichText(), userId: peerId))
|
||||
case let .textSpoiler(text):
|
||||
return .textSpoiler(Api.RichText.Cons_textSpoiler(text: text.apiRichText()))
|
||||
case let .textDate(text, date, format):
|
||||
return .textDate(Api.RichText.Cons_textDate(flags: format?.rawValue ?? 0, text: text.apiRichText(), date: date))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ private enum RichTextTypes: Int32 {
|
|||
case textMention = 25
|
||||
case textMentionName = 26
|
||||
case textSpoiler = 27
|
||||
case textDate = 28
|
||||
}
|
||||
|
||||
public indirect enum RichText: PostboxCoding, Equatable {
|
||||
|
|
@ -62,6 +63,7 @@ public indirect enum RichText: PostboxCoding, Equatable {
|
|||
case textMention(text: RichText)
|
||||
case textMentionName(text: RichText, peerId: Int64)
|
||||
case textSpoiler(text: RichText)
|
||||
case textDate(text: RichText, date: Int32, format: MessageTextEntityType.DateTimeFormat?)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
switch decoder.decodeInt32ForKey("r", orElse: 0) {
|
||||
|
|
@ -127,6 +129,8 @@ public indirect enum RichText: PostboxCoding, Equatable {
|
|||
self = .textMentionName(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, peerId: decoder.decodeInt64ForKey("mn.p", orElse: 0))
|
||||
case RichTextTypes.textSpoiler.rawValue:
|
||||
self = .textSpoiler(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textDate.rawValue:
|
||||
self = .textDate(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, date: decoder.decodeInt32ForKey("dt", orElse: 0), format: decoder.decodeOptionalInt32ForKey("df").flatMap { MessageTextEntityType.DateTimeFormat(rawValue: $0) })
|
||||
default:
|
||||
self = .empty
|
||||
}
|
||||
|
|
@ -233,9 +237,18 @@ public indirect enum RichText: PostboxCoding, Equatable {
|
|||
case let .textSpoiler(text):
|
||||
encoder.encodeInt32(RichTextTypes.textSpoiler.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textDate(text, date, format):
|
||||
encoder.encodeInt32(RichTextTypes.textDate.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
encoder.encodeInt32(date, forKey: "dt")
|
||||
if let format {
|
||||
encoder.encodeInt32(format.rawValue, forKey: "df")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "df")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static func ==(lhs: RichText, rhs: RichText) -> Bool {
|
||||
switch lhs {
|
||||
case .empty:
|
||||
|
|
@ -366,6 +379,8 @@ public indirect enum RichText: PostboxCoding, Equatable {
|
|||
if case let .textMentionName(rhsText, rhsPeerId) = rhs, lhsText == rhsText, lhsPeerId == rhsPeerId { return true } else { return false }
|
||||
case let .textSpoiler(text):
|
||||
if case .textSpoiler(text) = rhs { return true } else { return false }
|
||||
case let .textDate(lhsText, lhsDate, lhsFormat):
|
||||
if case let .textDate(rhsText, rhsDate, rhsFormat) = rhs, lhsText == rhsText, lhsDate == rhsDate, lhsFormat == rhsFormat { return true } else { return false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -433,6 +448,8 @@ public extension RichText {
|
|||
return text.plainText
|
||||
case let .textSpoiler(text):
|
||||
return text.plainText
|
||||
case let .textDate(text, _, _):
|
||||
return text.plainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -580,6 +597,12 @@ extension RichText {
|
|||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textSpoiler(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextDate:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Date.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
let formatValue = value.format
|
||||
self = .textDate(text: try RichText(flatBuffersObject: value.text), date: value.date, format: formatValue == -1 ? nil : MessageTextEntityType.DateTimeFormat(rawValue: formatValue))
|
||||
case .none_:
|
||||
self = .empty
|
||||
}
|
||||
|
|
@ -770,6 +793,14 @@ extension RichText {
|
|||
let start = TelegramCore_RichText_Spoiler.startRichText_Spoiler(&builder)
|
||||
TelegramCore_RichText_Spoiler.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_Spoiler.endRichText_Spoiler(&builder, start: start)
|
||||
case let .textDate(text, date, format):
|
||||
valueType = .richtextDate
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_Date.startRichText_Date(&builder)
|
||||
TelegramCore_RichText_Date.add(text: textOffset, &builder)
|
||||
TelegramCore_RichText_Date.add(date: date, &builder)
|
||||
TelegramCore_RichText_Date.add(format: format?.rawValue ?? -1, &builder)
|
||||
offset = TelegramCore_RichText_Date.endRichText_Date(&builder, start: start)
|
||||
}
|
||||
|
||||
return TelegramCore_RichText.createRichText(&builder, valueType: valueType, valueOffset: offset)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ extension RichText {
|
|||
return "Fx"
|
||||
case let .textCustomEmoji(_, alt):
|
||||
return alt
|
||||
case let .textAutoEmail(value), let .textAutoPhone(value), let .textAutoUrl(value), let .textBankCard(value), let .textBotCommand(value), let .textCashtag(value), let .textHashtag(value), let .textMention(value), let .textMentionName(value, _), let .textSpoiler(value):
|
||||
case let .textAutoEmail(value), let .textAutoPhone(value), let .textAutoUrl(value), let .textBankCard(value), let .textBotCommand(value), let .textCashtag(value), let .textHashtag(value), let .textMention(value), let .textMentionName(value, _), let .textSpoiler(value), let .textDate(value, _, _):
|
||||
return value.previewText()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
// to request a full bubble re-layout (so the bubble grows with the reveal).
|
||||
private var lastAppliedRevealedCount: Int = 0
|
||||
private var displayContentsUnderSpoilers: Bool = false
|
||||
private var relativeDateTimer: (timer: SwiftSignalKit.Timer, period: Int32)?
|
||||
|
||||
override public var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
|
|
@ -177,6 +178,7 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
|
||||
deinit {
|
||||
self.linkProgressDisposable?.dispose()
|
||||
self.relativeDateTimer?.timer.invalidate()
|
||||
}
|
||||
|
||||
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))) {
|
||||
|
|
@ -360,7 +362,14 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
current.boundingWidth == suggestedBoundingWidth,
|
||||
current.presentationThemeIdentity == presentationThemeIdentity,
|
||||
current.expandedDetails == currentExpandedDetails,
|
||||
current.messageStableVersion == currentMessageStableVersion {
|
||||
current.messageStableVersion == currentMessageStableVersion,
|
||||
current.layout.formattedDateUpdatePeriod == nil {
|
||||
// Reuse the cached layout only when it has no relative `textDate`. A relative
|
||||
// date's formatted string ("N minutes ago") is baked into the laid-out text at
|
||||
// layout time, and none of the cache-key inputs change as wall-clock advances —
|
||||
// so reusing it would freeze the date and defeat the refresh timer (which fires
|
||||
// `requestFullUpdate` precisely to re-run `layoutInstantPageV2` → `formatDate`).
|
||||
// Forcing a recompute for relative-date pages keeps the timer's tick visible.
|
||||
pageLayout = current.layout
|
||||
} else {
|
||||
#if DEBUG && false
|
||||
|
|
@ -705,6 +714,26 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
self.pageViewMessageKey = nil
|
||||
}
|
||||
|
||||
if let formattedDateUpdatePeriod = pageLayout?.formattedDateUpdatePeriod {
|
||||
// Recreate the timer only when the period changes — unlike the TextBubble
|
||||
// reference (ChatMessageTextBubbleContentNode), which rebuilds it every apply.
|
||||
// The timer fires `requestFullUpdate`, which relays out and re-enters here; at
|
||||
// a steady period this guard is false, so the running timer keeps its schedule
|
||||
// instead of being reallocated (no per-apply churn, no firing-phase reset, no
|
||||
// self-trigger loop). Do not "simplify" this to match the reference.
|
||||
if self.relativeDateTimer?.period != formattedDateUpdatePeriod {
|
||||
self.relativeDateTimer?.timer.invalidate()
|
||||
let timer = SwiftSignalKit.Timer(timeout: Double(formattedDateUpdatePeriod), repeat: true, completion: { [weak self] in
|
||||
self?.requestFullUpdate?(ControlledTransition(duration: 0.15, curve: .easeInOut, interactive: false))
|
||||
}, queue: Queue.mainQueue())
|
||||
self.relativeDateTimer = (timer, formattedDateUpdatePeriod)
|
||||
timer.start()
|
||||
}
|
||||
} else if let (timer, _) = self.relativeDateTimer {
|
||||
self.relativeDateTimer = nil
|
||||
timer.invalidate()
|
||||
}
|
||||
|
||||
// === Streaming state apply ===
|
||||
|
||||
// 1. Compute / cache the cost map.
|
||||
|
|
@ -965,6 +994,9 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
return .hashtag(hashtag.peerName, hashtag.hashtag)
|
||||
} else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
|
||||
return .bankCard(bankCard)
|
||||
} else if let date = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Date)] as? Int32 {
|
||||
// The displayed string is unused downstream (ChatMessageBubbleItemNode matches `.date(date, _)`).
|
||||
return .date(date, "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue