Skip to content

feat(chat): show typists' avatars ahead of the group typing indicator - #844

Merged
bmc08gt merged 3 commits into
mainfrom
feat/typing-indicator-avatars
Sep 24, 2026
Merged

bmc08gt merged 3 commits into
mainfrom
feat/typing-indicator-avatars

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Group chats now show who is typing: an overlapping stack of the typists' avatars sits to the leading side of the dots bubble. DMs and tip chats draw the bubble alone, as before.

The stack matches Android's TypingIndicator from code-payments/code-android-app#1544:

  • 24pt avatars, the same as the sender avatar beside a group's messages, overlapping by 8pt with a 4pt gap to the bubble. A member without a picture shows initials.
  • The newest 3 typists, oldest at the back on the left and newest on top at the right.
  • Each avatar has a 2pt transparent ring cut where the one above it sits, so overlapping faces stay apart over any background.
  • A newcomer grows from 0.4 scale at its left edge in 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. The dots are unchanged.

How it's wired

  • ConversationTyping records when each typist started as well as their expiry deadline. A STILL heartbeat extends the deadline but keeps the typist's place, so the order is by when they started typing, like Android's sort on since. typists(in:) returns them oldest to newest; isCounterpartTyping(in:) stays as it was.
  • ChatItem.typingIndicator carries typists: [ChatAuthor]. ConversationLoadCoordinator fills it only when the transcript attributes its rows (groups) and caps it at ChatItem.maxTypingAvatars before it enters Inputs, so dropped typists are neither diffed nor fetched. Typists feed the same attribution path as message senders, so a typist the roster doesn't cover is fetched the way an unknown sender is.
  • The item's id ignores typists, so a typist joining or leaving reconfigures the row in place instead of deleting and reinserting it. ChatTypingIndicatorCell keys its avatars on user id, so an avatar keeps its place when its picture arrives.
  • The slots, presence and count run on a display-link spring at Compose's StiffnessMediumLow with no bounce, mirroring Android's Animatables. The rings are recut from the live positions every frame, which a UIView animation block can't drive.

ChatItem is Codable, but the only persisted copy is the notification preview cache, which holds message rows only, so no cache or schema bump is needed.

Tests cover typist ordering, heartbeat ordering and expiry in ConversationControllerTests, and the coordinator emitting avatars for a group, none for a DM, and the newest 3 when more are typing in ConversationTypingIndicatorTests.

In a group chat, the typing bubble now has an overlapping row of the
typists' avatars on its leading side, matching Android's TypingIndicator:
40pt circles overlapping by 20pt, a 10pt gap before the bubble, the newest
10 typists, and the earliest typist drawn on top. Avatars fade and scale in
and out as people start and stop typing, and the row's width animates.

ConversationTyping now records when each typist started, so a STILL
heartbeat extends the deadline without moving them in line, and exposes
typists(in:) oldest to newest. The coordinator carries them on
.typingIndicator(typists:) only for group transcripts; DMs and tip chats
keep an empty list and draw the bubble alone. The item's id ignores the
typists, so a typist joining reconfigures the row instead of reinserting it.
@bmc08gt bmc08gt self-assigned this Sep 23, 2026
code-android-app#1544 settled on a different stack from the one the first
commit ported. This follows it:

- 24pt avatars, the transcript's sender avatar, overlapping by 8pt with a
  4pt gap to the bubble. A member without a picture shows initials.
- The newest 3 typists, oldest at the back on the left and newest on top
  at the right.
- Each avatar has a 2pt transparent ring cut where the one above it sits.
- A newcomer grows from 0.4 scale at its left edge in its final slot. A
  leaver drops beneath the stack as its neighbours slide over it. The
  width follows the animated count, so the bubble slides.

The slots, presence and count run on a display-link spring at Compose's
StiffnessMediumLow with no bounce, since the rings are recut from the
live positions every frame and a UIView animation block cannot drive
them. ChatAuthorAvatarView goes back to its single 24pt form.
…-avatars

* origin/main:
  feat(chat): mark where unread messages start with a divider (#845)
  feat(chat): thank the sender when a cash link from chat is collected (#843)
  fix(notifications): treat a lost WAL lock race as contention in the extension store (#842)

# Conflicts:
#	FlipcashCore/Sources/FlipcashCore/Models/Chat/ChatItem.swift
@bmc08gt
bmc08gt merged commit bd1b260 into main Sep 24, 2026
1 check passed
@bmc08gt
bmc08gt deleted the feat/typing-indicator-avatars branch October 5, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant