RichText: entity cases (mention / hashtag / cashtag / bot command / bank card / auto link)

Implement the nine message-entity RichText cases (textMention,
textMentionName, textHashtag, textCashtag, textBotCommand, textBankCard,
textAutoUrl, textAutoEmail, textAutoPhone): model + Postbox coding,
FlatBuffers schema/codec, Api.RichText parsing/serialization (lossless),
InstantPage display attaching the matching TelegramTextAttributes keys, and
tap routing in the rich-data bubble mirroring ChatMessageTextBubbleContentNode.
mentionName display resolves via EnginePeer.Id (PeerId not in scope).

Also lets rich-data text selection reach a line's trailing edge and fixes
the date/status node positioning to match TextBubble.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
isaac
2026-05-23 22:12:47 +04:00
co-authored by Claude Opus 4.7
parent b48b96ecd9
commit 99e4c41b24
13 changed files with 558 additions and 78 deletions
@@ -21,6 +21,15 @@ private enum RichTextTypes: Int32 {
case anchor = 15
case formula = 16
case textCustomEmoji = 17
case textAutoEmail = 18
case textAutoPhone = 19
case textAutoUrl = 20
case textBankCard = 21
case textBotCommand = 22
case textCashtag = 23
case textHashtag = 24
case textMention = 25
case textMentionName = 26
}
public indirect enum RichText: PostboxCoding, Equatable {
@@ -42,6 +51,15 @@ public indirect enum RichText: PostboxCoding, Equatable {
case anchor(text: RichText, name: String)
case formula(latex: String)
case textCustomEmoji(fileId: Int64, alt: String)
case textAutoEmail(text: RichText)
case textAutoPhone(text: RichText)
case textAutoUrl(text: RichText)
case textBankCard(text: RichText)
case textBotCommand(text: RichText)
case textCashtag(text: RichText)
case textHashtag(text: RichText)
case textMention(text: RichText)
case textMentionName(text: RichText, peerId: Int64)
public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("r", orElse: 0) {
@@ -87,6 +105,24 @@ public indirect enum RichText: PostboxCoding, Equatable {
self = .formula(latex: decoder.decodeStringForKey("l", orElse: ""))
case RichTextTypes.textCustomEmoji.rawValue:
self = .textCustomEmoji(fileId: decoder.decodeInt64ForKey("ce.f", orElse: 0), alt: decoder.decodeStringForKey("ce.a", orElse: ""))
case RichTextTypes.textAutoEmail.rawValue:
self = .textAutoEmail(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case RichTextTypes.textAutoPhone.rawValue:
self = .textAutoPhone(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case RichTextTypes.textAutoUrl.rawValue:
self = .textAutoUrl(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case RichTextTypes.textBankCard.rawValue:
self = .textBankCard(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case RichTextTypes.textBotCommand.rawValue:
self = .textBotCommand(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case RichTextTypes.textCashtag.rawValue:
self = .textCashtag(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case RichTextTypes.textHashtag.rawValue:
self = .textHashtag(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case RichTextTypes.textMention.rawValue:
self = .textMention(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case RichTextTypes.textMentionName.rawValue:
self = .textMentionName(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, peerId: decoder.decodeInt64ForKey("mn.p", orElse: 0))
default:
self = .empty
}
@@ -162,6 +198,34 @@ public indirect enum RichText: PostboxCoding, Equatable {
encoder.encodeInt32(RichTextTypes.textCustomEmoji.rawValue, forKey: "r")
encoder.encodeInt64(fileId, forKey: "ce.f")
encoder.encodeString(alt, forKey: "ce.a")
case let .textAutoEmail(text):
encoder.encodeInt32(RichTextTypes.textAutoEmail.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
case let .textAutoPhone(text):
encoder.encodeInt32(RichTextTypes.textAutoPhone.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
case let .textAutoUrl(text):
encoder.encodeInt32(RichTextTypes.textAutoUrl.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
case let .textBankCard(text):
encoder.encodeInt32(RichTextTypes.textBankCard.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
case let .textBotCommand(text):
encoder.encodeInt32(RichTextTypes.textBotCommand.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
case let .textCashtag(text):
encoder.encodeInt32(RichTextTypes.textCashtag.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
case let .textHashtag(text):
encoder.encodeInt32(RichTextTypes.textHashtag.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
case let .textMention(text):
encoder.encodeInt32(RichTextTypes.textMention.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
case let .textMentionName(text, peerId):
encoder.encodeInt32(RichTextTypes.textMentionName.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t")
encoder.encodeInt64(peerId, forKey: "mn.p")
}
}
@@ -275,6 +339,24 @@ public indirect enum RichText: PostboxCoding, Equatable {
} else {
return false
}
case let .textAutoEmail(text):
if case .textAutoEmail(text) = rhs { return true } else { return false }
case let .textAutoPhone(text):
if case .textAutoPhone(text) = rhs { return true } else { return false }
case let .textAutoUrl(text):
if case .textAutoUrl(text) = rhs { return true } else { return false }
case let .textBankCard(text):
if case .textBankCard(text) = rhs { return true } else { return false }
case let .textBotCommand(text):
if case .textBotCommand(text) = rhs { return true } else { return false }
case let .textCashtag(text):
if case .textCashtag(text) = rhs { return true } else { return false }
case let .textHashtag(text):
if case .textHashtag(text) = rhs { return true } else { return false }
case let .textMention(text):
if case .textMention(text) = rhs { return true } else { return false }
case let .textMentionName(lhsText, lhsPeerId):
if case let .textMentionName(rhsText, rhsPeerId) = rhs, lhsText == rhsText, lhsPeerId == rhsPeerId { return true } else { return false }
}
}
}
@@ -322,6 +404,24 @@ public extension RichText {
return latex
case let .textCustomEmoji(_, alt):
return alt
case let .textAutoEmail(text):
return text.plainText
case let .textAutoPhone(text):
return text.plainText
case let .textAutoUrl(text):
return text.plainText
case let .textBankCard(text):
return text.plainText
case let .textBotCommand(text):
return text.plainText
case let .textCashtag(text):
return text.plainText
case let .textHashtag(text):
return text.plainText
case let .textMention(text):
return text.plainText
case let .textMentionName(text, _):
return text.plainText
}
}
}
@@ -419,6 +519,51 @@ extension RichText {
throw FlatBuffersError.missingRequiredField()
}
self = .textCustomEmoji(fileId: value.fileId, alt: value.alt)
case .richtextAutoemail:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_AutoEmail.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textAutoEmail(text: try RichText(flatBuffersObject: value.text))
case .richtextAutophone:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_AutoPhone.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textAutoPhone(text: try RichText(flatBuffersObject: value.text))
case .richtextAutourl:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_AutoUrl.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textAutoUrl(text: try RichText(flatBuffersObject: value.text))
case .richtextBankcard:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_BankCard.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textBankCard(text: try RichText(flatBuffersObject: value.text))
case .richtextBotcommand:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_BotCommand.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textBotCommand(text: try RichText(flatBuffersObject: value.text))
case .richtextCashtag:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Cashtag.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textCashtag(text: try RichText(flatBuffersObject: value.text))
case .richtextHashtag:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Hashtag.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textHashtag(text: try RichText(flatBuffersObject: value.text))
case .richtextMention:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Mention.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textMention(text: try RichText(flatBuffersObject: value.text))
case .richtextMentionname:
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_MentionName.self) else {
throw FlatBuffersError.missingRequiredField()
}
self = .textMentionName(text: try RichText(flatBuffersObject: value.text), peerId: value.peerId)
case .none_:
self = .empty
}
@@ -548,6 +693,61 @@ extension RichText {
TelegramCore_RichText_CustomEmoji.add(fileId: fileId, &builder)
TelegramCore_RichText_CustomEmoji.add(alt: altOffset, &builder)
offset = TelegramCore_RichText_CustomEmoji.endRichText_CustomEmoji(&builder, start: start)
case let .textAutoEmail(text):
valueType = .richtextAutoemail
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_AutoEmail.startRichText_AutoEmail(&builder)
TelegramCore_RichText_AutoEmail.add(text: textOffset, &builder)
offset = TelegramCore_RichText_AutoEmail.endRichText_AutoEmail(&builder, start: start)
case let .textAutoPhone(text):
valueType = .richtextAutophone
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_AutoPhone.startRichText_AutoPhone(&builder)
TelegramCore_RichText_AutoPhone.add(text: textOffset, &builder)
offset = TelegramCore_RichText_AutoPhone.endRichText_AutoPhone(&builder, start: start)
case let .textAutoUrl(text):
valueType = .richtextAutourl
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_AutoUrl.startRichText_AutoUrl(&builder)
TelegramCore_RichText_AutoUrl.add(text: textOffset, &builder)
offset = TelegramCore_RichText_AutoUrl.endRichText_AutoUrl(&builder, start: start)
case let .textBankCard(text):
valueType = .richtextBankcard
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_BankCard.startRichText_BankCard(&builder)
TelegramCore_RichText_BankCard.add(text: textOffset, &builder)
offset = TelegramCore_RichText_BankCard.endRichText_BankCard(&builder, start: start)
case let .textBotCommand(text):
valueType = .richtextBotcommand
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_BotCommand.startRichText_BotCommand(&builder)
TelegramCore_RichText_BotCommand.add(text: textOffset, &builder)
offset = TelegramCore_RichText_BotCommand.endRichText_BotCommand(&builder, start: start)
case let .textCashtag(text):
valueType = .richtextCashtag
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_Cashtag.startRichText_Cashtag(&builder)
TelegramCore_RichText_Cashtag.add(text: textOffset, &builder)
offset = TelegramCore_RichText_Cashtag.endRichText_Cashtag(&builder, start: start)
case let .textHashtag(text):
valueType = .richtextHashtag
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_Hashtag.startRichText_Hashtag(&builder)
TelegramCore_RichText_Hashtag.add(text: textOffset, &builder)
offset = TelegramCore_RichText_Hashtag.endRichText_Hashtag(&builder, start: start)
case let .textMention(text):
valueType = .richtextMention
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_Mention.startRichText_Mention(&builder)
TelegramCore_RichText_Mention.add(text: textOffset, &builder)
offset = TelegramCore_RichText_Mention.endRichText_Mention(&builder, start: start)
case let .textMentionName(text, peerId):
valueType = .richtextMentionname
let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_RichText_MentionName.startRichText_MentionName(&builder)
TelegramCore_RichText_MentionName.add(text: textOffset, &builder)
TelegramCore_RichText_MentionName.add(peerId: peerId, &builder)
offset = TelegramCore_RichText_MentionName.endRichText_MentionName(&builder, start: start)
}
return TelegramCore_RichText.createRichText(&builder, valueType: valueType, valueOffset: offset)