Skip to content

feat(chat): show typists' avatars in a group's typing indicator - #1544

Merged
bmc08gt merged 6 commits into
code/cashfrom
feat/typing-indicator-avatars
Sep 24, 2026
Merged

bmc08gt merged 6 commits into
code/cashfrom
feat/typing-indicator-avatars

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

TypingIndicator has drawn an overlapping avatar row ahead of its dots for a while, but its one call site in ChatBottomBar never passed userImages, so the row never appeared. This wires it up for group chats.

Component. TypingIndicator now takes typists: List<T>, a key and an avatar slot instead of userImages: List<Any>. The slot draws what goes inside each circle, and the stack handles the rest:

  • Avatars are 24dp with an 8dp overlap and a 4dp gap to the bubble, matching the sender avatar beside a group's messages.
  • The last 3 typists show, oldest at the back on the left and newest on top at the right.
  • Each avatar cuts a 2dp transparent ring out of the ones beneath it, so overlapping faces stay separated over the blurred transcript as well as over a plain background.
  • A newcomer grows from its left edge at its final slot. A leaver drops beneath the stack while its neighbours slide over it. The stack's width follows the animated count, so the bubble slides instead of jumping.

This replaces the LazyRow. It clipped avatars on their way out, dropped a leaving avatar from its width before the fade finished, and put the oldest typist on top.

The component can't draw member pictures itself: they're MediaItems rendered by ContactAvatar, which lives in :apps:flipcash:shared:common-ui, and :ui:components can't depend on that module. PreviewTypingIndicator scripts joins and leaves with placeholder avatars.

Chat. ChatViewModel derives State.typingAvatars from typists and the sender profiles the transcript already uses. The logic is in typingAvatars() (TypingAvatars.kt):

  • Typists are sorted by since ascending, so takeLast(3) keeps whoever started most recently.
  • Each entry is a TypingAvatar(userId, profile), keyed on the user id's hex string. The key stays the same when the profile resolves, so the avatar doesn't animate out and back in.
  • A typist the roster doesn't cover is passed to requestSenderProfile, the same way an unknown sender is.
  • A CONTACT_DM or TIP_DM gets an empty list and keeps the dots on their own.

ChatBottomBar fills the slot with ContactAvatar(image, displayName, BlobAccessContext.profile(userId)), which is how MessageRow draws a sender. A member with a picture gets the same URL re-minting and BlurHash fallback as in the transcript, and a member without one shows initials. A typist whose profile hasn't resolved yet shows the unknown-person avatar.

TypingAvatarsTest covers group ordering by since, an empty list for a DM and for a tip chat, removal of a typist who stops, and a key that holds when the profile resolves.

ChatBottomBar called TypingIndicator without userImages, so its avatar
row never drew. The ViewModel now derives one entry per typist, oldest
first, from the sender profiles the transcript already resolves, and the
bottom bar passes them in. DMs and tip chats get an empty list and keep
the dots on their own.

An entry is a picture URL from MediaUrlResolver, or the typist's user id
when there is no picture or no profile yet, which UserAvatar draws as
the Person fallback.
@bmc08gt bmc08gt self-assigned this Sep 23, 2026
@github-actions github-actions Bot added the type: feature New functionality label Sep 23, 2026
TypingIndicator now takes the typists, a key and an avatar slot rather
than a list of image models. The row keeps its size, overlap, clip and
animation; what goes in each circle is the caller's.

The chat fills the slot with ContactAvatar, the same way the transcript
draws a sender, so a member's picture goes through MediaUrlResolver's
re-mint and BlurHash fallback, and a member without one shows initials.
An unresolved profile draws the unknown-person avatar. Items are keyed
on the user id, so an avatar no longer animates out and back in when a
profile resolves. MediaUrlResolver comes back out of ChatViewModel.

The row shows the last 3 typists, down from 10.
@github-actions github-actions Bot added the area: ui Compose UI, theme, components, resources label Sep 23, 2026
The LazyRow clipped avatars on their way out (its own scroll clip and
animateContentSize both clip), dropped a leaving avatar from its width
before the fade finished, and put the oldest typist on top.

AvatarStack replaces it. Each avatar keeps an animated slot and
presence, the stack's width follows the animated count, and the newest
typist sits on top at the right. A newcomer grows from its left edge
at its final slot, starting at 0.4 scale so it stays inside the
stack's width and clear of the bubble. A leaver drops to the bottom
while its neighbours slide over it.

Each avatar cuts a 2dp transparent ring out of the ones beneath it,
rather than painting a ring in a fixed color, so the separation holds
over the blurred transcript. Avatars are 24dp with an 8dp overlap and a
4dp gap to the bubble, matching the sender avatar beside a group's
messages.

The preview now scripts joins and leaves so it can be run and recorded.
The AnimatedContent around the indicator shrinks to nothing on exit and
clips to its bounds while it does, so the bubble and avatars were cut
off partway through their fade. SizeTransform(clip = false) lets the
indicator fade out whole. The preview's transition gets the same change.
A typist whose profile hasn't loaded now goes through the same
ContactAvatar call as a resolved one, with no picture and an empty
name, instead of the unknown-person glyph.
InitialsText fell back to "?" when the name had no letters. It now
draws nothing, matching iOS, which shows only the gradient.
@bmc08gt
bmc08gt merged commit fc91215 into code/cash Sep 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ui Compose UI, theme, components, resources type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant