mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
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>
148 lines
8.3 KiB
Swift
148 lines
8.3 KiB
Swift
import Foundation
|
|
import Postbox
|
|
import TelegramApi
|
|
|
|
|
|
extension RichText {
|
|
init(apiText: Api.RichText) {
|
|
switch apiText {
|
|
case .textEmpty:
|
|
self = .empty
|
|
case let .textPlain(textPlainData):
|
|
let text = textPlainData.text
|
|
self = .plain(text)
|
|
case let .textBold(textBoldData):
|
|
let text = textBoldData.text
|
|
self = .bold(RichText(apiText: text))
|
|
case let .textItalic(textItalicData):
|
|
let text = textItalicData.text
|
|
self = .italic(RichText(apiText: text))
|
|
case let .textUnderline(textUnderlineData):
|
|
let text = textUnderlineData.text
|
|
self = .underline(RichText(apiText: text))
|
|
case let .textStrike(textStrikeData):
|
|
let text = textStrikeData.text
|
|
self = .strikethrough(RichText(apiText: text))
|
|
case let .textFixed(textFixedData):
|
|
let text = textFixedData.text
|
|
self = .fixed(RichText(apiText: text))
|
|
case let .textUrl(textUrlData):
|
|
let (text, url, webpageId) = (textUrlData.text, textUrlData.url, textUrlData.webpageId)
|
|
self = .url(text: RichText(apiText: text), url: url, webpageId: webpageId == 0 ? nil : MediaId(namespace: Namespaces.Media.CloudWebpage, id: webpageId))
|
|
case let .textEmail(textEmailData):
|
|
let (text, email) = (textEmailData.text, textEmailData.email)
|
|
self = .email(text: RichText(apiText: text), email: email)
|
|
case let .textConcat(textConcatData):
|
|
let texts = textConcatData.texts
|
|
self = .concat(texts.map({ RichText(apiText: $0) }))
|
|
case let .textSubscript(textSubscriptData):
|
|
let text = textSubscriptData.text
|
|
self = .subscript(RichText(apiText: text))
|
|
case let .textSuperscript(textSuperscriptData):
|
|
let text = textSuperscriptData.text
|
|
self = .superscript(RichText(apiText: text))
|
|
case let .textMarked(textMarkedData):
|
|
let text = textMarkedData.text
|
|
self = .marked(RichText(apiText: text))
|
|
case let .textPhone(textPhoneData):
|
|
let (text, phone) = (textPhoneData.text, textPhoneData.phone)
|
|
self = .phone(text: RichText(apiText: text), phone: phone)
|
|
case let .textImage(textImageData):
|
|
let (documentId, w, h) = (textImageData.documentId, textImageData.w, textImageData.h)
|
|
self = .image(id: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), dimensions: PixelDimensions(width: w, height: h))
|
|
case let .textAnchor(textAnchorData):
|
|
let (text, name) = (textAnchorData.text, textAnchorData.name)
|
|
self = .anchor(text: RichText(apiText: text), name: name)
|
|
case let .textCustomEmoji(data):
|
|
self = .textCustomEmoji(fileId: data.documentId, alt: data.alt)
|
|
case let .textMath(textMath):
|
|
self = .formula(latex: textMath.source)
|
|
case let .textAutoEmail(textAutoEmailData):
|
|
self = .textAutoEmail(text: RichText(apiText: textAutoEmailData.text))
|
|
case let .textAutoPhone(textAutoPhoneData):
|
|
self = .textAutoPhone(text: RichText(apiText: textAutoPhoneData.text))
|
|
case let .textAutoUrl(textAutoUrlData):
|
|
self = .textAutoUrl(text: RichText(apiText: textAutoUrlData.text))
|
|
case let .textBankCard(textBankCardData):
|
|
self = .textBankCard(text: RichText(apiText: textBankCardData.text))
|
|
case let .textBotCommand(textBotCommandData):
|
|
self = .textBotCommand(text: RichText(apiText: textBotCommandData.text))
|
|
case let .textCashtag(textCashtagData):
|
|
self = .textCashtag(text: RichText(apiText: textCashtagData.text))
|
|
case let .textDate(value):
|
|
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):
|
|
self = .textMention(text: RichText(apiText: textMentionData.text))
|
|
case let .textMentionName(textMentionNameData):
|
|
self = .textMentionName(text: RichText(apiText: textMentionNameData.text), peerId: textMentionNameData.userId)
|
|
case let .textSpoiler(textSpoilerData):
|
|
self = .textSpoiler(text: RichText(apiText: textSpoilerData.text))
|
|
}
|
|
}
|
|
|
|
func apiRichText() -> Api.RichText {
|
|
switch self {
|
|
case .empty:
|
|
return .textPlain(Api.RichText.Cons_textPlain(text: ""))
|
|
case let .plain(value):
|
|
return .textPlain(Api.RichText.Cons_textPlain(text: value))
|
|
case let .bold(value):
|
|
return .textBold(Api.RichText.Cons_textBold(text: value.apiRichText()))
|
|
case let .italic(value):
|
|
return .textItalic(Api.RichText.Cons_textItalic(text: value.apiRichText()))
|
|
case let .underline(value):
|
|
return .textUnderline(Api.RichText.Cons_textUnderline(text: value.apiRichText()))
|
|
case let .strikethrough(value):
|
|
return .textStrike(Api.RichText.Cons_textStrike(text: value.apiRichText()))
|
|
case let .fixed(value):
|
|
return .textFixed(Api.RichText.Cons_textFixed(text: value.apiRichText()))
|
|
case let .url(text, url, webpageId):
|
|
return .textUrl(Api.RichText.Cons_textUrl(text: text.apiRichText(), url: url, webpageId: webpageId?.id ?? 0))
|
|
case let .email(text, email):
|
|
return .textEmail(Api.RichText.Cons_textEmail(text: text.apiRichText(), email: email))
|
|
case let .concat(values):
|
|
return .textConcat(Api.RichText.Cons_textConcat(texts: values.map { $0.apiRichText() }))
|
|
case let .`subscript`(value):
|
|
return .textSubscript(Api.RichText.Cons_textSubscript(text: value.apiRichText()))
|
|
case let .superscript(value):
|
|
return .textSuperscript(Api.RichText.Cons_textSuperscript(text: value.apiRichText()))
|
|
case let .marked(value):
|
|
return .textMarked(Api.RichText.Cons_textMarked(text: value.apiRichText()))
|
|
case let .phone(text, phone):
|
|
return .textPhone(Api.RichText.Cons_textPhone(text: text.apiRichText(), phone: phone))
|
|
case let .image(id, dimensions):
|
|
return .textImage(Api.RichText.Cons_textImage(documentId: id.id, w: dimensions.width, h: dimensions.height))
|
|
case let .anchor(text, name):
|
|
return .textAnchor(Api.RichText.Cons_textAnchor(text: text.apiRichText(), name: name))
|
|
case let .formula(latex):
|
|
return .textMath(Api.RichText.Cons_textMath(source: latex))
|
|
case let .textCustomEmoji(fileId, alt):
|
|
return .textCustomEmoji(Api.RichText.Cons_textCustomEmoji(documentId: fileId, alt: alt))
|
|
case let .textAutoEmail(text):
|
|
return .textAutoEmail(Api.RichText.Cons_textAutoEmail(text: text.apiRichText()))
|
|
case let .textAutoPhone(text):
|
|
return .textAutoPhone(Api.RichText.Cons_textAutoPhone(text: text.apiRichText()))
|
|
case let .textAutoUrl(text):
|
|
return .textAutoUrl(Api.RichText.Cons_textAutoUrl(text: text.apiRichText()))
|
|
case let .textBankCard(text):
|
|
return .textBankCard(Api.RichText.Cons_textBankCard(text: text.apiRichText()))
|
|
case let .textBotCommand(text):
|
|
return .textBotCommand(Api.RichText.Cons_textBotCommand(text: text.apiRichText()))
|
|
case let .textCashtag(text):
|
|
return .textCashtag(Api.RichText.Cons_textCashtag(text: text.apiRichText()))
|
|
case let .textHashtag(text):
|
|
return .textHashtag(Api.RichText.Cons_textHashtag(text: text.apiRichText()))
|
|
case let .textMention(text):
|
|
return .textMention(Api.RichText.Cons_textMention(text: text.apiRichText()))
|
|
case let .textMentionName(text, peerId):
|
|
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))
|
|
}
|
|
}
|
|
}
|