Skip to content

fix(chat): settle the chats list in one update after launch - #947

Merged
bmc08gt merged 3 commits into
mainfrom
fix/chat-list-launch-sync
Oct 2, 2026
Merged

bmc08gt merged 3 commits into
mainfrom
fix/chat-list-launch-sync

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

On cold launch the chats list painted from cache and then kept changing: rows reordered, previews updated one at a time, and unread counts popped in. The list should be current the first time it settles, with only new events changing it after that, and launch must not get slower.

Why it churned

  • start() and the .active hook in AppDelegate each ran loadFeed(). .active fires on cold launch too, so every launch made two full feed loads and two backfill passes.
  • The contact DM, tip DM and group feeds each applied on their own and re-sorted the list.
  • Backfill rewrote previews per chat after the list was showing.
  • resolveUnreadCount ran a GetMessage per row from the row's .task.

What changes

  • The cached list still paints immediately and is interactive.
  • loadFeed() shares one in-flight task, so launch, foreground and reconnect make one fetch per feed type.
  • Once all three feeds return, they apply in one non-animated update, with unread counts resolved at width 4 inside it. The wait is capped at 2s for a slow feed and 1.5s for unread (FeedReconcileTiming.launch); past a cap, whatever has landed is applied. The reconcile does not wait on backfill. The feed carries the read pointer's message id but not its unreadSeq, so counts can't be computed locally.
  • Backfill runs after the reconcile through BackfillQueue, and an opened chat jumps the queue. The open chat shows its cached transcript at once and shows the placeholder only when it is empty.
  • Writes are newer-wins. The store keeps a stored lastMessage and lastActivity when a feed copy is older, and advanceLastActivity ignores a date that isn't later. A test caught a delayed feed response rolling back a row that a stream event had just advanced.
  • getConversations() takes the newer of a row's activity and its newest stored message. The notification extension persists message rows but never updates the conversation row, so a chat that got a push while the app was closed used to launch at its old position.
  • An empty store keeps the existing progressive behaviour.

Tests: ChatListLaunchSyncTests (one reconcile with counts populated, no wait on backfill, older data doesn't move a row, empty store isn't held, one fetch per feed type), plus BackfillQueueTests, BoundedWaitTests, and store and database round-trip cases.

If the server deletes a chat's latest message, the row keeps its later activity and position, a side effect of the newer-wins rule.

Android counterpart: code-payments/code-android-app#1665.

…elaying it

On cold launch the cached chats list painted, then changed: the feed loaded
twice, its three parts applied separately, backfill rewrote previews one by
one, and unread counts popped in per row.

- The cached list paints and stays interactive immediately.
- start() and the foreground hook share one in-flight loadFeed(), so each
  feed type is fetched once.
- Once all three feeds return, one non-animated update applies them with
  unread counts resolved (width 4) inside it. The wait is capped at 2s for a
  slow feed type and 1.5s for the unread pass; past a cap, what has landed is
  applied. It does not wait on backfill.
- Backfill runs after the reconcile through BackfillQueue, promotes the open
  chat, and is idempotent: a row's preview, lastActivity or order changes only
  for a genuinely newer message. The store keeps a stored lastMessage and
  lastActivity that are newer than a feed copy.
- An empty store keeps the progressive behaviour.
- The open chat shows its cached transcript at once, with a placeholder only
  when it is empty.
- getConversations() takes the newer of a row's activity and its newest stored
  message, so messages persisted by the notification extension position the
  chat correctly at launch.
KnownAuthorDirectory persisted names but its snapshot stayed empty until a
.task ran after the cached list painted, so previews drew without sender
prefixes and refilled about a second later. Read the table at session start
and land it when the Chats screen appears, beside the conversation cache.
…om onAppear

The preload read finishes in about 3ms, but the Chats screen only applied it
from onAppear, and applied the conversation cache first. The rows rendered once
with an empty author table before it landed, so group previews drew without
their sender prefix. The preload now applies itself on the main actor when it
finishes, and onAppear takes the authors before the conversation cache.
@bmc08gt
bmc08gt merged commit d220a2a into main Oct 2, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the fix/chat-list-launch-sync branch October 5, 2026 15:16
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