Skip to content

feat(chat): archive chats and filter the chat list by All, Unread and Groups - #1669

Merged
bmc08gt merged 40 commits into
code/cashfrom
feat/chat-archive
Oct 3, 2026
Merged

bmc08gt merged 40 commits into
code/cashfrom
feat/chat-archive

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Adds chat archiving and All / Unread / Groups filter chips to the chat list, matching iOS in code-payments/code-ios-app#948 and the restyle in code-payments/code-ios-app#949.

Archiving

A swipe on a chat row now shows Archive as the inner action, with Mute staying the outer full-swipe action. Archiving shows a "Chat archived" toast with Undo. Archived chats move to an Archived chats screen, reached from an "Archived" row at the top of the list; a swipe there unarchives. Archived chats stay archived when new messages arrive. The Archive rows on the DM and group profile screens from the original plan are left out, as on iOS.

Archive state is local. It lives in a new chat_archive table (Room 40 → 41, auto-migration, 41.json committed), read through ChatArchiveStore. FeedSyncDelegate splits the feed into main and archived lists in one read (feedWithArchived), so archiving the last visible chat can't flash the empty state between two emissions. Leaving a chat or erasing the account removes its record. The group invite picker no longer offers archived chats, as on iOS.

Filter chips

The chips are the list's first item and start scrolled just out of view. A pull from the top brings them in with the finger, and on release they settle on the chips or just past them in one fling from the nested-scroll pre-fling, rather than a second scroll after the overscroll. A fling from mid-list stops at the first chat. Once revealed they stay, including after opening a chat, and they always show while Unread or Groups is selected. A trailing filler keeps short lists able to park the chips out of view.

The "Archived" row shows only under All. Filter counts and the row's count come from projectChatList, which chat_archive.json tests against the same cases iOS uses.

Notifications

  • Archived and muted never notifies.
  • Archived and unmuted alerts normally only for an @mention of the viewer or a reply to the viewer's message. Encrypted DMs are decrypted before that check. Cash alone does not count.
  • Any other archived push is posted silently (setSilent, no sound, vibration or heads-up) in the background, and not posted in the foreground. iOS delivers these as passive notifications.

The archive check runs after authenticateIfNeeded, so a push that cold-starts the app reads the signed-in user's database. Muted chats that aren't archived behave as before: Android still drops them, where iOS posts them passively.

Toast

CodeSnackbar is replaced by FloatingToast, hosted once at the app root and reached through LocalFloatingToastHost. It draws on the nav bar's glass, now shared through Modifier.floatingGlass, and is capped at the bar's width. A new toast replaces the current one. It is dismissed while a sheet, the bill or a bottom-bar prompt is up, since those draw inside each nav entry and the root toast would otherwise sit on top of them. The developer-mode countdown on the menu tab uses it too, with taps passing through to the version label. CodeScaffold no longer has a snackbarHost slot.

The shared design is in code-payments/flipcash-client-orchestrator#33.

bmc08gt added 30 commits October 2, 2026 17:07
The sources test module has no Robolectric, so the test covers only the no-database paths. The read/write paths are exercised over a real database by the archive store's test.
…mentioned or replied to

An archived chat's message that neither @mentions the viewer nor replies to
one of their messages is posted quietly (no sound, vibration or heads-up)
while the app is in the background, and not posted while it is in the
foreground, as iOS delivers it passive. Muted chats still post nothing.
…rypted DMs first

The archive read and the mention/reply classification ran before
authentication, when a cold start from a push has not opened the per-user
database or loaded the viewer's profile, so an archived chat's push posted
loudly. The planner now exposes planNotification, which the post path calls
after authentication to re-decide.

The classifier read inlined DM content as received, so a mention or reply in
an encrypted DM never counted. It now opens the message through the new
ChatCoordinator.openPushedChatMessage first.
…M throws

The classifier now treats a throw from openPushedChatMessage as an unopened
message, so the push is classified as not addressed to the viewer and posts
silently instead of being dropped by execute's outer catch. Also moves the
postNotification silent KDoc back onto postNotification.
7dp vertical padding and the count at full opacity, as iOS draws them.
ChatsViewModel combines the main and archived feeds and runs them through projectChatList, so the chips, the Archived row and their counts come from the one tested function. The selected filter and whether the chips have been revealed live in the ViewModel, so both survive opening a chat and coming back. Row mapping moves to ConversationMapping so the archived feed maps the same way.
… list

The chips are the list's first item and the list opens on item 1, so they sit under the bar until a pull from the top brings them on. ChipRevealConnection settles the release in onPreFling with one spring that starts at the finger's velocity, and stops any scroll toward the top that did not start there at item 1. The settle decision is chipSettle, a pure function.

A row's swipe now offers Archive inside and Mute outside; archiving shows a Chat archived snackbar with Undo. The Archived row shows under All when anything is archived; its tap is a no-op until the Archived chats screen lands.
… are on

Parked, the chip row lies at zero alpha under the title bar, so a tap on the bar's lower edge reached an invisible chip and slid the row in. Touches now stop at the row while it is hidden; its semantics stay, so a screen reader can still reveal it.

A reveal was recorded only once the settle spring came to rest, 300-500ms after the chips were fully on. A touch in that time cancelled the spring before the reveal was recorded, and the next scroll toward the top parked the chips again. The reveal is now recorded when the chips reach the top, or when a cancelled settle leaves more than half the row on screen.

