8 Commits

Author SHA1 Message Date
bl 080b9c46d6 chore: Release cnats version 0.2.4
Release / build (x86_64, ubuntu-latest) (push) Successful in 7m14s
Release / docker (push) Successful in 4m32s
Release / build (aarch64, aarch64) (push) Successful in 14m2s
Release / update-aur (push) Successful in 10s
2026-07-30 15:06:52 +02:00
bl 3807294333 Fix mic feedback in solo calls: force .muted on the local preview tile
Every video tile is built client-side via document.createElement, never
parsed from HTML - browsers only seed the live .muted property from the
muted attribute for parser-inserted elements, so the attribute alone
never actually silenced the local preview. Result: your own mic played
back through your own speakers even with no other peers in the call.
Now sets el.set_muted(true) directly whenever the preview's srcObject
is (re)assigned.
2026-07-30 15:06:14 +02:00
bl 9c89cfbe9c chore: Release cnats version 0.2.3
Release / build (x86_64, ubuntu-latest) (push) Successful in 7m5s
Release / docker (push) Successful in 3m54s
Release / build (aarch64, aarch64) (push) Successful in 13m37s
Release / update-aur (push) Successful in 11s
2026-07-29 10:50:09 +02:00
bl 9326f93576 Fix hydration mismatch in CallPanel that crashed room switching
The SSR branch rendered an empty call-panel div while the hydrate
branch expected a child button node. On any full load/refresh of the
lobby room, the mismatched hydration cursor hit tachys's
unreachable!() panic path, trapping the wasm instance and killing all
reactivity (routing, room switching, SSE) for the rest of the page
load, while the already-rendered SSR HTML stayed visually intact.

