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>
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>
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>
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>
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>
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>
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>
Eight-task plan covering ContextUI struct field additions,
PortalTransitionStaging helper, CCEPN animateIn/animateOut wiring,
ChatControllerNode contextTransitionContainer, two adopter sources,
and manual visual verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three task plan: (1) trim ShimmeringMask BUILD deps, (2) replace stub
with full reveal-mask CAGradientLayer implementation, (3) wrap
streamingStatusTextNode in ChatMessageTextBubbleContentNode. Plus a
manual-verification task since the project has no unit-test harness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reusable view that applies a moving alpha-mask shimmer (rest=1.0,
dip=peakAlpha) to its contentView. First consumer: the streaming-status
text node in ChatMessageTextBubbleContentNode for ChatGPT-style
"thinking" effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces CCEPN's manual visible-area clipping with a portal-based
transition mirroring CMTN's primitive. Adds optional
sourceTransitionSurface to TakeViewInfo/PutBackInfo; chat is the
first adopter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan covers two single-line edits in LinkHighlightingNode.swift's
modern branch (X-snap dy direction; floor → ceil for stair-step
fillet radii), each landed as its own commit, with a final
full-project build for validation since this repo has no tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document the analysis and intended fixes for two bugs in
LinkHighlightingNode's modern path branch: the X-edge snap is
unreachable after the midY snap (positive dy in insetBy shrinks
rect[i] so it can't intersect the touching neighbor), and the
floor() in nextRadius/prevRadius can produce zero-radius arcs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
drawInTile previously stroked each cell's full perimeter, double-drawing
every interior gridline; visible now that the rich-data chat bubble uses
tableBorderColor at 0.25 alpha. Stroking each segment in its own
strokePath call would also have left ~1pt² overdraw at every interior
4-cell junction (where a horizontal divider crosses a vertical one) and
at every T-junction with the outer rounded rect — each strokePath
rasterizes independently and composites against the previous result.
Build a single CGMutablePath containing each cell's interior top/left
segment (skipping cells on the table's top/left boundary) plus the outer
rounded perimeter rect, and call strokePath once. CGContextStrokePath
fills the union of all stroke geometries as a single fill op, so each
pixel is painted exactly once regardless of how many segments overlap.
Empty cells (text == nil) are no longer skipped wholesale: their fill
and text remain gated on text != nil (preserves today's no-fill-for-
empty behavior), but their interior divider segments still get appended
so divider continuity is preserved around them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds drag-handle text selection driven by TextSelectionNode. Exposes
attributedString and selection helpers on InstantPage text items, and
introduces a multi-text adapter aggregating items as a TextNodeProtocol.
Gates selection actions on reply-options and fixes highlight z-order.
Adds a base getAnchorRect on ChatMessageBubbleContentNode, the rich-bubble
override (including titleHeight in details recursion), bubble-item
forwarding to content nodes, ChatControllerInteraction.scrollToMessageIdWithAnchor,
and a scrollToAnchor that lands the anchor at the top of the content area /
its line. Threads anchor/scroll params through ChatController and related
call sites.
Documents a two-pass refactor for InstantPageTableItem.drawInTile that
draws each interior divider exactly once (top+left of each cell that
isn't on the table boundary) plus a single rounded-rect outer-perimeter
stroke. Needed now that tableBorderColor is being made semi-transparent
(0.25 alpha) by the rich-data chat bubble; current per-cell whole-bounds
strokes overdraw shared edges.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ChatControllerInteraction dependency, link-progress state, URL tap
detection, press-highlight separation from in-flight URL shimmer, media-tap
routing through openMessage with explicit IV media subject, and gallery↔bubble
transition with hidden-media coordination. Stops re-appending to
currentLayoutItemsWithNodes across re-layouts. Drops a leftover MetalEngine
debug print as well.
When a pinToEdgeWithInset item is taller than half of the visible
area (visibleSize.height - insets.top - insets.bottom), cap its
visible portion at halfArea. The remaining height extends past
visibleSize - insets.bottom into the bottom-inset region (occluded
by overlay UI like the chat input panel).
A new private helper `pinToEdgeBottomExtension(forPinnedHeight:)`
returns max(0, pinnedHeight - halfArea). Three sites consume it:
- calculatePinToEdgeTopInset caps the pinned item's contribution
to totalAboveAndPinned via `pinnedHeight - extension`.
- replayOperations isPinToEdgeTarget anchors the pinned item's
apparent maxY at visibleSize - insets.bottom + extension.
- isStrictlyScrolledToPinToEdgeItem matches the new anchor.
Both isPinToEdgeTarget and isStrictlyScrolledToPinToEdgeItem fire
when either calculatePinToEdgeTopInset() > 0 OR extension > 0, so
the cap also applies when items above the pinned item overflow
the visible area (in which case calculatePinToEdgeTopInset returns
0 but extension is still positive).
When the pinned item fits within halfArea, extension == 0 and
behavior is identical to before this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementation plan for the spec from
docs/superpowers/specs/2026-05-01-listview-pin-to-edge-half-cap-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire URL tap detection, link-highlight feedback, and item-callback
routing in ChatMessageRichDataBubbleContentNode, with stubbed
intra-page anchor handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delay outgoing messages while the peer in the same (peerId, threadId)
chat is live-typing an incoming message, gated via a new per-account
Postbox `.allTypingDrafts` view feeding a synchronous Set membership
check inside PendingMessageManager.
Postbox:
- Add `AllTypingDraftsView` tracking `Set<PeerAndThreadId>` of active
typing drafts; expose via `PostboxViewKey.allTypingDrafts`.
- Disambiguate `PostboxViewKey` hash arms for `.typingDrafts` (constant
prefix 23) and `.contacts` (16 → 24) to avoid runtime collisions
with peerId-only arms and `.combinedReadState`. Hash values are
runtime-only, so this is safe.
PendingMessageManager:
- Add `.waitingForSendGate(groupId:content:)` parking state for single
messages and albums whose upload finished but are gated on a
typing-draft-clearing event; update `groupId` accessor and
`dataForPendingMessageGroup` so partially-parked albums drain on
gate open.
- Add `.waitingForForwardSendGate` parking state for forwards (no
associated values) so drain section (1) routes single non-grouped
messages via `commitSendingSingleMessage` and section (3) routes
forwards via `sendGroupMessagesContent`, avoiding the double-dispatch
that occurred when forwards reused `.waitingForSendGate`.
- Subscribe to `.allTypingDrafts`; wire the gate at single-message,
album, and forward send paths and at drain.
- Cleanup parked forwards on pending-message removal; snapshot
Dictionary keys before iterating to avoid mutation during iteration.
Specs and plan documents are included under `docs/superpowers/`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Design doc for filling in the empty stub at ListView.swift:2674.
Defines the "strictly scrolled" condition as the equation that the
pin-to-edge scroll math at line 3115 lands on:
apparentFrame.maxY == (visibleSize.height - insets.bottom) + scrollPositioningInsets.bottom
with a 0.5pt tolerance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the wave-71-shadow ratchet: caller already holds EnginePeer and
demotes once at the boundary. Migration drops the demote, drops `import
Postbox` from the node, and rewrites 3 `as? TelegramChannel` downcasts
to `case let .channel(channel)`. 7 edits across 2 files, 1-iter target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Private free function in ContactMultiselectionController.swift. Five
_asPeer() bridges drop in one atomic single-file commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trims the Postbox-refactor section of CLAUDE.md from ~750 lines to ~160
by moving per-wave outcomes, the running tally of Postbox-free modules,
and verbose wave-selection guidance backstory into
docs/superpowers/postbox-refactor-log.md. Also adds a spec document for
the reorganization.
Kept in-place in CLAUDE.md: standing rules, engine typealias cheat sheet,
MediaResource consumer-migration patterns, TelegramEngine.Resources facade
inventory, and terse bullet-form guidance with log cross-references.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>