A list restored on the chips with a fresh ViewModel now counts them as revealed, and the Archived row's icon sits in the avatar column instead of one inset further in.
With only a few chats, everything after the chip row was shorter than the viewport, so the list could not hold item 1 at the top. It fell back to index 0 on the first frame, so the chips showed and the list could not scroll. iOS parks regardless by setting the scroll distance to the chips' height.

A last item now makes up the difference: the viewport less the height of every item after the chips. Each of those items reports its height as it is measured. The list measures forward from item 1, so in a short list the filler's height is already right in the first pass. It takes nothing once the list is longer than the screen, and nothing while any row has not been measured. So the list never scrolls further past its last row than the chip row's height.
ChatsViewModel combined chatListFeed() and archivedChatListFeed(), two
separate maps of the chat state with no dedupe. Archiving the last main
chat could emit the new empty main list before the archived list caught
up, flashing the no-chats prompt ahead of the Archived row, and every
unrelated state change emitted twice.

FeedOperations.feedWithArchived() builds both lists from one ChatState
read and drops repeats; the Chats screen reads it through
chatListFeeds(), first frame included.
If applying archive to a push threw, the whole notification was dropped.
Fall back to posting it unchanged and trace the failure.

PushMessageClassifier's runCatching around the suspending
openPushedChatMessage also swallowed CancellationException; rethrow it.
Comments cited rules 1, 2, 3 and 5, which nothing in the repo defines.
Also correct ChatListEntry.id (the app passes the base58 chat id) and
note that tabBadge is read only by the shared fixture tests on Android.
Archiving a chat fades its row out and slides the rows below into the gap; Undo fades it back in. Filter changes animate the same way, and the empty states fade so their message doesn't land on rows still fading out. The Archived row fades inside its always-present slot. The Archived chats screen animates an unarchived row the same way and keeps its empty state in the list, so the last row fades out rather than the list being swapped for the message.
A solid white selected chip was the brightest thing on the dark chat list.
Selected is now white at 16% with the label in the main text colour;
unselected has no fill and uses the secondary text colour. Both colours
animate on change.
CodeSnackbar was a full-width Material snackbar while iOS floated a
capsule. It is now the pill both platforms share: surfaceVariant fill
with a 0.5dp white 10% border, an optional 18dp icon, the message, and
the action in its own white 12% pill.

CodeSnackbarHost replaces Material's SnackbarHost, whose fade and scale
cannot be turned off. It slides the toast up and fades it in, leaves the
same way, and dismisses on a swipe down. A short toast stays 4 seconds,
extended by the accessibility timeout. showSnackbar(SnackData) now
replaces the toast on screen instead of queueing. The chat list passes
the archive icon to its undo toast.
Snackbars now draw through it: the bar's glass instead of the surfaceVariant pill and hairline, an 18dp leading icon in the main text colour, and the bar's horizontal inset instead of 12dp. FloatingToastHost adds a host that can rise out of the bar, hug its message, and pass taps through.
bmc08gt added 10 commits October 3, 2026 06:15
The toast is hosted at the app root, outside the haze source and under the bar in z, so its glass blurs the list and it grows up from behind the bar. Bar visibility moves into AppNavigationBarVisibility so the bar and the toast read the same route, modal, bill and card-expansion gate. With no bar on screen the toast slides up from the bottom.
The countdown, unlocked and already-unlocked messages leave the system Toast for FloatingToast. The toast hugs the message, swaps it in place on each tap, and takes no pointer input, so the version taps beneath it keep counting.
…pt is up

The root toast host draws above the nav content, so a toast shown before a sheet, the bill or a BottomBarManager prompt opened stayed on top of it and took its taps (archive a chat, then open Mute on another within 4s). Covering the host now dismisses the toast on screen, and a toast shown while covered returns Dismissed without appearing, so the Menu countdown's next tick cannot bring it back over a sheet.
CodeSnackbar, CodeSnackbarHost and SnackData.showSnackbar had no callers after the move to FloatingToast; CodeScaffold's default snackbar host is now empty.
It sat above AppNavigationBarVisibility's own KDoc; AppNavigationBar already carries a fuller copy.
The theme's Medium weight resolves to the Demi file, and the message had
been forced to Normal, which drew the Regular cut and read as a different
face from the rest of the app.
Replaces the grow-out-of-the-bar entrance. One progress value drives both
the offset and the alpha, so leaving plays the entrance backwards.
* origin/code/cash:
  build: update release manifest
  fix(chat): settle the chats list in one update after launch (#1665)

# Conflicts:
#	apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/ChatsViewModel.kt
#	apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/FeedSyncDelegate.kt
Toasts go through the app's root FloatingToastHost, and nothing passed a host
to this slot any more.
@bmc08gt bmc08gt self-assigned this Oct 3, 2026
@github-actions github-actions Bot added area: ui Compose UI, theme, components, resources area: notifications Push notifications, in-app messaging type: feature New functionality labels Oct 3, 2026
@bmc08gt
bmc08gt merged commit a7467eb into code/cash Oct 3, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/chat-archive branch October 3, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: notifications Push notifications, in-app messaging 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