Add a first-class InstantPageBlock.thinking(RichText) case mirroring .kicker:
- enum case + InstantPageBlockType.thinking = 30; Postbox coding & equality
- FlatBuffers InstantPageBlock_Thinking schema (appended last) + Swift codecs
- API parse at InstantPage.swift; output-only on send (apiInputBlock returns nil)
- no-op stubs in the two exhaustive switches (preview text, V2 layout)
Rendering is out of scope. Verified by a full Bazel build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the user long-presses Send in the main chat and the composed text passes the
same rich-markdown gate used at send time (richMarkdownAttributeIfNeeded), the
send-options preview bubble renders the message as rich InstantPage content,
crossfading from the raw text as the menu animates in. Plain text and custom chat
contents / edit previews are unchanged.
ChatSendMessageActionUI cannot depend on InstantPageUI (it closes a dependency
cycle via LocationUI -> AttachmentUI -> ... -> ChatTextInputActionButtonsNode), so
the rich view is built in TelegramUI (ChatSendMessageRichTextPreview, wrapping
InstantPageV2View + the outgoing message theme) and injected into the screen via a
new ChatSendMessageContextScreenRichTextPreview protocol, mirroring the existing
media-preview pattern. The main-chat send call site classifies the compose text
and injects the preview; MessageItemView lays it out within the send-button-bounded
bubble envelope and crossfades between the raw text and the rich layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rich messages (RichTextMessageAttribute, text == "") are copyable as
markdown two ways: the context-menu Copy action copies the whole
message, and a text selection inside the rich-data bubble copies just
the selected range. Both reconstruct markdown mirroring the edit
round-trip (markdownStringFromInstantPage).
Implements the full RichText entity case set
(mention/hashtag/cashtag/bot-command/bank-card/auto url/email/phone) with
tap interaction, the InstantPage -> markdown inverse converter and edit
round-trip, markdown-context stamping during V2 layout
(InstantPageMarkdownBlockContext: heading level, list/code/table/quote
depth), partial-selection markdown emission
(InstantPageMultiTextAdapter.markdownForRange), and numerous converter
edge-case fixes (tables, links, fenced code, blockquote line coalescing,
compact nested >> markers).
CLAUDE.md documents the feature; the spec/plan scratch docs generated
during development are not committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Upgrade InstantPageBlock.blockQuote from (text, caption) to
(blocks: [InstantPageBlock], caption). Legacy inbound shapes (API
pageBlockBlockquote, Postbox "t", FlatBuffers text) lift into
[.paragraph(text)]; outbound stays on the legacy wire constructor for
empty/single-paragraph quotes and uses pageBlockBlockquoteBlocks
otherwise. V1/V2 renderers recurse into child blocks with a fixed 10pt
inter-child gap; markdown forward/reverse, entity-expressibility, and
preview text updated. pullQuote is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upgrades the InstantPageBlock.blockQuote case from a text-only
payload to a nested-blocks payload, covering API parse, Postbox +
FlatBuffers serialization, API encode, V1/V2 layout, markdown
forward/reverse, entity-expressibility, and preview text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bubble's bottom inset is supplied by the `statusBottomEdge + 6.0`
max() in the measure closure, but that branch is gated by `!hasDraft`.
During streaming (status hidden, alpha=0) the gate skips the max() and
nothing else supplies the inset — boundingSize.height ends up at only
`revealedContentSize.height + 2` (= bounds.maxY + closingPad + rim),
so descenders of the last revealed line sit cramped against the bubble's
bottom edge and the bubble visibly grows by 6pt the moment streaming
ends and the status node fades in.
Adds an explicit `if hasDraft { boundingSize.height += 6.0 }` after
the streamingHeaderOffset application — same 6pt the status max() would
contribute, so the streaming bubble's bottom breathing room matches its
post-stream height and the grow-pop disappears.
The `hadDraft && !hasDraft` finalize pass already gets the inset via
the status max() (since `!hasDraft` is true), so it's untouched. The
non-streaming path is also untouched.
TextBubble doesn't have this issue because it computes a position-based
`bottomInset` and adds it to boundingSize.height unconditionally (see
ChatMessageTextBubbleContentNode.swift:234-256, :827). RichData absorbed
that value into the `+ 6.0` constant inside its `!hasDraft` status
max(), which made it disappear during streaming. CLAUDE.md gains a
sibling bullet flagging that future refactor-the-constant-into-bottomInset
work must remove both ends in the same commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders `RichText.image(id:, dimensions:)` runs as real fetched images
inside `ChatMessageRichDataBubbleContentNode` / `InstantPageV2View`,
replacing the gray `InstantPageV2MediaPlaceholderView`. Source is
whatever populates `instantPage.media[id]` (server-pushed webpage
InstantPages); the markdown converter is unchanged
(`markdownImageParsingEnabled` stays false).
Architecture mirrors inline custom emoji: inline images are NOT
top-level `InstantPageV2LaidOutItem`s. `InstantPageV2View` walks each
text view's `line.imageItems`, resolves each MediaId against
`layout.media`, and creates `InstantPageV2InlineImageView` children
inside a new `imageContainerView` on each `InstantPageV2TextView`
(sibling of `renderContainer`, above the reveal mask, below
`emojiContainerView`).
Streaming reveal participation: image cells contribute their full
width to the per-line character rects (mirrors emoji), so the cost map
charges the cursor by the image's width when crossing it. When the
cursor crosses the cell, `updateImageReveal` pops the view in
(opacity 0→1 + scale 0.1→1.0 over 0.2s ease-out), matching
`updateEmojiReveal` exactly. `applyReveal` calls both alongside each
other.
Non-interactive (V1 parity): `isUserInteractionEnabled = false` lets
URL-wrapping `RichText.url(text: .image(...))` route taps through to
the underlying text view's URL handler.
Key files:
- New `InstantPageV2InlineImageView.swift`: lightweight
`TransformImageNode`-backed view. Picks the fetch signal by
EngineMedia kind (.image → chatMessagePhoto; image-mime .file →
instantPageImageFile; video .file → chatMessageVideo single frame).
`MetaDisposable` cancels fetches on dealloc.
- `InstantPageV2Layout.swift`: `media`/`webpage` fields on the layout
struct; image cells contribute to char rects; the gray-placeholder
branch in `layoutTextItem` is removed; the dead `media`/`webpage`
params on `layoutTextItem` are dropped along with their 13 call
sites.
- `InstantPageRenderer.swift`: `imageContainerView` on text view
(sibling of `renderContainer`, between it and `emojiContainerView`);
`InlineImageKey` + `InstantPageInlineImageData` types;
`updateInlineImages()` + `updateImageReveal()` on
`InstantPageV2View`; wired into `update(layout:theme:animation:)`.
- `InstantPageV2RevealCost.swift`: `applyReveal` calls
`updateImageReveal` next to `updateEmojiReveal` in both the
clear-path branch and the entry-walking branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thread the chat list's animated transition through InstantPageV2View.update so
reused-item frame changes animate: the toggled details view's own frame-height
(clipsToBounds) clip-reveals/clip-hides its body, sibling items below slide, and
the chevron rotates — all on the one transition the bubble already drives for its
outer height. On expand the body is added up-front; on collapse it is kept and
torn down in finalizePendingCollapse(), called from the completion of the
frame-shrink (clip) animation that hides it. Drop the forced scroll-to-center on
toggle so the bubble grows in place.
Also render the chevron as a UIImageView with a template image + tintColor (the
message's primary text color) instead of baking the tint into a CALayer's cgImage
contents, which dropped the color and rendered it black.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a section covering the first-class InstantPageListItem.checked field:
where things live, the API flags.0/flags.1 transmission, tri-state Postbox/
FlatBuffers persistence, V1/V2 detection + V2 CheckNode hosting, and the
markdown round-trip contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a first-class `checked: Bool?` to InstantPageListItem (orthogonal to the
ordered-list `num`), replacing the prior sentinel-string-in-num prototype.
- Data model: enum third associated value on .text/.blocks; Postbox + a new
FlatBuffers `checkState` tri-state field (0=nil,1=unchecked,2=checked,
backward-compatible); Equatable.
- Transmission: read/write the native API `checkbox`=flags.0 / `checked`=flags.1
bits on all four PageListItem / PageListOrderedItem constructors, so checkbox
state survives the server round-trip for sender and recipients.
- Markdown: forward parser routes detected `[ ]`/`[x]` into `checked` (keeping
the real number for ordered items); reverse converter emits `- [ ]`/`- [x]`
so editing a rich message round-trips.
- Display: V1 + V2 layout detect via `item.checked`; the V2 renderer draws real
CheckNode artwork (was a placeholder); preview text shows a checkbox glyph
instead of leaking the old sentinel.
Spec: docs/superpowers/specs/2026-05-27-instantpage-list-checkbox-design.md
Plan: docs/superpowers/plans/2026-05-27-instantpage-list-checkbox.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 tasks: data model (enum + Postbox + FlatBuffers + Equatable), API
transmission via checkbox/checked flag bits, markdown forward/reverse,
preview text, V1/V2 layout detection, V2 CheckNode artwork, build-to-green,
and manual round-trip verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First-class checked: Bool? on InstantPageListItem (orthogonal to num),
replacing the prior sentinel-in-num prototype. Covers parsing, Postbox +
FlatBuffers serialization, API transmission via the native checkbox/checked
flag bits, V2 CheckNode artwork, edit round-trip, and preview text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>