Commit graph

9 commits

Author SHA1 Message Date
isaac
0050cc7a08 Rich-message media in gallery/shared-media/preview pipelines via Message.effectiveMedia
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>
2026-06-04 23:46:56 +02:00
isaac
0a92dbddc3 InstantPage V2: fix inline emoji/image/formula x-offset on RTL lines
Inline attachments anchored their left edge at
CTLineGetOffsetForStringIndex(line, range.location), which is the glyph's
LEFT edge for LTR runs but its RIGHT edge for RTL runs (string index
increases leftward). On an RTL line (e.g. an Arabic thinking block) this
shoved emoji/images/formulas ~one advance (~24pt) too far right while the
CoreText-drawn text stayed correct.

Add v2LeadingOffsetForRange(_:range:), which returns
min(offset(start), offset(end)) with directional-boundary secondary-offset
handling — the true leading edge in both directions. Mirrors
Display.TextNode.addEmbeddedItem and the strikethrough/underline/spoiler
decorations already in this file (which used the min/abs form; the inline
attachments had regressed to a single offset). Applied at all 5 sites:
the emoji/image/formula display frames and the emoji/image characterRect
(reveal mask). Widths unchanged; only x corrected. LTR is byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:39:13 +02:00
isaac
dce6a8bef7 Rich messages: "Show more" to load the full page for partial InstantPages
Server-sent rich messages can arrive partial (RichMessage isPartial ->
instantPage.isComplete == false). The bubble renders the partial page with
an inline "Show more" link; tapping it fetches the full page once and
expands in place.

- RichTextMessageAttribute keeps the partial instantPage and gains an
  optional fullInstantPage, filled by engine.messages.requestFullRichText
  via transaction.updateMessage. The seed-config merge preserves a fetched
  fullInstantPage across later server updates.
- ChatMessageRichDataBubbleContentNode: node-local, per-message expand
  state (collapsed on every fresh display, even when fullInstantPage is
  already cached); renders (expanded ? fullInstantPage : nil) ?? instantPage;
  gates the link on !expanded && !isComplete (+ not streaming, Cloud-only,
  not preview/messageOptions); expand state threaded through both layout
  caches; shimmer while fetching (instant when cached); bubble grows
  downward on expand via setInvertOffsetDirection.
- New localized string Chat.RichText.ShowMore; docs in
  docs/instantpage-richtext.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:18:15 +02:00
isaac
a81d0ee944 Anchor navigation in InstantPage V2 / rich-content bubbles
Tapping an intra-message #fragment link in a rich-data bubble now scrolls
the chat so the matching anchor lands just below the content-area top,
expanding any enclosing collapsed <details> first. Anchors come from
server/AI-sent InstantPages (block .anchor or inline RichText.anchor); the
compose path is unchanged.

- InstantPageV2View.anchorFrame(name:) resolves an anchor's frame in the
  live layout (text/codeBlock/thinking/details/table), mirroring findTextItem.
- instantPageAnchorPath(in:name:) is a pure model walk returning the
  <details>-sibling-ordinal path to an anchor; its recursion set matches
  exactly the containers the V2 layout flattens through layoutBlock
  (.blockQuote/.cover/.list .blocks), keeping ordinals consistent with the
  layout's detailsIndexCounter.
- InstantPageV2View.firstCollapsedDetails(forOrdinalPath:) maps that path to
  the first not-yet-expanded details' live index (read, never reproduced).
- The rich bubble fills the two stubbed seams: getAnchorRect, and a
  fragment-link route in tapActionAtPoint that drives a resolve -> expand ->
  scroll state machine (pendingScrollAnchor + progress guard + a
  post-relayout hook). Taps are gated off while the message streams.

Verified by the full Bazel build; runtime behavior not yet exercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 01:11:27 +02:00
isaac
f6da30da70 InstantPage V2: audio/music rendering, playback, and file-bubble styling
Render and play InstantPageBlock.audio in the InstantPage V2 renderer
(rich-data message bubbles + the rich send preview), where audio was
previously an inert grey placeholder.

- New InstantPageV2AudioContentNode replicates the standard music message
  bubble (ChatMessageInteractiveFileNode's music layout): a SemanticStatusNode
  control (album art via playerAlbumArt + play/pause) with a download/progress
  overlay, title + "duration · performer" lines, exact fonts/colors from
  theme.chat.message.{incoming|outgoing}. Tap is driven by a
  UITapGestureRecognizer (ASControl .touchUpInside is cancelled by the chat
  ListView's gesture system). V1's InstantPageAudioNode is unchanged.
- Playback runs on InstantPageMediaPlaylist with a discriminated, message-scoped
  InstantPageMediaPlaylistId (.instantPage / .richMessage) so concurrent
  rich-message audio bubbles don't collide; the big control's play/pause comes
  from filteredPlaylistState, the overlay's download/progress from
  messageMediaFileStatus, and fetch goes through the fetch manager.
- Rich-message audio fetches via a MessageReference (threaded through the V2
  render context) instead of the synthesized webpage; FetchedMediaResource's
  .message revalidation arm now also searches RichTextMessageAttribute instant
  pages, so a stale instant-page audio/image reference can recover. Corrected a
  dormant inverted InstantPagePlaylistLocation.isEqual.
- New .mediaAudio laid-out item + layout/reveal-cost arms; the audio block lays
  out full-width at the file node's music normHeight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:44:45 +02:00
isaac
ab68a1af82 InstantPage V2: render collage & slideshow blocks
Port V1's collage and slideshow InstantPage blocks into the V2 renderer
(previously grey-box placeholders).

- Collage: layoutCollage computes the mosaic (MosaicLayout, the grouped-message
  engine) over inner image/video sizes and flattens it into existing top-level
  .mediaImage/.mediaVideo items + a caption, so gallery / reveal-cost / registry
  / hidden-media all handle the cells with no collage-specific code. Right-edge
  cells bleed 4pt for the bubble's rounded clip.
- Slideshow: a new .slideshow laid-out item + InstantPageV2SlideshowView, an
  eager paged carousel (UIScrollView + PageControlNode) of InstantPageImageNode
  pages, wired through frame/offsetBy/collectMedias/stableId/reuse/makeItemView
  and the reveal-cost non-text list.
- Gallery transitions generalized onto InstantPageItemView via
  instantPageTransitionNode(for:)/instantPageUpdateHiddenMedia(_:) (default
  nil/no-op; explicit per-class witnesses on the 4 static media views, the
  slideshow forwards to its live pages) so the multi-media slideshow can
  participate alongside single-media views.

Docs: document both in docs/instantpage-richtext.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:50:40 +02:00
isaac
071799368d InstantPage V2: fall back to 600x300 for zero-dimension map blocks
AI/server-sent .map blocks can arrive with dimensions == 0x0 (the wire
w/h are required Int32, but the sender may put 0; our parse and both
serializers preserve whatever arrives). A zero naturalSize.height hit
instantPageV2MediaFrame's else branch and returned a height-0 frame:
the map collapsed to no space, the caption slid up into it, and the V1
node's pin floated over the caption. A zero-sized MapSnapshotMediaResource
would also make MKMapSnapshotter render nothing.

Substitute PixelDimensions(600, 300) (2:1) whenever width <= 0 ||
height <= 0, feeding effectiveDimensions to BOTH the layout naturalSize
AND the InstantPageMapAttribute so the snapshot resource is non-zero and
actually renders. Scoped to the V2 .map arm; V1 (real web articles)
always carries real dimensions and never trips this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:36:18 +02:00
isaac
9b1573e87e InstantPage V2: flush, un-rounded block media (except audio)
All block-media kinds except .audio now lay out edge-to-edge (0 inset) with
cornerRadius 0; the bubble's existing rounded containerNode clip rounds media at
the bubble edge. Small images keep natural size (not upscaled); captions stay
inset. Shared instantPageV2MediaFrame helper + flush flag on the two media
layout helpers. V1 unchanged. Invariants documented in docs/instantpage-richtext.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:32:24 +02:00
isaac
804c02743a docs: slim CLAUDE.md, extract rich-text/InstantPage notes
Remove stale data from CLAUDE.md and split out the large InstantPage V2 /
rich-text feature documentation into its own file.

- Drop two dead spec links (table inset/corner-radius design docs no longer
  exist), the orphaned debugRichText cleanup note (flag is read by nothing),
  and the stale "238 waves as of 2026-05-04" count.
- Remove the duplicated Postbox "Wave-selection guidance" and "facade
  inventory" subsections; both live in docs/superpowers/postbox-refactor-log.md
  and TelegramEngineResources.swift (pointer folded into the Historical record).
- Move the 13 InstantPage V2 / rich-text sections to
  docs/instantpage-richtext.md, leaving a brief pointer in CLAUDE.md.

CLAUDE.md: 507 -> 161 lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:33:41 +02:00