mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Rich data bubble: restore bottom inset during streaming
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>
This commit is contained in:
parent
c13118921d
commit
9195cfa7ca
2 changed files with 15 additions and 0 deletions
|
|
@ -96,6 +96,7 @@ The `ChatMessageDateAndStatusNode` mirrors TextBubble's placement, adapted to th
|
|||
- **Trail the last line only when the bottom-most item is text.** `lastTextLineFrameIfLastItemIsText(in:)` (in `InstantPageV2Layout.swift`) returns the last line frame *only* when the bottom-most top-level item (max `maxY`) is a `.text`; otherwise nil, so the date wraps below all content (anchored at `contentSize.height`). For tables/images/etc. the date must not trail text buried above the final item.
|
||||
- **InstantPage draws the baseline at the line frame's `maxY`** (`InstantPageRenderer` draws each line at `lineOrigin.y + lineFrame.height`), so the visible text of a plain line sits ~5pt below `maxY`. A date that **trails** on the line (`statusHeight == 0`) adds `trailingBottomPadding` (5pt) to align with the text; a date that **wraps** onto its own line below (`statusHeight > 0`) sits at the bare `maxY`. The pad is 0 for lines taller than their font line height (an inline animated emoji, ~`pointSize·24/17`, already pushes `maxY` down). `lastTextLineFrameIfLastItemIsText` returns `(frame, trailingBottomPadding)`; the bubble applies the pad only in the trailing case.
|
||||
- **Bubble height leaves ~6pt below the date.** One unified formula for all cases: `boundingSize.height = max(boundingSize.height, statusBottomEdge + 6.0)`, where `statusBottomEdge = statusAnchorY + max(1, statusHeight)`. The `statusAnchorY` in the measure (`continue`) closure must mirror the `statusFrameY (+ streamingHeaderOffset)` in the apply closure exactly, or the date will be clipped/misplaced. 6pt matches TextBubble's bottom bubble inset.
|
||||
- **`hasDraft` adds the same 6pt at the streaming site.** The status max() above is gated by `!hasDraft`, so during streaming (status hidden, alpha=0) it can't supply the bubble's bottom inset. A separate `boundingSize.height += 6.0` inside `if hasDraft` in the SizeBlock closure does it instead — same 6pt, so the streaming bubble's bottom breathing room matches its post-stream height and there's no 6pt grow-pop when the status node fades in at finalize. The `hadDraft && !hasDraft` finalize pass doesn't need it because `!hasDraft` re-enables the status max(). If you ever refactor the `+6.0` constant out of the status max() into a `bottomInset` (TextBubble's pattern), kill this separate term at the same time — they're two ends of the same invariant.
|
||||
|
||||
## Inline custom emoji (RichText.textCustomEmoji)
|
||||
|
||||
|
|
|
|||
|
|
@ -466,6 +466,20 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
boundingSize.height += streamingHeaderOffset
|
||||
}
|
||||
|
||||
if hasDraft {
|
||||
// The bubble's bottom inset is supplied by the `statusBottomEdge + 6.0`
|
||||
// max() in the measure closure below — but that branch is gated by
|
||||
// `!hasDraft`, so during streaming the bubble has only its 1pt bottom rim
|
||||
// past `revealedContentSize.height` (= bounds.maxY + closingPad). Without
|
||||
// this, descenders of the last revealed line sit cramped against the
|
||||
// bubble's bottom edge and the bubble visibly grows by 6pt when streaming
|
||||
// ends and the status node fades in. 6pt matches the constant inside the
|
||||
// status max() (which itself tracks `TextBubble`'s `bubbleInsets.bottom`).
|
||||
// `hadDraft && !hasDraft` (the finalize pass) doesn't need this because
|
||||
// `!hasDraft` re-enables the status max(), which supplies the inset for it.
|
||||
boundingSize.height += 6.0
|
||||
}
|
||||
|
||||
let message = item.message
|
||||
let incoming = isIncoming
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue