fix: make call join/leave sounds audible to all participants + server-side hard voice limit docs
Sounds (P5-16): browsers block the Web Audio context until a user gesture starts it, so join/leave sounds — which fire later with no gesture — were silent. unlockCallSounds() now primes/resumes the shared AudioContext inside the Join click (centralized in useCallStart so every join path is covered), making the per-client sounds reliably audible to everyone in the call. Voice limit (P5-10): the limit is now a hard, cross-client server-side cap enforced by the voice-limit-guard sidecar (matrix repo) that fronts lk-jwt-service and refuses LiveKit tokens when a room is full. Updated LOTUS_FEATURES.md / README.md / LOTUS_TODO.md to reflect that the client 'Channel Full' check is UX only and the server is authoritative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+12
-6
@@ -253,19 +253,25 @@ Hook: `src/app/hooks/useAfkAutoMute.ts`
|
||||
|
||||
### Voice Channel User Limit (P5-10)
|
||||
|
||||
Room admins can cap the number of participants allowed in a room's voice call.
|
||||
Room admins can cap the number of participants allowed in a room's voice call. The cap is a **hard, server-side limit enforced for every Matrix client** (Element, FluffyChat, …), backed by a client-side UX layer in Lotus Chat.
|
||||
|
||||
**Implementation:**
|
||||
**Client (this repo):**
|
||||
|
||||
- Limit is stored in the `io.lotus.voice_limit` room state event with content `{ max_users: N }` (0 / absent = no limit)
|
||||
- `RoomVoiceLimit` component in Room Settings → General → **Voice** lets admins set the cap with a number input. Editing is gated by `permissions.stateEvent(StateEvent.LotusVoiceLimit, …)`, so only users with `state_default` power (or above) can change it
|
||||
- `CallPrescreen` (`CallView.tsx`) reads the limit reactively via `useStateEvent` and compares it against the live `useCallMembers` count
|
||||
- When the call is at capacity, the **Join** button is disabled and a "Channel Full (N/N)" message is shown
|
||||
- A user who is already a member of the session (rejoining) is never blocked — only new joiners are gated
|
||||
- Enforcement is local to Lotus Chat clients (no server-side gatekeeping)
|
||||
- `CallPrescreen` (`CallView.tsx`) reads the limit reactively via `useStateEvent` and compares it against the live `useCallMembers` count; at capacity the **Join** button is disabled and a "Channel Full (N/N)" message is shown
|
||||
- A user already in the session (rejoining) is never blocked — only new joiners are gated
|
||||
|
||||
Files: `src/app/features/common-settings/general/RoomVoiceLimit.tsx`, `src/app/features/call/CallView.tsx`, `StateEvent.LotusVoiceLimit` in `src/types/matrix/room.ts`
|
||||
|
||||
**Server (the hard backstop — `matrix` repo `livekit/voice-limit-guard.py`):**
|
||||
|
||||
- Every client must fetch a LiveKit JWT from `lk-jwt-service` before joining a call. A fail-open guard sidecar sits in front of it (guard on `:8070`, lk-jwt-service moved to `:8071`)
|
||||
- On each token request the guard reads the room's `io.lotus.voice_limit` (Synapse admin API), and if the room is at capacity it returns `403` so the client cannot obtain a token and therefore cannot join — regardless of which client they use
|
||||
- Distinct Matrix users are counted via LiveKit `ListParticipants`; rejoins / extra devices are allowed. Any failure fails open so calls never break
|
||||
|
||||
> The client-side "Channel Full" check is UX/early-feedback; the server guard is the actual enforcement.
|
||||
|
||||
### Custom Join / Leave Sound Effects (P5-16)
|
||||
|
||||
A local sound plays when another participant joins or leaves a call you're in.
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@ Themes:
|
||||
**What:** Admins set max participants via custom state event `io.lotus.voice_limit: { max_users: N }`. Show "Channel Full (5/5)" to users over the limit. Local enforcement only.
|
||||
**[AUDIT REQUIRED]** Check if Element Call has its own participant limit that should be integrated with rather than duplicated.
|
||||
**Complexity:** Medium.
|
||||
**Done:** `RoomVoiceLimit` admin control in Room Settings → General → Voice; `CallPrescreen` disables Join + shows "Channel Full (N/N)" when at capacity (rejoiners exempt). State event `StateEvent.LotusVoiceLimit`.
|
||||
**Done:** `RoomVoiceLimit` admin control in Room Settings → General → Voice; `CallPrescreen` disables Join + shows "Channel Full (N/N)" when at capacity (rejoiners exempt). State event `StateEvent.LotusVoiceLimit`. **Hard enforcement is server-side for ALL clients** via `voice-limit-guard` (matrix repo `livekit/voice-limit-guard.py`) — a fail-open sidecar fronting `lk-jwt-service` (guard `:8070`, lk-jwt `:8071`) that refuses the LiveKit JWT (403) when the room is at capacity. The client check is UX-only. EC has only a global `max_participants` (50), so per-room limits were not duplicating an EC feature.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the origina
|
||||
- Dark/light mode inside calls matches your Lotus Chat theme
|
||||
- Calls are available in DMs and private groups only — no accidental mass rings
|
||||
- AFK auto-mute: mic is automatically silenced after a configurable idle timeout (1–30 min); a toast confirms the action
|
||||
- Voice channel user limit: admins can cap how many people can be in a room's call; others see "Channel Full" until a spot opens
|
||||
- Voice channel user limit: admins can cap how many people can be in a room's call — enforced server-side for every Matrix client (not just Lotus Chat); others see "Channel Full" until a spot opens
|
||||
- Custom join/leave sound effects when someone enters or leaves your call — choose Chime, Soft, Retro, or off
|
||||
|
||||
### Customization & Appearance
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useCallMembersChange, useCallSession } from './useCall';
|
||||
import { CallPreferences } from '../state/callPreferences';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { unlockCallSounds } from '../utils/callSounds';
|
||||
|
||||
const CallEmbedContext = createContext<CallEmbed | undefined>(undefined);
|
||||
|
||||
@@ -84,6 +85,10 @@ export const useCallStart = (dm = false) => {
|
||||
if (!container) {
|
||||
throw new Error('Failed to start call, No embed container element found!');
|
||||
}
|
||||
// startCall is always invoked from a click/tap handler — unlock the Web
|
||||
// Audio context now (within the gesture) so join/leave sounds that fire
|
||||
// later, without any gesture, are audible to everyone in the call.
|
||||
unlockCallSounds();
|
||||
const callEmbed = createCallEmbed(
|
||||
mx,
|
||||
room,
|
||||
|
||||
@@ -12,6 +12,17 @@ const getAudioContext = (): AudioContext | undefined => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create and resume the shared AudioContext from within a user gesture
|
||||
* (e.g. clicking "Join"). Browsers block AudioContext playback until it has
|
||||
* been started by a gesture, so join/leave sounds — which fire later without
|
||||
* any gesture — would otherwise be silent. Call this on call entry so every
|
||||
* participant's later membership-change sounds are actually audible.
|
||||
*/
|
||||
export const unlockCallSounds = (): void => {
|
||||
getAudioContext();
|
||||
};
|
||||
|
||||
type Note = {
|
||||
freq: number;
|
||||
/** Offset from now, in seconds */
|
||||
|
||||
Reference in New Issue
Block a user