Skip to content

fix(sidenav): show the skeleton, not "No Chats Yet", while sessions load - #985

Merged
philmerrell merged 1 commit into
developfrom
fix/sidenav-session-list-loading-state
Sep 6, 2026
Merged

fix(sidenav): show the skeleton, not "No Chats Yet", while sessions load#985
philmerrell merged 1 commit into
developfrom
fix/sidenav-session-list-loading-state

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

The regression

On a cold load the sidebar rendered the "No Chats Yet" empty state for the whole duration of the sessions fetch, then popped the list in. It should have shown the loading skeleton.

Root cause

isLoading() in session-list.ts tested sessionsResource.value() === undefined. That stopped being true during the fetch, for two reasons that compound:

  1. The loader short-circuits to null. sessionsResource's loader returns null when sessionsRequest is false, so the resource resolvesundefinednull — before a single session is fetched.

  2. SessionService is constructed before the BFF bootstrap resolves. Its constructor only calls enableSessionsLoading() if bffSession.isAuthenticated() is already true, and the comment there assumes the service is instantiated post-bootstrap. That no longer holds: provideAppInitializer(() => { inject(AnnouncementModalService); }) pulls in MessageMapService, which injects SessionService. Angular's runInitializers invokes every initializer synchronously in registration order, collecting promises and only Promise.all-ing them afterwards — so that initializer runs while SessionService.bootstrap() is still in flight, isAuthenticated() is false, and the eager-fetch branch is skipped.

The auth effect then enables loading and calls reload() — but Angular's resource preserves the previous value across a reload (status projects to reloading, the stream is kept). So value() stayed null for the entire real fetch: isLoading() read false, groupedSessions() was empty, and the template fell to the empty-state branch.

The fix

isLoading() now means "we have nothing to draw yet":

  • No API response — undefined before the first load resolves, or the short-circuit null — and no rows from the local cache.
  • An empty sessions array is a real response and still falls through to the empty state.
  • error() is checked first: reading value() on an errored resource throws, which the old ordering walked straight into.

Deliberately not keyed on status() === 'reloading'refreshSessions() reloads after send/rename/mark-unread, and that would have flashed the whole list to a skeleton every time.

Testing

Four new specs covering the short-circuit null, the genuinely-empty response, the locally-cached rows case, and the error path. Verified they are not vacuous: reverting the implementation makes 2 of them fail.

  • ng test — 2500 passed, 221 files
  • tsc --noEmit -p tsconfig.app.json — clean

Follow-up, not in this PR

The underlying fragility is that SessionService's constructor gates on auth state that may not exist yet. This fix makes the UI correct under any construction order, but the service would be more honest with a params gate on the resource (undefinedidle) instead of a loader that resolves null. That changes logout/idle semantics, so it doesn't belong in a regression fix.

🤖 Generated with Claude Code

`isLoading()` tested `sessionsResource.value() === undefined`, which stopped
being true during the cold-start fetch.

The loader short-circuits to `null` when `sessionsRequest` is false, so the
resource *resolves* before any session is fetched. That path is now the
ordinary one: `SessionService`'s constructor only enables loading if the BFF
session is already authenticated, but the service is constructed during the
APP_INITIALIZER pass — `AnnouncementModalService` -> `MessageMapService` ->
`SessionService` — and Angular's `runInitializers` invokes every initializer
synchronously before awaiting any of them, so `bootstrap()` is still in flight
and `isAuthenticated()` is false. The auth effect enables loading afterwards,
but `reload()` preserves the previous value, so `null` survives the whole real
fetch. `isLoading()` read false, `groupedSessions()` was empty, and the sidebar
rendered the empty state until the response landed.

`isLoading()` now means "nothing to draw yet": no API response (`undefined`
before the first load resolves, or the short-circuit `null`) and no locally
cached rows. An empty `sessions` array is a real response and still falls
through to the empty state. `error()` is checked first — reading `value()` on
an errored resource throws, which the old ordering walked into.

Deliberately not keyed on `status() === 'reloading'`: `refreshSessions()`
reloads after send/rename/mark-unread, and that would flash the list to a
skeleton every time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@philmerrell
philmerrell merged commit 0bba863 into develop Sep 6, 2026
4 checks passed
@philmerrell
philmerrell deleted the fix/sidenav-session-list-loading-state branch September 6, 2026 20:11
@philmerrell philmerrell mentioned this pull request Sep 6, 2026
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