From 26c70f5a1d819a7797ce5aaedb9789c6e2152a8b Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 12 Sep 2026 19:46:06 -0400 Subject: [PATCH] fix(calls): sidebar voice-channel join respects the voice limit channelFull was computed only in the prescreen; a second click on the channel in the room nav joined a full channel. Extract useVoiceChannelFull, use it in both places, and refuse with a "Channel full (N/N)" toast. Fixes #30 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/call/CallView.tsx | 14 ++++------ src/app/features/room-nav/RoomNavItem.tsx | 21 ++++++++++++++ src/app/hooks/useVoiceChannelFull.ts | 34 +++++++++++++++++++++++ 3 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 src/app/hooks/useVoiceChannelFull.ts diff --git a/src/app/features/call/CallView.tsx b/src/app/features/call/CallView.tsx index 8d6000342..7611aa39d 100644 --- a/src/app/features/call/CallView.tsx +++ b/src/app/features/call/CallView.tsx @@ -18,8 +18,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient'; import { StateEvent } from '../../../types/matrix/room'; import { useCallMembers, useCallSession } from '../../hooks/useCall'; import { LotusDecorationPusher } from '../lotus/LotusDecorationPusher'; -import { useStateEvent } from '../../hooks/useStateEvent'; -import { VoiceLimitContent } from '../common-settings/general/RoomVoiceLimit'; +import { useVoiceChannelFull } from '../../hooks/useVoiceChannelFull'; import { CallMemberRenderer } from './CallMemberCard'; import * as css from './styles.css'; import { CallControls } from './CallControls'; @@ -114,12 +113,9 @@ function CallPrescreen() { const callEmbed = useCallEmbed(); const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId; - // Voice channel user limit (io.lotus.voice_limit). 0 / absent means no limit. - const limitEvent = useStateEvent(room, StateEvent.LotusVoiceLimit); - const maxUsers = limitEvent?.getContent().max_users ?? 0; - // A user already counted in the session is rejoining and should not be blocked. - const alreadyMember = callMembers.some((m) => m.sender === mx.getSafeUserId()); - const channelFull = maxUsers > 0 && !alreadyMember && callMembers.length >= maxUsers; + // [Gitea #30] Voice channel user limit (io.lotus.voice_limit), shared with the + // room-nav join path via useVoiceChannelFull so both agree on "full". + const { channelFull, current: callMembersCount, max: maxUsers } = useVoiceChannelFull(room); const canJoin = hasPermission && livekitSupported && rtcSupported && !channelFull; @@ -144,7 +140,7 @@ function CallPrescreen() { {!inOtherCall && !hasPermission && } {!inOtherCall && hasPermission && channelFull && ( - + )} {!inOtherCall && hasPermission && !channelFull && ( = (evt) => { const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels); @@ -714,6 +719,22 @@ function RoomNavItem_({ if (callEmbed) { return; } + + // [Gitea #30] Refuse to start a call into a full voice channel — the + // prescreen already blocks this, but the sidebar second-click join path + // skipped the check entirely. + if (channelFull) { + evt.preventDefault(); + setToast( + createErrorToast( + `Channel full (${voiceCurrent}/${voiceMax})`, + Icons.Warning, + 'Cannot join', + ), + ); + return; + } + // Start call in second click if (selected) { evt.preventDefault(); diff --git a/src/app/hooks/useVoiceChannelFull.ts b/src/app/hooks/useVoiceChannelFull.ts new file mode 100644 index 000000000..638ae1d67 --- /dev/null +++ b/src/app/hooks/useVoiceChannelFull.ts @@ -0,0 +1,34 @@ +import { Room } from 'matrix-js-sdk'; +import { useMemo } from 'react'; +import { useStateEvent } from './useStateEvent'; +import { StateEvent } from '../../types/matrix/room'; +import { VoiceLimitContent } from '../features/common-settings/general/RoomVoiceLimit'; +import { useCallMembers, useCallSession } from './useCall'; +import { useMatrixClient } from './useMatrixClient'; + +export type VoiceChannelFull = { + channelFull: boolean; + current: number; + max: number; +}; + +/** + * [Gitea #30] Voice channel user limit (`io.lotus.voice_limit`), shared between + * `CallPrescreen` and any other join path (e.g. the room-nav second-click join) + * so they agree on when a channel is full. 0/absent `max_users` means no limit. + */ +export const useVoiceChannelFull = (room: Room): VoiceChannelFull => { + const mx = useMatrixClient(); + const callSession = useCallSession(room); + const callMembers = useCallMembers(callSession); + + const limitEvent = useStateEvent(room, StateEvent.LotusVoiceLimit); + + return useMemo(() => { + const maxUsers = limitEvent?.getContent().max_users ?? 0; + // A user already counted in the session is rejoining and should not be blocked. + const alreadyMember = callMembers.some((m) => m.sender === mx.getSafeUserId()); + const channelFull = maxUsers > 0 && !alreadyMember && callMembers.length >= maxUsers; + return { channelFull, current: callMembers.length, max: maxUsers }; + }, [limitEvent, callMembers, mx]); +};