Thread PresentationStrings through RichText/InstantPageBlock/InstantPage
previewText(), replacing the hardcoded //TODO:localize placeholders
("Photo", "Fx", "Table", "Map", ...) with localized keys and reusing the
existing Message.Photo/Video/Location strings. Add RichTextPreview.Formula
("[formula]"), RichTextPreview.Table ("[table]"), and RichTextPreview.Music
("Music").
The .audio block previously rendered the wrong label (Message.Audio is
"Voice Message"). Thread InstantPage.media down so the block can resolve
media[id] as TelegramMediaFile and split: isVoice -> "Voice Message",
otherwise -> "Music", mirroring MessageContentKind's voice/music handling.
Update both previewText() call sites (MessageContentKind, ChatListItemStrings)
to pass strings, and complete the InstantPageListItem migration that was
left on the old signature.
Also remove the dead streaming-status ("Thinking...") rendering block from
ChatMessageTextBubbleContentNode (guarded by an always-false `!"".isEmpty`)
along with the now-orphaned streamingTextFrame layout machinery it fed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Message/EngineMessage.effectiveMedia (= message.media when non-empty, else
richText.instantPage.allMedia()) and route the media-consuming sites through it
so a rich message's instant-page media participates in the same pipelines as
normal message.media: shared-media grids/file-rows, search media grid, gallery
open + item nodes + footer, the peer audio/voice playlist, secret-media preview,
resource-by-id resolution, recent downloads, downloaded-media store, delete-time
resource cleanup, cache-usage stats, the in-chat download manager, and the
context-menu / share actions (Save to Camera Roll, copy image, save audio/music
to files). For normal messages effectiveMedia == message.media, so each swap is
behavior-preserving; rich messages render their own bubble via
ChatMessageRichDataBubbleContentNode (not the text/file bubbles), so those paths
are deliberately untouched, as are the forward path (the attribute travels with
the forward) and the markdown-based rich-edit path. First-media scope for now.
See docs/instantpage-richtext.md for the full architecture + invariants.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconstruct markdown source from a stored InstantPage to populate the edit
field, then re-classify on save (the inverse of the send path). Adds the
InstantPageToMarkdown converter, edit-field population and save-time
re-classification in ChatControllerLoadDisplayNode, and a shared InstantPage
previewText surfaced through MessageContentKind for reply/pinned/forward
previews. Documented in CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the RichText.textSpoiler case end-to-end: model + Postbox coding,
FlatBuffers schema/codec, and Api.RichText parsing/serialization (lossless).
Display attaches the spoiler attribute, collects per-line spoiler rects in
the V2 layout, clips obscured glyphs and draws an energy-gated dust overlay
in the V2 text view, propagates the reveal flag across the nested V2 tree
(building dust for freshly-created text views), and wires tap-to-reveal
routing in the rich-data bubble.
Also seeds the streaming reveal cursor across chunk rebuilds and refines
status date positioning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Add the RichText.textCustomEmoji(fileId:alt:) case end-to-end: model +
Postbox coding, FlatBuffers schema/codec, and Api.RichText
parsing/serialization (lossless), plus display in the InstantPage V2
renderer. The emoji renders as an InlineStickerItemLayer that participates
in the streaming reveal (pops in as the reveal cursor crosses it) and is
gated by the bubble's visibility rect, propagated recursively through the
nested V2 view tree. Also frees the CTRunDelegate extent buffers for the
image/formula/custom-emoji attachment arms and documents the feature in
CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds two TelegramEngine.Resources facades and migrates 4 Shape-A sites across 3 consumer files.
Also imports RangeSet in TelegramEngineResources.swift to disambiguate the RangeSet type from
Swift stdlib's iOS-18-only type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave 11: ActionSheetPeerItem de-Postboxed. Revisits wave-1 abandonment.
`postbox: Postbox, network: Network` init params collapse to
`stateManager: AccountStateManager`; avatar setPeer call routes via
`item.stateManager.postbox` / `.network`. Module never names Postbox.
Sole caller (ShareController.swift:1146) migrated in place.
Wave 12: HorizontalPeerItem de-Postboxed (same pattern). Ripples the
collapse up into ChatListSearchRecentPeersNode's public init
(`postbox:/network:` -> `stateManager:`). That module still imports
Postbox for PostboxViewKey/UnreadMessageCountsView internals but its
public surface simplifies. 3 external caller sites migrated.
Wave 13: AttachmentTextInputPanelNode minor cleanup. Module was already
Postbox-free at source level (wave 6) but carried a dead BUILD dep and
had 2 raw `peerId?.namespace == Namespaces.Peer.SecretChat` checks.
Both now use existing `PeerId.isSecretChat` extension in TelegramCore.
Wave 14: BUILD-dep sweep mirroring wave 6's source sweep. 98 modules
had `//submodules/Postbox:Postbox` (or `//submodules/Postbox`) BUILD
deps despite no source file importing Postbox since wave 6. Single
iteration, zero restores -- Bazel Swift requires source-level `import`
for symbol resolution, so redundant BUILD deps are pure metadata.
Net: 110 files, +116/-149. Build verified green (debug_sim_arm64).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the seven remaining raw-Postbox leaks in TelegramEngine public
facades surfaced by a post-wave-6 scouting pass (all non-permanently-
blocked candidates).
Facades migrated in place (6 rewrites + 1 deletion; all _internal_*
implementations unchanged per the "internal Postbox-facing stays raw"
rule):
Messages:
downloadMessage Signal<Message?> -> Signal<EngineMessage?>
topPeerActiveLiveLocationMessages Signal<(Peer?, [Message])> -> Signal<(EnginePeer?, [EngineMessage])>
getSynchronizeAutosaveItemOperations deleted (dead facade; sole caller uses _internal_ directly)
Peers:
updatedRemotePeer Signal<Peer> -> Signal<EnginePeer>
(PeerReference param kept; no EnginePeer.Reference alias today)
Resources:
renderStorageUsageStatsMessages [EngineMessage.Id: Message] -> [EngineMessage.Id: EngineMessage]
clearStorage(peerId: ...) [Message] -> [EngineMessage]
clearStorage(peerIds: ...) [Message] -> [EngineMessage]
clearStorage(messages:) [Message] -> [EngineMessage]
(no external callers; migrated for overload-set consistency)
Consumer call-site updates (5 files):
- ChatListSearchListPaneNode drop redundant .flatMap(EngineMessage.init) wrap
- LocationViewControllerNode drop redundant .map(EngineMessage.init) wrap
- LiveLocationSummaryManager drop redundant EnginePeer(...) / EngineMessage(...) ctors
- StorageUsageScreen bridge [Message] <-> [EngineMessage] at the 4 facade-call points
(internal [MessageId: Message] / [Message] storage kept;
full-consumer-module migration is out of scope)
Discovery: grep of TelegramEngine/*/TelegramEngine*.swift public signatures
for `: Postbox|: Account|: MediaBox|: MediaResource|: Peer\b|: Message\b|
-> Signal<.*(Peer|Message)` turned up these seven candidates and no others.
After this wave, the full TelegramEngine.* facade surface is engine-typed
modulo the four permanently-blocked TelegramMediaResource-conforming
classes recorded in CLAUDE.md (ICloudFileResource, InstantPageExternal-
MediaResource, VideoLibraryMediaResource, YoutubeEmbedStoryboardMedia-
Resource).
No modules became Postbox-free in this wave. Plan: docs/superpowers/
plans/2026-04-20-postbox-to-telegramengine-wave-7.md.
Full project build verified green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>