Skip to content

Commit 5445042

Browse files
morethanwordsclaude
andcommitted
Chat: a message that changes where it stands moves to its new look instead of jumping to it
An edit, a reaction, a reply's quote coming in, a pin, a translation or a voice message's text changed a bubble at once: it took its new size in one frame and the list jumped by the difference. Now the bubble goes from how it was to how it is, and the bubbles around it with it, in the times and curves of the Android and iOS apps (ChatListItemAnimator, ChatMessageCell): across fast, up slower. - bubbleTransitions.ts: what changes is read before the change and played after it, around the ScrollSaver of `flushBubbleModifications` (and of a bubble rendered anew). The background is laid as bands and corners and stretched to its new box (`bubbleShape.ts`, now shared with sendFlights); the parts are matched by their role and move from their anchor (text from its start, time from its end); the old text stays, cut by the growing or shrinking box, until the new one takes its place; a part that went fades out as a copy, a new one fades in, reaction chips scale and their counts roll; quotes, code, reaction blocks and tags stretch their own background, the quote's mark and border with it; a change of kind (text and big emoji, media with or without a caption) crossfades the bubble as a whole; the bubbles around move by an added translate. Everything moves by transform and opacity, every read of the page goes before every write. - What goes through it: edits (text, formatting, "edited"), reactions (custom emoji and stars too), a reply and its quote loaded late, views, the replies count and the comments footer, pins, translation, voice transcription (`changeBubble`), a photo's or a file's caption coming and going. A pin now shows live: the bubbles listen to `peer_pinned_messages` and put the icon in or out of the time (a topic's unpin-all touches only its messages; scheduled and welcome chats are left out). - A streamed answer: the list follows its growth on the frame it is laid out (`followGrowth`), in a long chat by scroll and in a short one by the push of its layout, so a new line no longer moves the list in two steps. - At the reduced animation level nothing travels: sizes grow and parts fade where they stand, counts are replaced. With animations off, in a hidden tab or a chat that is not shown, a change is just made. A change that cannot be read or shown is just made too, a transition is over when the window it plays in goes (Document PiP) or a while after it should have ended, the next change waits for it, and a chat switch drops the changes queued for the old chat. - The structure a body is rebuilt by no longer counts reactions, views, forwards, the replies count and pins, so an edit after a reaction or a views change is shown changing instead of rendering the bubble anew; a reply refreshed twice keeps the latest; a reactions update for a bubble not yet in the page is no longer dropped. - copyElement.ts: copies of custom elements are inert stand-ins (`<tag>-copy`), so a copy never registers or mounts as the real one; custom emoji are copied as pictures of their frame; `drawMediaToCanvas` returns nothing for a video without a frame; the custom emoji layer measures every frame while things move (`markEmojiLayoutMoving`). - Bubble sandbox (dev only): `showBubbleSandbox()` in the console puts a bubble of the open chat into any state by click (text kinds, edited, replies with and without a quote or a late one, reactions, media and captions, voice text, views, pins, a streamed answer), in this tab only, and puts everything back on close. AGENTS.md describes it. - e2e/bubbleEditApp.spec.ts (`pnpm test:edit:app`, signed-in preview): the edit's grow and shrink with no jump of the list, the reduced level, a message out of sight, a streamed answer followed without a jump, and the sandbox walked through every state and back (no reaction shown twice, the quote mark moving, a late quote stretching). Left out: a photo changed for another photo crossfades rather than stretches; video, link previews and albums were not checked live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6c9d867 commit 5445042

23 files changed

Lines changed: 4666 additions & 525 deletions

‎AGENTS.md‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,28 @@ Either way every `rootScope.managers` call is answered from
130130
fixture without being marked `fixtureOnly` — that mark is what keeps live mode
131131
from quietly showing made-up data.
132132

