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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -18,8 +18,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|||||||
import { StateEvent } from '../../../types/matrix/room';
|
import { StateEvent } from '../../../types/matrix/room';
|
||||||
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||||
import { LotusDecorationPusher } from '../lotus/LotusDecorationPusher';
|
import { LotusDecorationPusher } from '../lotus/LotusDecorationPusher';
|
||||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
import { useVoiceChannelFull } from '../../hooks/useVoiceChannelFull';
|
||||||
import { VoiceLimitContent } from '../common-settings/general/RoomVoiceLimit';
|
|
||||||
import { CallMemberRenderer } from './CallMemberCard';
|
import { CallMemberRenderer } from './CallMemberCard';
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
import { CallControls } from './CallControls';
|
import { CallControls } from './CallControls';
|
||||||
@@ -114,12 +113,9 @@ function CallPrescreen() {
|
|||||||
const callEmbed = useCallEmbed();
|
const callEmbed = useCallEmbed();
|
||||||
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
|
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
|
||||||
|
|
||||||
// Voice channel user limit (io.lotus.voice_limit). 0 / absent means no limit.
|
// [Gitea #30] Voice channel user limit (io.lotus.voice_limit), shared with the
|
||||||
const limitEvent = useStateEvent(room, StateEvent.LotusVoiceLimit);
|
// room-nav join path via useVoiceChannelFull so both agree on "full".
|
||||||
const maxUsers = limitEvent?.getContent<VoiceLimitContent>().max_users ?? 0;
|
const { channelFull, current: callMembersCount, max: maxUsers } = useVoiceChannelFull(room);
|
||||||
// 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;
|
|
||||||
|
|
||||||
const canJoin = hasPermission && livekitSupported && rtcSupported && !channelFull;
|
const canJoin = hasPermission && livekitSupported && rtcSupported && !channelFull;
|
||||||
|
|
||||||
@@ -144,7 +140,7 @@ function CallPrescreen() {
|
|||||||
<Box className={css.PrescreenMessage} alignItems="Center">
|
<Box className={css.PrescreenMessage} alignItems="Center">
|
||||||
{!inOtherCall && !hasPermission && <NoPermissionMessage />}
|
{!inOtherCall && !hasPermission && <NoPermissionMessage />}
|
||||||
{!inOtherCall && hasPermission && channelFull && (
|
{!inOtherCall && hasPermission && channelFull && (
|
||||||
<ChannelFullMessage current={callMembers.length} max={maxUsers} />
|
<ChannelFullMessage current={callMembersCount} max={maxUsers} />
|
||||||
)}
|
)}
|
||||||
{!inOtherCall && hasPermission && !channelFull && (
|
{!inOtherCall && hasPermission && !channelFull && (
|
||||||
<JoinMessage
|
<JoinMessage
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import {
|
|||||||
} from '../../hooks/useRoomMeta';
|
} from '../../hooks/useRoomMeta';
|
||||||
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||||
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
|
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
|
||||||
|
import { useVoiceChannelFull } from '../../hooks/useVoiceChannelFull';
|
||||||
import { callChatAtom } from '../../state/callEmbed';
|
import { callChatAtom } from '../../state/callEmbed';
|
||||||
import { createErrorToast, toastQueueAtom } from '../../state/toast';
|
import { createErrorToast, toastQueueAtom } from '../../state/toast';
|
||||||
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
|
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
|
||||||
@@ -691,8 +692,12 @@ function RoomNavItem_({
|
|||||||
const callMembers = useCallMembers(callSession);
|
const callMembers = useCallMembers(callSession);
|
||||||
const startCall = useCallStart(direct);
|
const startCall = useCallStart(direct);
|
||||||
const callEmbed = useCallEmbed();
|
const callEmbed = useCallEmbed();
|
||||||
|
// [Gitea #30] Same voice-limit check the call prescreen uses, so the sidebar
|
||||||
|
// second-click join path can't bypass a full channel.
|
||||||
|
const { channelFull, current: voiceCurrent, max: voiceMax } = useVoiceChannelFull(room);
|
||||||
const callPref = useAtomValue(useCallPreferencesAtom());
|
const callPref = useAtomValue(useCallPreferencesAtom());
|
||||||
const autoDiscoveryInfo = useAutoDiscoveryInfo();
|
const autoDiscoveryInfo = useAutoDiscoveryInfo();
|
||||||
|
const setToast = useSetAtom(toastQueueAtom);
|
||||||
|
|
||||||
const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
|
const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
|
||||||
const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
|
const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
|
||||||
@@ -714,6 +719,22 @@ function RoomNavItem_({
|
|||||||
if (callEmbed) {
|
if (callEmbed) {
|
||||||
return;
|
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
|
// Start call in second click
|
||||||
if (selected) {
|
if (selected) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|||||||
@@ -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<VoiceLimitContent>().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]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user