mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
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>
This commit is contained in:
parent
071799368d
commit
ab68a1af82
6 changed files with 415 additions and 27 deletions
|
|
@ -63,7 +63,7 @@ Every V2 block-media kind **except `.audio`** lays out **flush** with the bubble
|
|||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `submodules/InstantPageUI/Sources/InstantPageV2Layout.swift` | `instantPageV2MediaFrame(naturalSize:flush:cornerRadius:boundingWidth:horizontalInset:)` — the shared frame helper; `instantPageV2MediaEdgeBleed` constant; the `flush: Bool` parameter on `layoutTypedMediaWithCaption` (image/video/webEmbed-cover/map) and `layoutMediaWithCaption` (audio/webEmbed-placeholder/postEmbed/collage/slideshow/channelBanner/relatedArticles). |
|
||||
| `submodules/InstantPageUI/Sources/InstantPageV2Layout.swift` | `instantPageV2MediaFrame(naturalSize:flush:cornerRadius:boundingWidth:horizontalInset:)` — the shared frame helper; `instantPageV2MediaEdgeBleed` constant; the `flush: Bool` parameter on `layoutTypedMediaWithCaption` (image/video/webEmbed-cover/map) and `layoutMediaWithCaption` (audio/webEmbed-placeholder/postEmbed/channelBanner/relatedArticles). (Collage/slideshow no longer route through `layoutMediaWithCaption` — see the collage & slideshow section.) |
|
||||
| `submodules/InstantPageUI/Sources/InstantPageV2MediaViews.swift`, `…/InstantPageRenderer.swift` (`InstantPageV2MediaPlaceholderView`) | Renderer — **no change needed**: every media view + the placeholder view already does `clipsToBounds = item.cornerRadius > 0.0`, so cornerRadius 0 means the view doesn't self-clip; the bubble's `containerNode` clips. |
|
||||
| `…/Chat/ChatMessageRichDataBubbleContentNode/…` | The clipping container: `containerNode` (`clipsToBounds = true`, `cornerRadius = layoutConstants.image.defaultCornerRadius` ≈ 15–16pt) is what rounds flush media at the bubble edge. |
|
||||
|
||||
|
|
@ -76,6 +76,28 @@ Every V2 block-media kind **except `.audio`** lays out **flush** with the bubble
|
|||
- **Audio is the lone exception** and routes through the non-flush branch of `instantPageV2MediaFrame` (inset by `horizontalInset`, caller's cornerRadius), reproducing the legacy behavior exactly.
|
||||
- **`.map` blocks get a 600×300 (2:1) fallback when the sender omits dimensions.** AI/server-sent `.map` blocks can arrive with `dimensions == 0×0` (the wire `w`/`h` are *required* `Int32`, but the sender may put 0; our `pageBlockMap` parse and both serializers — Postbox `sw`/`sh`, FlatBuffers `required dimensions` — preserve whatever arrives, so the zero originates upstream). A zero `naturalSize.height` hits `instantPageV2MediaFrame`'s `else` branch and returns a **height-0** frame: the map collapses to no space, the caption slides up into it, and the V1 node's pin (positioned at `size.height*0.5 − 10 − pinSize/2`) floats over the caption. **The `.map` arm in `InstantPageV2Layout.swift` substitutes `PixelDimensions(600, 300)` whenever `width <= 0 || height <= 0`, and feeds that `effectiveDimensions` to BOTH the layout `naturalSize` AND the `InstantPageMapAttribute`** — the latter is essential because a `MapSnapshotMediaResource(width:0,height:0)` makes `MKMapSnapshotter` render nothing, so fixing only the frame would yield a correctly-sized *blank* box. Real web-article maps (the V1 renderer) always carry real dimensions, so V1 never trips this; the fallback is deliberately scoped to the V2 `.map` arm rather than V1 or the wire/parse layer.
|
||||
|
||||
## InstantPage V2 collage & slideshow blocks
|
||||
|
||||
`InstantPageBlock.collage` and `.slideshow` (grouped photos/videos with a caption — only ever produced by **real web Instant View articles**; nothing on the markdown/AI path emits them) render in V2 by porting V1. Collage flattens into the existing media-item machinery; slideshow is a dedicated interactive carousel.
|
||||
|
||||
### Where things live
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `submodules/InstantPageUI/Sources/InstantPageV2Layout.swift` | `layoutCollage(...)` — mosaic via `chatMessageBubbleMosaicLayout` (the `MosaicLayout` module, same engine grouped messages use), emitting one existing `.mediaImage`/`.mediaVideo` item per cell. `layoutSlideshow(...)` + the `InstantPageV2SlideshowItem` laid-out item (+ its `frame`/`offsetBy`/`collectMedias` arms). |
|
||||
| `submodules/InstantPageUI/Sources/InstantPageV2SlideshowView.swift` | The carousel view: a paged `UIScrollView` of `InstantPageImageNode` pages + a `PageControlNode`, with all pages created **eagerly**. |
|
||||
| `…/InstantPageRenderer.swift` | `InstantPageItemView.instantPageTransitionNode(for:)` / `instantPageUpdateHiddenMedia(_:)` (gallery hooks, nil/no-op defaults); `transitionArgsFor`/`applyHiddenMedia` dispatch through them. The `.slideshow` arms in `InstantPageV2ItemKind`/`stableId`/`reuse`/`makeItemView`. |
|
||||
| `…/InstantPageV2RevealCost.swift` | `.slideshow` is a non-text reveal entry (collage cells already are, being top-level media items). |
|
||||
|
||||
### Non-obvious invariants
|
||||
|
||||
- **Collage is a flatten, not a container.** `layoutCollage` computes the mosaic, then emits each cell as an ordinary top-level `.mediaImage`/`.mediaVideo` item (cornerRadius 0) into the parent layout — exactly as V1 does (`flattenedItemsWithOrigin`). Consequence: gallery enumeration (`allMedias`), the media registry, hidden-media, the reveal-cost map, and view reuse all handle collage cells **for free**, with no collage-specific code in any of those subsystems. There is **no** `.collage` laid-out item or view.
|
||||
- **Right-edge collage cells bleed 4pt** (`instantPageV2MediaEdgeBleed`, applied only to `MosaicItemPosition.right` cells) for the same bubble-rounded-clip reason as full-width single media; interior gaps are the mosaic's 1pt spacing; outer corners are rounded by the bubble's `containerNode`.
|
||||
- **Slideshow IS a container** (it's swipeable), so it gets its own laid-out item + view, unlike collage. Adding the `.slideshow` case to `InstantPageV2LaidOutItem` forces a `.slideshow` arm in every no-`default` switch over it: `frame`, `offsetBy`, `stableId`, `reuse`, `makeItemView`, and the reveal-cost `computeEntries` (plus `collectMedias`, which has a `default` but needs the arm to enumerate slideshow medias for the gallery).
|
||||
- **Slideshow pages are created eagerly, deviating from V1's lazy central±1 paging.** In a chat bubble a slideshow is a handful of images, so eager creation avoids V1's index bookkeeping and makes the gallery transition source available for **every** page (even off-screen). Height = the tallest image `fitted(boundingWidth × 1200)`; only `.image` inner blocks render (matches V1 — videos become empty pages).
|
||||
- **The slideshow registers under EVERY contained media index, and re-registers on an in-window rebuild.** Its stableId is positional (`.positional(.slideshow, position)`, not `.media(index)` like the static media views), so it can be reused for a *different* slideshow at the same block position; `rebuildPages()` re-runs `registerMedias()` (guarded by `window != nil`) so the new indices land in the registry. The gallery hooks iterate the live page nodes and match by `InstantPageMedia` identity, so registering one view under N indices is idempotent.
|
||||
- **The 4 static media views answer the gallery hooks with explicit per-class witnesses, NOT a shared protocol-extension override** — an extension-only implementation is statically dispatched and would silently bind to the nil default when invoked through the `InstantPageItemView`-typed registry wrapper.
|
||||
|
||||
## InstantPage V2 text item height (true font line box)
|
||||
|
||||
`layoutTextItem` (`InstantPageV2Layout.swift`) sizes a `.text` item to the **true font line height**, not the cap box. A single-line item measures exactly `fontAscent + fontDescentBelowBaseline` (`A + D`); the old behavior was the cap box `fontLineHeight = floor(fontAscent + fontDescent)` (`A − D`).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue