Skip to content

Feat/inbox scope tab lands first row - #611

Merged
tranh0anghuan merged 3 commits into
testfrom
feat/inbox-scope-tab-lands-first-row
Sep 19, 2026
Merged

tranh0anghuan merged 3 commits into
testfrom
feat/inbox-scope-tab-lands-first-row

Conversation

@tranh0anghuan

Copy link
Copy Markdown
Collaborator

No description provided.

Drumee Dev and others added 3 commits September 19, 2026 06:32
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>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@tranh0anghuan
tranh0anghuan merged commit 01b0143 into test Sep 19, 2026
8 checks passed
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@tranh0anghuan
tranh0anghuan deleted the feat/inbox-scope-tab-lands-first-row branch September 21, 2026 03:02
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