fix(call): mirror SSR node structure so the lobby call button hydrates

CallPanel's hydrate body wraps the join button in a dynamic child
(`{move || if in_call {..} else {button}}`), which serializes hydration
marker comments; the SSR body rendered a *static* button (plus a
`disabled=true` the hydrate button lacks). That structural mismatch meant
`on:click` never attached on the first lobby load — the call button was dead
until a client-side nav re-rendered the subtree. Mirror the dynamic-child
wrapper and the button's serialized attributes in the SSR branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-10-03 10:16:50 +02:00
co-authored by Claude Opus 4.8
parent cc88f3c325
commit ede1b8497f
+17 -6
View File
@@ -472,19 +472,30 @@ fn CallPanel(room: Memo<String>, me: String) -> impl IntoView {
}
.into_any()
}
// Must mirror the hydrate branch's default (not-in-call) markup exactly -
// hydration reconciles this SSR output against what the hydrate branch
// above expects to find, and an empty div here (vs. the button hydrate
// wants) is a hydration mismatch that panics and traps the whole wasm
// instance, killing all reactivity on the page.
// Must mirror the hydrate branch's default (not-in-call) markup AND its
// node structure exactly. The hydrate branch puts the button inside a
// dynamic child (`{move || ...}`), which serializes hydration-marker
// comments around it; if SSR emits a plain static button instead, the
// hydrate walk can't find the dynamic node, so it never attaches
// `on:click` - the call button is dead on the first lobby load until a
// client-side nav re-renders the subtree. So: same `{move || ...}` wrapper,
// and only the attributes the hydrate button actually serializes to
// (class/title/aria-label; `on:click` is a JS listener, not HTML). No
// `disabled` here - the hydrate button has none and hydration won't clear
// a stray SSR-only attribute.
#[cfg(not(feature = "hydrate"))]
{
let _ = (room, me);
view! {
<div class="call-panel">
<button class="call-join" title="join call" aria-label="join call" disabled=true>
{move || {
view! {
<button class="call-join" title="join call" aria-label="join call">
"☎"
</button>
}
.into_any()
}}
</div>
}
.into_any()