docs: spec for flush un-rounded InstantPage V2 block media
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
804c02743a
commit
9dc990d5d6
1 changed files with 145 additions and 0 deletions
|
|
@ -0,0 +1,145 @@
|
||||||
|
# InstantPage V2 — flush (edge-to-edge) un-rounded block media
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
In the InstantPage **V2** renderer (rich-text chat bubbles), block media is
|
||||||
|
currently laid out as an **inset rounded rect**: the media frame is inset by the
|
||||||
|
page `horizontalInset` (11pt) on each side and drawn with an 8pt corner radius.
|
||||||
|
Change it so block media is **flush with the bubble interior (0-inset)** and
|
||||||
|
**un-rounded (cornerRadius 0)**, relying on the bubble's existing rounded-corner
|
||||||
|
clipping container to round media that reaches the bubble's top/bottom edge.
|
||||||
|
|
||||||
|
**V1 is out of scope** — only the V2 layout/renderer changes.
|
||||||
|
|
||||||
|
## Scope of "block media"
|
||||||
|
|
||||||
|
Every block-media kind in the V2 layout goes flush + un-rounded **except
|
||||||
|
`.audio`**:
|
||||||
|
|
||||||
|
| Block | Today | After |
|
||||||
|
|---|---|---|
|
||||||
|
| `.image` | inset, r=8 | flush, r=0 |
|
||||||
|
| `.video` | inset, r=8 | flush, r=0 |
|
||||||
|
| `.map` | inset, r=8 | flush, r=0 |
|
||||||
|
| `.webEmbed` (cover image) | inset, r=0 | flush, r=0 |
|
||||||
|
| `.webEmbed` (grey placeholder) | inset, r=0 | flush, r=0 |
|
||||||
|
| `.postEmbed` | inset, r=8 | flush, r=0 |
|
||||||
|
| `.collage` | inset, r=8 | flush, r=0 |
|
||||||
|
| `.slideshow` | inset, r=8 | flush, r=0 |
|
||||||
|
| `.channelBanner` | inset, r=0 | flush, r=0 |
|
||||||
|
| `.relatedArticles` | inset, r=0 | flush, r=0 |
|
||||||
|
| **`.audio`** | inset, r=8 | **unchanged** (inset, r=8) |
|
||||||
|
|
||||||
|
`.audio` is the single exception — it keeps today's inset + 8pt rounding.
|
||||||
|
|
||||||
|
## Decisions (from brainstorming)
|
||||||
|
|
||||||
|
- **Small images are NOT upscaled.** Keep the existing `scale = min(availableWidth /
|
||||||
|
naturalSize.width, 1.0)` cap. A small image renders at its natural pixel size,
|
||||||
|
flush-left at x=0 (not stretched to full width). Large images (the common
|
||||||
|
AI-generated case) still fill the width.
|
||||||
|
- **Square corners everywhere.** No per-corner rounding logic. Media views draw
|
||||||
|
square; the existing rounded-corner clipping container rounds whatever media
|
||||||
|
reaches the bubble's top/bottom edge.
|
||||||
|
- **Captions stay inset.** Caption/credit text is laid out separately at
|
||||||
|
`horizontalInset` and is unchanged — inset text under a full-bleed image.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Two layout helpers in
|
||||||
|
`submodules/InstantPageUI/Sources/InstantPageV2Layout.swift` are the only places
|
||||||
|
that compute the inset media frame and carry the corner radius:
|
||||||
|
|
||||||
|
- `layoutTypedMediaWithCaption(produceItem:naturalSize:caption:isCover:cornerRadius:boundingWidth:horizontalInset:context:)`
|
||||||
|
— used by `.image`, `.video`, `.webEmbed` cover, `.map`.
|
||||||
|
- `layoutMediaWithCaption(kind:naturalSize:caption:isCover:cornerRadius:boundingWidth:horizontalInset:context:)`
|
||||||
|
— used by `.audio`, `.webEmbed` placeholder, `.postEmbed`, `.collage`,
|
||||||
|
`.slideshow`, `.channelBanner`, `.relatedArticles`.
|
||||||
|
|
||||||
|
Both compute `availableWidth = boundingWidth − horizontalInset*2`, place the
|
||||||
|
media at `x: horizontalInset`, and pass a caller-supplied `cornerRadius` onto the
|
||||||
|
produced item.
|
||||||
|
|
||||||
|
### Change: add a `flush` parameter to both helpers
|
||||||
|
|
||||||
|
Add `flush: Bool` to both helper signatures. When `flush == true`:
|
||||||
|
|
||||||
|
- `availableWidth = boundingWidth` (full width, was `boundingWidth − horizontalInset*2`).
|
||||||
|
- media frame `x: 0` (was `horizontalInset`).
|
||||||
|
- the produced item's `cornerRadius` is forced to `0` (the per-call-site
|
||||||
|
`cornerRadius` argument becomes irrelevant on the flush path).
|
||||||
|
|
||||||
|
When `flush == false` (audio only): today's behavior verbatim (inset frame,
|
||||||
|
caller's corner radius).
|
||||||
|
|
||||||
|
The `scale = min(availableWidth / naturalSize.width, 1.0)` cap is **kept** in both
|
||||||
|
paths — only `availableWidth` and the frame `x` change, so the no-upscale
|
||||||
|
behavior is preserved.
|
||||||
|
|
||||||
|
### Call sites
|
||||||
|
|
||||||
|
Every media `case` in `layoutBlock` passes `flush: true` **except `.audio`**,
|
||||||
|
which passes `flush: false`. The cover-padding logic (`isCover && captionHeight >
|
||||||
|
0`) and the caption/credit layout (`layoutCaptionAndCredit` at `horizontalInset`)
|
||||||
|
are unchanged in both helpers.
|
||||||
|
|
||||||
|
### Render side — no change
|
||||||
|
|
||||||
|
`InstantPageV2MediaViews.swift` already gates clipping on the radius:
|
||||||
|
`self.clipsToBounds = item.cornerRadius > 0.0` and `self.layer.cornerRadius =
|
||||||
|
item.cornerRadius` in every media view's `update(...)`. With `cornerRadius 0` the
|
||||||
|
media view does **not** clip itself; the bubble's container does (below). No edit
|
||||||
|
to the renderer or the media views is required.
|
||||||
|
|
||||||
|
### Rounded clip is the existing `containerNode`
|
||||||
|
|
||||||
|
In
|
||||||
|
`submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift`
|
||||||
|
the `containerNode` already:
|
||||||
|
|
||||||
|
- sets `clipsToBounds = true` (init), and
|
||||||
|
- sets `cornerRadius = layoutConstants.image.defaultCornerRadius` (≈ 15–16pt) on
|
||||||
|
every layout pass.
|
||||||
|
|
||||||
|
So flush, square-cornered media at the bubble's top/bottom edge is clipped to the
|
||||||
|
bubble's rounded shape automatically. **No new clipping container is introduced.**
|
||||||
|
|
||||||
|
## Implementation detail to resolve in the plan: the 1px hairline
|
||||||
|
|
||||||
|
The pageView is positioned at `x: −1` inside the `containerNode` (an existing
|
||||||
|
hairline that hides the bubble border seam): `containerNode` is at self-(1, 1)
|
||||||
|
with width `boundingWidth − 2`, and the pageView is at containerNode-`x: −1`.
|
||||||
|
|
||||||
|
Consequence: a flush media frame at page-layout `x: 0`, `width: boundingWidth`
|
||||||
|
covers the container's clip region only to within ~1px on the right, and the
|
||||||
|
contentSize-width interaction must be handled so the bubble is not widened. The
|
||||||
|
plan must:
|
||||||
|
|
||||||
|
1. Make flush media cover the rounded `containerNode` clip region edge-to-edge
|
||||||
|
(small symmetric bleed under the rounded corners is fine — the container
|
||||||
|
clips it), leaving no 1px bubble-background sliver, **and**
|
||||||
|
2. Ensure the flush media frame does **not** inflate the layout's
|
||||||
|
`contentSize.width` beyond the bounding width (which would widen the bubble or
|
||||||
|
feed back into the suggested width). Clamp `contentSize.width` to the bounding
|
||||||
|
width if needed.
|
||||||
|
|
||||||
|
This is mechanical; the exact arithmetic is deferred to the plan.
|
||||||
|
|
||||||
|
## Out of scope / unchanged
|
||||||
|
|
||||||
|
- **V1** renderer (`InstantPageLayout.swift`, V1 `InstantPageRenderer`).
|
||||||
|
- **`.audio`** block (stays inset + r=8).
|
||||||
|
- **Captions/credits** (stay inset).
|
||||||
|
- **Streaming reveal** — media still contributes `frame.width` to the reveal
|
||||||
|
cost map; a wider flush frame slightly increases that cost (expected, benign).
|
||||||
|
- **Status node** (date/time/checks) placement — trails text only when the
|
||||||
|
bottom-most item is `.text`; media being flush does not change that logic.
|
||||||
|
- **Tap / gallery routing, edit / copy / paste round-trips** — the edit and copy
|
||||||
|
converters already skip media; nothing changes.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
No unit tests in this project (per CLAUDE.md). Verification is a full Bazel build
|
||||||
|
plus a manual check that a rich message containing an image and/or video renders
|
||||||
|
edge-to-edge with no corner rounding and a correctly-clipped rounded top/bottom
|
||||||
|
where the media meets the bubble edge, with the caption still inset.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue