feat: add Vite configuration with custom plugins and server settings
This commit is contained in:
156
src/app/features/common-settings/general/LivekitConfig.tsx
Normal file
156
src/app/features/common-settings/general/LivekitConfig.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { FormEventHandler, useCallback, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
color,
|
||||
Input,
|
||||
Spinner,
|
||||
Text,
|
||||
} from 'folds';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { useStateEvent } from '../../../hooks/useStateEvent';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
|
||||
interface LivekitConfigContent {
|
||||
service_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Room-specific LiveKit configuration component.
|
||||
* Allows room admins to override the LiveKit service URL for calls in this room.
|
||||
*/
|
||||
export function LivekitConfig() {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const creators = useRoomCreators(room);
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
|
||||
const stateEvent = useStateEvent(room, StateEvent.RoomLivekitConfig);
|
||||
const currentConfig = stateEvent?.getContent<LivekitConfigContent>();
|
||||
const currentUrl = currentConfig?.service_url || '';
|
||||
|
||||
const [urlValue, setUrlValue] = useState(currentUrl);
|
||||
const [urlError, setUrlError] = useState<string>();
|
||||
|
||||
const [saveState, saveConfig] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (serviceUrl: string) => {
|
||||
const content: LivekitConfigContent = serviceUrl
|
||||
? { service_url: serviceUrl }
|
||||
: {};
|
||||
|
||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomLivekitConfig as any, content, '');
|
||||
},
|
||||
[mx, room]
|
||||
)
|
||||
);
|
||||
|
||||
const canEdit = permissions.stateEvent(StateEvent.RoomLivekitConfig, mx.getUserId()!);
|
||||
|
||||
const handleSave: FormEventHandler<HTMLFormElement> = async (evt) => {
|
||||
evt.preventDefault();
|
||||
const serviceUrl = urlValue.trim();
|
||||
|
||||
// Basic URL validation
|
||||
if (serviceUrl && !serviceUrl.startsWith('http://') && !serviceUrl.startsWith('https://')) {
|
||||
setUrlError('URL must start with http:// or https://');
|
||||
return;
|
||||
}
|
||||
|
||||
setUrlError(undefined);
|
||||
await saveConfig(serviceUrl);
|
||||
};
|
||||
|
||||
const handleClear = async () => {
|
||||
setUrlValue('');
|
||||
setUrlError(undefined);
|
||||
await saveConfig('');
|
||||
};
|
||||
|
||||
const hasChanges = urlValue !== currentUrl;
|
||||
const isSaving = saveState.status === AsyncStatus.Loading;
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="LiveKit Service URL"
|
||||
description="Override the LiveKit JWT service endpoint for Matrix RTC calls in this room. Leave empty to use the server default or user preference."
|
||||
/>
|
||||
<Box as="form" onSubmit={handleSave} direction="Column" gap="200" style={{ padding: '0 16px' }}>
|
||||
<Input
|
||||
value={urlValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUrlValue(e.target.value);
|
||||
setUrlError(undefined);
|
||||
}}
|
||||
placeholder="https://example.com/livekit/jwt"
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
disabled={!canEdit || isSaving}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{urlError && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||
{urlError}
|
||||
</Text>
|
||||
)}
|
||||
{saveState.status === AsyncStatus.Error && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||
Failed to save: {saveState.error instanceof Error ? saveState.error.message : 'Unknown error'}
|
||||
</Text>
|
||||
)}
|
||||
{saveState.status === AsyncStatus.Success && !hasChanges && (
|
||||
<Text size="T200" style={{ color: color.Success.Main }}>
|
||||
Configuration saved
|
||||
</Text>
|
||||
)}
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Button
|
||||
type="submit"
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
disabled={!canEdit || isSaving || !hasChanges}
|
||||
before={isSaving && <Spinner size="100" variant="Primary" fill="Solid" />}
|
||||
>
|
||||
<Text size="B300">Save</Text>
|
||||
</Button>
|
||||
{currentUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
radii="300"
|
||||
disabled={!canEdit || isSaving}
|
||||
onClick={handleClear}
|
||||
>
|
||||
<Text size="B300">Clear</Text>
|
||||
</Button>
|
||||
)}
|
||||
{!canEdit && (
|
||||
<Text size="T200" priority="300">
|
||||
You don't have permission to change this setting
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</SequenceCard>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user