CI / Build & Quality Checks (push) Successful in 1m38s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 8s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 8m52s
e2e/a11y.spec.ts runs @axe-core/playwright (WCAG 2.x A/AA) over the login
page, room timeline + composer, message options menu, thread panel, user
settings and room settings, failing on critical/serious findings other
than colour contrast (reported, not gated: generated avatar colours and
portal false positives). Aria snapshots of the composer, message menu,
thread panel and settings nav catch lost names/roles/live regions.
Burned down what the first run found:
- NavItem: callers' aria-selected is not valid on a div (axe critical);
it now drives data-selected for styling and aria-current="page".
- Composer placeholder at 0.5 opacity was ~2.3:1; now P300.
- Voice-limit and explore custom-limit number inputs had no label.
- Thread panel is an <aside aria-label="Thread">; the settings modal is a
role=dialog; the settings sections are a <nav>; the message action
menu carries data-message-menu + a label.
Also allows WebKit's CI wording for the well-known probe ("Could not
connect … Connection refused") that failed run #2003.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
import React, { FormEventHandler, useCallback } from 'react';
|
|
import { Box, Button, color, Input, Spinner, Text } from 'folds';
|
|
import { MatrixError } from 'matrix-js-sdk';
|
|
import { SequenceCard } from '../../../components/sequence-card';
|
|
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
|
import { SettingTile } from '../../../components/setting-tile';
|
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|
import { useRoom } from '../../../hooks/useRoom';
|
|
import { StateEvent } from '../../../../types/matrix/room';
|
|
import { sendStateEvent } from '../../../utils/room';
|
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
|
import { useStateEvent } from '../../../hooks/useStateEvent';
|
|
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
|
|
|
|
export type VoiceLimitContent = {
|
|
max_users?: number;
|
|
};
|
|
|
|
type RoomVoiceLimitProps = {
|
|
permissions: RoomPermissionsAPI;
|
|
};
|
|
export function RoomVoiceLimit({ permissions }: RoomVoiceLimitProps) {
|
|
const mx = useMatrixClient();
|
|
const room = useRoom();
|
|
|
|
const canEdit = permissions.stateEvent(StateEvent.LotusVoiceLimit, mx.getSafeUserId());
|
|
|
|
const limitEvent = useStateEvent(room, StateEvent.LotusVoiceLimit);
|
|
const maxUsers = limitEvent?.getContent<VoiceLimitContent>().max_users ?? 0;
|
|
|
|
const [submitState, submit] = useAsyncCallback(
|
|
useCallback(
|
|
async (value: number) => {
|
|
const content: VoiceLimitContent = value > 0 ? { max_users: value } : {};
|
|
await sendStateEvent(mx, room.roomId, StateEvent.LotusVoiceLimit, content);
|
|
},
|
|
[mx, room.roomId],
|
|
),
|
|
);
|
|
const submitting = submitState.status === AsyncStatus.Loading;
|
|
|
|
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
|
evt.preventDefault();
|
|
const target = evt.target as HTMLFormElement;
|
|
const limitInput = target.elements.namedItem('limitInput') as HTMLInputElement | null;
|
|
if (!limitInput) return;
|
|
const parsed = parseInt(limitInput.value, 10);
|
|
const value = Number.isNaN(parsed) || parsed < 0 ? 0 : parsed;
|
|
submit(value);
|
|
};
|
|
|
|
return (
|
|
<SequenceCard
|
|
className={SequenceCardStyle}
|
|
variant="SurfaceVariant"
|
|
direction="Column"
|
|
gap="400"
|
|
>
|
|
<SettingTile
|
|
title="Voice Channel Limit"
|
|
description="Set the maximum number of participants allowed in this room's voice call. Set to 0 for no limit. Enforced on the server for all Matrix clients."
|
|
>
|
|
<Box as="form" onSubmit={handleSubmit} gap="200" alignItems="Center">
|
|
<Box style={{ maxWidth: '100px' }} grow="Yes">
|
|
<Input
|
|
key={maxUsers}
|
|
name="limitInput"
|
|
aria-label="Voice channel participant limit"
|
|
defaultValue={maxUsers}
|
|
type="number"
|
|
min={0}
|
|
max={99}
|
|
size="300"
|
|
variant="Secondary"
|
|
radii="300"
|
|
readOnly={!canEdit}
|
|
disabled={!canEdit}
|
|
/>
|
|
</Box>
|
|
<Button
|
|
type="submit"
|
|
size="300"
|
|
variant="Primary"
|
|
fill="Solid"
|
|
radii="300"
|
|
disabled={!canEdit || submitting}
|
|
before={submitting ? <Spinner size="100" variant="Primary" fill="Solid" /> : undefined}
|
|
>
|
|
<Text size="B300">Save</Text>
|
|
</Button>
|
|
</Box>
|
|
{submitState.status === AsyncStatus.Error && (
|
|
<Text style={{ color: color.Critical.Main }} size="T200">
|
|
{(submitState.error as MatrixError).message}
|
|
</Text>
|
|
)}
|
|
</SettingTile>
|
|
</SequenceCard>
|
|
);
|
|
}
|