import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Box, Switch, Text } from 'folds'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { SettingsSelect } from '../../../components/settings-select/SettingsSelect'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useRoom } from '../../../hooks/useRoom'; import { StateEvent } from '../../../../types/matrix/room'; import { useStateEvent } from '../../../hooks/useStateEvent'; import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AUDIO_BITRATE_OPTIONS, RoomQualityContent, SCREENSHARE_BITRATE_OPTIONS, SCREENSHARE_FRAMERATE_OPTIONS, } from '../../../utils/callQuality'; // Only the numeric cap keys are edited via `update`; the boolean policy keys // are handled by `setAllow`. type CapKey = 'audio_max_kbps' | 'screenshare_max_kbps' | 'screenshare_max_fps'; // String <-> numeric bridge for SettingsSelect (which needs string values). const toValue = (n?: number): string => (typeof n === 'number' ? String(n) : 'auto'); const CAP_KEYS: (keyof RoomQualityContent)[] = [ 'audio_max_kbps', 'screenshare_max_kbps', 'screenshare_max_fps', 'allow_screenshare', 'allow_camera', ]; const capsEqual = (a: RoomQualityContent, b: RoomQualityContent): boolean => CAP_KEYS.every((k) => a[k] === b[k]); type RoomQualityProps = { permissions: RoomPermissionsAPI; }; /** * [P5-31] Room-admin quality ceiling. Writes `io.lotus.room_quality`; every * Lotus client clamps its per-user quality to these caps. Hard enforcement for * ALL Matrix clients is a server-side follow-up (see LOTUS_TODO.md P5-31). */ export function RoomQuality({ permissions }: RoomQualityProps) { const mx = useMatrixClient(); const room = useRoom(); const canEdit = permissions.stateEvent(StateEvent.LotusRoomQuality, mx.getSafeUserId()); const event = useStateEvent(room, StateEvent.LotusRoomQuality); const caps = useMemo(() => event?.getContent() ?? {}, [event]); const [submitState, submit] = useAsyncCallback( useCallback( async (next: RoomQualityContent) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any await mx.sendStateEvent(room.roomId, StateEvent.LotusRoomQuality as any, next); }, [mx, room.roomId], ), ); const submitting = submitState.status === AsyncStatus.Loading; // Optimistic mirror: `useStateEvent` only refreshes when the write echoes // back via /sync (not when sendStateEvent resolves), so consecutive edits // must build on the pending write — otherwise a second edit spreads a stale // `caps` and silently drops the first. `effective` is what the UI shows and // what each edit merges into; it's reconciled below once the echo lands. const [pending, setPending] = useState(null); const effective = pending ?? caps; useEffect(() => { if (!pending) return; // Revert the optimistic view if the write failed… if (submitState.status === AsyncStatus.Error) { setPending(null); return; } // …or drop it once the synced state actually reflects it. if (capsEqual(caps, pending)) setPending(null); }, [caps, pending, submitState.status]); const commit = (next: RoomQualityContent) => { setPending(next); submit(next); }; const update = (key: CapKey, value: string) => { const next: RoomQualityContent = { ...effective }; if (value === 'auto') delete next[key]; else next[key] = parseInt(value, 10); commit(next); }; const setAllow = (key: 'allow_screenshare' | 'allow_camera', allowed: boolean) => { const next: RoomQualityContent = { ...effective }; // Absent = allowed, so only persist the key when forbidding. if (allowed) delete next[key]; else next[key] = false; commit(next); }; // Absent/true = allowed. const screenshareAllowed = effective.allow_screenshare !== false; const cameraAllowed = effective.allow_camera !== false; return ( Control what participants may share in this room. These are enforced on the server for every Matrix client (Element, FluffyChat, Lotus Chat, …). } /> setAllow('allow_screenshare', v)} disabled={!canEdit || submitting} /> } /> setAllow('allow_camera', v)} disabled={!canEdit || submitting} /> } /> Set a maximum microphone bitrate, screenshare bitrate, and screenshare framerate for this room. Lotus Chat clamps each participant to these ceilings (best-effort — applies to Lotus Chat clients). Auto = no cap. } /> update('audio_max_kbps', v)} options={AUDIO_BITRATE_OPTIONS} disabled={!canEdit || submitting} /> } /> update('screenshare_max_kbps', v)} options={SCREENSHARE_BITRATE_OPTIONS} disabled={!canEdit || submitting} /> } /> update('screenshare_max_fps', v)} options={SCREENSHARE_FRAMERATE_OPTIONS} disabled={!canEdit || submitting} /> } /> ); }