Feat/inbox scope tab lands first row - #611
Merged
Merged
Conversation
Switching between Direct chat and Workspace chat refetched the list but left the conversation pane alone, so the previous scope's chat stayed open beside a list it no longer belonged to — a Direct conversation showing while "Workspace chat" read as selected. The landing that opens a conversation on first load is a `once` on the list's `eod`, consumed by the first page and never re-armed. _setRoomScope now re-arms it per switch, via _armScopeLanding. Armed AFTER list.restart(), which is load-bearing: restart() triggers `eod` synchronously to flush stale listeners before it calls start() (ui-core letc/widgets/list/index.js), so a handler armed before the call burns on that flush against the old page and the real one arrives with nothing listening. The scope is captured at arm time and re-checked both when `eod` fires and after the widget_chat wait, so a fast second tab press wins instead of letting the page it left open a row. Mobile/tablet stays on the inbox, as the first-load landing already does: the user just tapped a tab there, and opening flips data-mview to "chat" and hides it. An empty scope clears the pane to the header's `--empty` variant on every screen size, dropping chatWidget so a stale widget_chat cannot keep acknowledging messages in a scope nobody is looking at. _landingRow also learns to tell the two senses of "placeholder" apart: `is_placeholder` marks the support row we draw (a real conversation), while an empty list carries the smart list's own NO_CONTACT note in `children`. Only the first is landable, so the note is excluded by requiring an entity_id — latent before, since openChat ran on the note until _openConversation bailed on a missing hub_id, and load-bearing now that a scope switch can land on an empty list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Switching scope tabs is a refetch: restart()'s start(1) empties the
collection synchronously and only then fetches, so the inbox column goes
blank for a round trip while the conversation beside it waits to be
replaced by the new scope's first row. The first mount has the same gap
and no cover at all — the list's spinner option draws nothing in ui-team,
since ui-core's spinner injects markup with no base styles here.
Both panes now hold their shape, using the house primitives
(mixins/drumee skeleton-layer / skeleton-content-in), so the pulse
follows the theme and stands down under prefers-reduced-motion.
Gated on two stamps on __ui, the widget's own root — never a lazy
placeholder, so a kind that has not resolved cannot read as "ready":
data-list-state loading|ready, seeded to loading so the FIRST paint is
already the skeleton rather than a blank 400px column.
data-chat-mounted 0|1, paired with :not(:has([data-painted="1"])).
widget_chat stamps itself once its messages are ready,
so CSS alone lowers the pane skeleton with nothing to
watch it. The flag supplies what CSS cannot infer: a
pane with no painted child because it is LOADING versus
one that is empty on purpose (_clearConversation), which
must not pulse forever.
The dataset keys are hyphenated deliberately. onRender applies that map
with setAttribute("data-" + key) verbatim, with none of the camelCase ->
kebab folding el.dataset does, so `listState` would have landed as
data-liststate and no selector written the obvious way would have matched.
A 6s deadline lowers the inbox skeleton regardless, since `eod` is the
only thing that lowers it and a dropped request never fires one. Armed on
first paint as well as on every switch — the first fetch is the one case
with no tab press behind it to re-arm anything.
Forced by the stamp: the first-load eod handler gains the scope capture
_armScopeLanding already had. restart() fires eod synchronously as a
listener flush, so pressing a tab while the first page is still in flight
detonated that handler against the list the user just left. Latent before
(it opened the wrong row); visible now, as a flash of empty list.
Verified by compiling the skin standalone and rendering the real class
chain headless: the mask's first avatar lands at x=28 y=28 48x48 against a
real row's x=28 y=28 48x48, and its fifth at y=108 against the second
row's y=108, confirming the 80px pitch. All three states check out —
skeleton up while loading, down on ready, and absent over a deliberately
empty pane.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The inbox column and the conversation pane could never overlap. A tab
press raised the list skeleton; the pane kept the OLD conversation, still
carrying data-painted, so its skeleton stayed down; `eod` then lowered the
list skeleton; and only then did _openConversation clear the pane and
raise the pane skeleton. Two loading states in sequence, so the screen
resolved in two steps with a blank pane beside an uncovered list in
between.
Now the inbox waits for the conversation, which is the slower of the two.
The list holds its skeleton even once its own rows have arrived, and both
columns reveal on the same frame.
data-list-state and data-chat-mounted collapse into a single data-loading
on __ui. One attribute rather than one per region is what makes the two
move together: a single flip raises both and a single flip lowers both, so
their pulses start in phase and end together by construction, with nothing
to keep in sync. data-chat-mounted's old job — telling "loading" apart
from "empty on purpose" — is just data-loading="0", and the
:has([data-painted]) gating leaves the stylesheet entirely.
_raiseSkeletons also clears the pane, which _openConversation used to do a
round trip later. Until the previous scope's widget_chat goes there is a
painted conversation in the pane and nothing for a skeleton to cover.
Nothing is wasted: _openConversation cleared it anyway.
Lowered on the conversation's paint. widget_chat stamps data-painted but
announces it to nobody, so the DOM is the only place that fact exists —
hence a MutationObserver scoped to the pane and to that one attribute,
disconnected as soon as it fires. Deliberately NOT the CSS condition it
replaces: "the pane has no painted child" is equally true while the user
is merely clicking between conversations, and would have dragged the inbox
list under a skeleton on every click, taking the row they just clicked
with it. The flag is raised only for a scope load.
Lowered directly on the four paths where nothing will ever paint: an empty
scope, an account with no conversations, either mobile bail, and the 6s
deadline.
Verified headless with getAnimations({subtree:true}) over both regions:
one flip, both pulses running with an identical startTime, and both gone
after the second flip. The animation clock is frozen under virtual time,
so equal startTime plus identical duration is the real evidence of phase,
not the elapsed-time sample. Screenshot confirms both columns up together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


No description provided.