SSR now renders the same default join-button markup hydrate expects.
2026-07-29 10:50:02 +02:00
bl 9858460696 chore: Release cnats version 0.2.2
Release / build (x86_64, ubuntu-latest) (push) Successful in 7m10s
Release / docker (push) Successful in 3m59s
Release / build (aarch64, aarch64) (push) Successful in 15m24s
Release / update-aur (push) Successful in 12s
2026-07-28 12:03:23 +02:00
bl 9550fa1f72 CI: install cargo-leptos via cargo-binstall instead of building from source
Building cargo-leptos from source pulls in swc/lightningcss/rhai and was
OOM-killing the aarch64 (Raspberry Pi) runner. cargo-leptos publishes
prebuilt aarch64-unknown-linux-gnu binaries, so binstall sidesteps the
compile entirely.
2026-07-28 12:02:26 +02:00
bl e0061af9ce chore: Release cnats version 0.2.1
Release / update-aur (push) Has been cancelled
Release / build (aarch64, aarch64) (push) Has been cancelled
Release / build (x86_64, ubuntu-latest) (push) Successful in 20m47s
Release / docker (push) Successful in 4m6s
2026-07-28 10:53:51 +02:00
bl 64bc5222fc Fix release build: box CallPanel's in-call subtree to avoid recursion limit
Mesh calling's nested Show/For inside ChatShell's own Show inlined as a
type parameter, blowing rustc's query recursion limit during the wasm
release build (CI: "queries overflow the depth limit!"). Splitting the
in-call view into its own component and type-erasing both CallPanel
branches with .into_any() keeps the type shallow instead of raising the
crate-wide limit.
2026-07-28 10:53:32 +02:00
5 changed files with 85 additions and 31 deletions
+8 -1
View File
@@ -36,8 +36,15 @@ jobs:
~/.cargo/bin
key: ${{ runner.os }}-${{ matrix.arch }}-cargo-leptos-${{ hashFiles('Cargo.lock') }}
- name: Install cargo-binstall
run: |
command -v cargo-binstall || \
curl -L --proto '=https' --tlsv1.2 -sSf \
https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh \
| bash
- name: Install cargo-leptos
run: command -v cargo-leptos || cargo install cargo-leptos --locked
run: command -v cargo-leptos || cargo binstall cargo-leptos --locked --no-confirm
- name: Build
run: cargo leptos build --release
Generated
+1 -1
View File
@@ -345,7 +345,7 @@ dependencies = [
[[package]]
name = "cnats"
version = "0.2.0"
version = "0.2.4"
dependencies = [
"anyhow",
"async-nats",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "cnats"
version = "0.2.0"
version = "0.2.4"
edition = "2021"
[lib]
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
pkgname=cnats
pkgver=0.2.0
pkgver=0.2.4
pkgrel=1
pkgdesc="Web chat over NATS subjects with Kanidm SSO (Leptos SSR)"
arch=('x86_64' 'aarch64')
+74 -27
View File
@@ -450,9 +450,19 @@ fn CallPanel(room: Memo<String>, me: String) -> impl IntoView {
// Mirror the local MediaStream into the preview <video> element -
// `srcObject` has no HTML attribute form, has to be set via JS.
//
// Also force `.muted` via the JS property here, not just the
// `muted` attribute on the element below: every video tile in this
// app is built client-side via `document.createElement` (never
// parsed from HTML), and browsers only seed the live `.muted`
// property from the `muted` *attribute* for parser-inserted
// elements. Without this, the local preview plays back the user's
// own mic through their speakers - audible as feedback in a
// solo call.
Effect::new(move |_| {
let stream = call_state.get_value().local_stream().get();
if let Some(el) = local_video_ref.get() {
el.set_muted(true);
el.set_src_object(stream.as_ref());
}
});
@@ -472,45 +482,82 @@ fn CallPanel(room: Memo<String>, me: String) -> impl IntoView {
view! {
<div class="call-panel">
<Show
when=move || in_call.get()
fallback=move || {
{move || {
if in_call.get() {
view! {
<CallActive
local_video_ref=local_video_ref
call_state=call_state.get_value()
on_leave=on_leave
/>
}
.into_any()
} else {
view! {
<button class="call-join" on:click=on_join>
"☎ join call"
</button>
}
.into_any()
}
>
<div class="call-active">
<div class="video-grid">
<video
class="video-tile video-tile-local"
node_ref=local_video_ref
autoplay=true
muted=true
playsinline=true
></video>
<For
each=move || call_state.get_value().peer_streams()
key=|(id, _)| id.clone()
children=move |(_id, stream)| {
view! { <PeerVideoTile stream=stream/> }
}
/>
</div>
<button class="call-leave" on:click=on_leave>
"⏏ leave call"
</button>
</div>
</Show>
}}
</div>
}
.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.
#[cfg(not(feature = "hydrate"))]
{
let _ = (room, me);
view! { <div class="call-panel"></div> }
view! {
<div class="call-panel">
<button class="call-join" disabled=true>
"☎ join call"
</button>
</div>
}
.into_any()
}
}
/// The in-call subtree (video grid + leave button), split out of
/// `CallPanel` so its `<For>`-over-peers view doesn't get inlined as a type
/// parameter of `CallPanel`'s own `if`/`else` branch - that inlining is what
/// was blowing the compiler's query recursion limit once mesh calling's
/// nested `Show`/`For` landed inside `ChatShell`'s own `Show`.
#[cfg(feature = "hydrate")]
#[component]
fn CallActive(
local_video_ref: NodeRef<leptos::html::Video>,
call_state: crate::webrtc::CallState,
on_leave: impl Fn(leptos::ev::MouseEvent) + 'static,
) -> impl IntoView {
view! {
<div class="call-active">
<div class="video-grid">
<video
class="video-tile video-tile-local"
node_ref=local_video_ref
autoplay=true
muted=true
playsinline=true
></video>
<For
each=move || call_state.peer_streams()
key=|(id, _)| id.clone()
children=move |(_id, stream)| {
view! { <PeerVideoTile stream=stream/> }
}
/>
</div>
<button class="call-leave" on:click=on_leave>
"⏏ leave call"
</button>
</div>
}
}