133+
### Bubble sandbox
134+
135+
A bubble of the open chat put in any state by click, as many times as it takes
136+
to watch the change between two states: `showBubbleSandbox()` from the console
137+
of a signed-in dev server (`?static=0` through the tunnel). Its panel
138+
(`src/components/bubbleSandbox/`) acts on the last bubble in view, or on the
139+
ones picked (Pick, then a click; Shift adds). A state is the message as it is
140+
with parts of it changed, axis by axis (`states.ts`: text of every kind, media
141+
of the chat's own, edited, views, a pin, a forward, a reply (a quoting one, one
142+
whose quote comes late), buttons, reactions (their counts, custom emoji, stars),
143+
a voice message's text — a change of a new kind is a new axis or option there),
144+
and showing one hands the chat the events the worker would hand it for that
145+
change, in that tab only (`dispatchEventSingle`): nothing reaches Telegram.
146+
Stream has a bot's answer come in pieces at the chat's end the same way, and go
147+
when pressed again. After every
148+
change it says whether the chat changed the bubble in place or rendered it
149+
anew, and for what. Shuffle and Auto go through the ticked axes, Back returns
150+
to the state before (Space on a focused button repeats it); the animation
151+
level, the speed every WAAPI animation plays at, the direction and how an edit
152+
shows in the time are switched there too. Closing it, or leaving the chat, puts
153+
everything back. `window.bubbleSandbox` drives it from a script while it is open.
154+
133155
### Memory
134156

135157
What a tab costs, and where it went. Chrome charges a tab for every SharedWorker
@@ -634,7 +656,10 @@ import {Message, Chat, User, InputPeer} from '@layer';
634656
the hold-to-peek on photos (`components/mediaPeek`), `test:idle:app` the profile
635657
column (`components/sidebarRight`), `test:viewer:app` the
636658
media viewer's paging (`components/mediaViewer`), `test:send:app` a message
637-
flying out of the composer (`components/chat/sendFlights`), `test:calls` the calls'
659+
flying out of the composer (`components/chat/sendFlights`), `test:edit:app` a
660+
message changing where it stands as it is edited or reacted to, and the list keeping up with a streamed answer (`components/chat/bubbleTransitions`,
661+
and the shape it shares with the flight, `components/chat/bubbleShape`, and the bubble
662+
sandbox, `components/bubbleSandbox`), `test:calls` the calls'
638663
noise suppressor (`lib/calls/noiseSuppression`), `test:memory:app` what a long
639664
session leaves in memory (caches, players, registries, the workers), `test:hot:app` (with `test:popups`, and
640665
`test:calls`, whose worklet runs the registry) the dev server's hot updates (`scripts/hot-*.mjs`,
@@ -866,6 +891,7 @@ pnpm test:a11y:app # Axe past the login screen (needs PLAYWRIGHT_BASE_URL)
866891
pnpm test:idle:app # the profile sidebar goes idle once open, and slides in with its profile filled (needs PLAYWRIGHT_BASE_URL)
867892
pnpm test:peek:app # holding a photo peeks at it: chat, rich message, shared media (needs PLAYWRIGHT_BASE_URL)
868893
pnpm test:viewer:app # the media viewer: its place and header, a step pressed mid-step, paging right to left, a dead picture URL, a video's stand-in (needs PLAYWRIGHT_BASE_URL)
894+
pnpm test:edit:app # an edited message changes where it stands: its background goes from its old box to its new one, the old text gives way to the new one (cut by the background as it shrinks), the messages it moves go there without a step, all moved by the compositor and nothing left after; at the reduced level the text only gives way where it stands; a message out of sight just changes; a streamed answer moves the list up a line at a time without a step; the bubble sandbox walks a message through every state it has — its reactions coming and going (none shown twice), their counts, a reply (a quoting one stretching with its mark, one whose quote comes late), buttons, a forward, media, a photo's caption going and coming and a pin shown changing too — and puts it back (sends to Saved Messages, edits and deletes; needs PLAYWRIGHT_BASE_URL)
869895
pnpm test:send:app # a sent message flies out of the composer and lands on its bubble, on the very pixels it is drawn on, moved by the compositor (nothing of it restyled while it flies) while the list makes room smoothly; its custom emoji fly with it, drawn where they stand; a sticker or a GIF from the panel and the big emoji fly themselves, held still on the frame they were chosen at; nothing flies at the reduced level (sends to Saved Messages and deletes; needs PLAYWRIGHT_BASE_URL)
870896
pnpm test:calls # the calls' noise suppressor: RNNoise in its AudioWorklet, in Chromium (~30 s)
871897
pnpm test:memory:app # a session played several times: no isolate's heap, DOM or renderer allocator keeps growing, idle lottie heaps go back (needs PLAYWRIGHT_BASE_URL of a --static preview; ~7 min)

0 commit comments

Comments
 (0)