feat: voice channel user limit (P5-10) + call join/leave sounds (P5-16)
CI / Build & Quality Checks (push) Successful in 10m54s
Trigger Desktop Build / trigger (push) Successful in 6s

P5-10 Voice Channel User Limit:
- New StateEvent.LotusVoiceLimit (io.lotus.voice_limit) with { max_users }
- RoomVoiceLimit admin control in Room Settings > General > Voice
  (power-level gated via permissions.stateEvent)
- CallPrescreen reads the limit reactively and disables Join with a
  'Channel Full (N/N)' message at capacity; existing members can rejoin

P5-16 Custom Join/Leave Sound Effects:
- useCallJoinLeaveSounds hook wired into CallUtils; detects participant
  join/leave via MatrixRTCSession membership changes (sender|deviceId),
  filters out self, only fires while joined
- Sounds synthesized in-browser with Web Audio (callSounds.ts) - no
  assets bundled; styles Off/Chime/Soft/Retro
- 'Join & Leave Sounds' selector in Settings > Calls (previews on change)

Docs: LOTUS_FEATURES.md, README.md, LOTUS_TODO.md (P5-10/P5-16 marked done)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 22:20:22 -04:00
parent 2b1c3256b6
commit 702e2e00eb
13 changed files with 371 additions and 15 deletions
@@ -65,6 +65,7 @@ import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
import { useDateFormatItems } from '../../../hooks/useDateFormat';
import { SequenceCardStyle } from '../styles.css';
import { useTauriUpdater } from '../../../hooks/useTauriUpdater';
import { playCallJoinSound } from '../../../utils/callSounds';
type ThemeSelectorProps = {
themeNames: Record<string, string>;
@@ -1113,6 +1114,15 @@ function Calls() {
const [deafenKey, setDeafenKey] = useSetting(settingsAtom, 'deafenKey');
const [afkAutoMute, setAfkAutoMute] = useSetting(settingsAtom, 'afkAutoMute');
const [afkTimeoutMinutes, setAfkTimeoutMinutes] = useSetting(settingsAtom, 'afkTimeoutMinutes');
const [callJoinLeaveSound, setCallJoinLeaveSound] = useSetting(
settingsAtom,
'callJoinLeaveSound',
);
const handleJoinLeaveSoundChange = (value: 'off' | 'chime' | 'soft' | 'retro') => {
setCallJoinLeaveSound(value);
if (value !== 'off') playCallJoinSound(value);
};
const pttBind = useKeyBind(setPttKey);
const deafenBind = useKeyBind(setDeafenKey);
@@ -1227,6 +1237,34 @@ function Calls() {
/>
)}
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Join & Leave Sounds"
description="Play a sound when someone joins or leaves a call you are in."
after={
<select
value={callJoinLeaveSound}
onChange={(e) =>
handleJoinLeaveSoundChange(e.target.value as 'off' | 'chime' | 'soft' | 'retro')
}
style={{
background: 'var(--bg-surface)',
color: 'inherit',
border: '1px solid var(--border-interactive-normal)',
borderRadius: '6px',
padding: '4px 8px',
fontSize: 'inherit',
cursor: 'pointer',
}}
>
<option value="off">Off</option>
<option value="chime">Chime</option>
<option value="soft">Soft</option>
<option value="retro">Retro</option>
</select>
}
/>
</SequenceCard>
</Box>
);
}