feat(call): add docked call panel and remote cursor overlay
- Implemented DockedCallPanel component to render the docked call interface when an active call is present. - Created RemoteCursorOverlay component to display remote cursor positions during screen sharing. - Added hooks for managing docked call state and remote cursor functionality. - Introduced pending drag state management for seamless transitions between docked and floating states. - Developed embed filter management components for personal and room-wide settings, allowing users to customize URL embed visibility. - Implemented auto-joining functionality for rooms within spaces, enhancing user experience during space navigation. - Added state management for tracking joining progress of rooms in spaces.
This commit is contained in:
315
src/app/features/common-settings/general/EmbedFilters.tsx
Normal file
315
src/app/features/common-settings/general/EmbedFilters.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import React, { FormEventHandler, useCallback, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
color,
|
||||
config,
|
||||
Icon,
|
||||
Icons,
|
||||
Input,
|
||||
Spinner,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
} 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 {
|
||||
useRoomEmbedFilters,
|
||||
useRoomWideEmbedFilters,
|
||||
useSetRoomWideEmbedFilters,
|
||||
EmbedFiltersContent,
|
||||
} from '../../../hooks/useRoomEmbedFilters';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Validates that a string is a valid regex pattern.
|
||||
* @param pattern - The pattern to validate
|
||||
* @returns True if the pattern is valid
|
||||
*/
|
||||
const isValidPattern = (pattern: string): boolean => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new RegExp(pattern);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the pattern list editor component.
|
||||
*/
|
||||
interface PatternListEditorProps {
|
||||
patterns: string[];
|
||||
onAdd: (pattern: string) => Promise<void>;
|
||||
onRemove: (pattern: string) => Promise<void>;
|
||||
disabled?: boolean;
|
||||
saving: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable component for adding/removing patterns.
|
||||
*/
|
||||
function PatternListEditor({
|
||||
patterns,
|
||||
onAdd,
|
||||
onRemove,
|
||||
disabled,
|
||||
saving,
|
||||
error,
|
||||
}: PatternListEditorProps) {
|
||||
const [newPattern, setNewPattern] = useState('');
|
||||
const [patternError, setPatternError] = useState<string>();
|
||||
|
||||
const handleAddPattern: FormEventHandler<HTMLFormElement> = async (evt) => {
|
||||
evt.preventDefault();
|
||||
const pattern = newPattern.trim();
|
||||
|
||||
if (!pattern) {
|
||||
setPatternError('Pattern cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidPattern(pattern)) {
|
||||
setPatternError('Invalid regex pattern');
|
||||
return;
|
||||
}
|
||||
|
||||
if (patterns.includes(pattern)) {
|
||||
setPatternError('Pattern already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
setPatternError(undefined);
|
||||
await onAdd(pattern);
|
||||
setNewPattern('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="300">
|
||||
<Box as="form" onSubmit={handleAddPattern} gap="200" alignItems="Start">
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Input
|
||||
value={newPattern}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewPattern(e.target.value);
|
||||
setPatternError(undefined);
|
||||
}}
|
||||
placeholder="e.g. ^https?://example\.com"
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
disabled={disabled || saving}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{patternError && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||
{patternError}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
type="submit"
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
disabled={disabled || saving || !newPattern.trim()}
|
||||
before={saving && <Spinner size="100" variant="Primary" fill="Solid" />}
|
||||
>
|
||||
<Text size="B300">Add</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{patterns.length > 0 && (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="T200" priority="300">
|
||||
Active patterns ({patterns.length}):
|
||||
</Text>
|
||||
<Box gap="200" wrap="Wrap">
|
||||
{patterns.map((pattern) => (
|
||||
<TooltipProvider
|
||||
key={pattern}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text size="T300">{pattern}</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<Chip
|
||||
ref={triggerRef}
|
||||
variant="Secondary"
|
||||
radii="Pill"
|
||||
onClick={() => onRemove(pattern)}
|
||||
after={!disabled && <Icon size="50" src={Icons.Cross} />}
|
||||
disabled={disabled || saving}
|
||||
>
|
||||
<Text
|
||||
size="T200"
|
||||
truncate
|
||||
style={{ maxWidth: config.space.S500, fontFamily: 'monospace' }}
|
||||
>
|
||||
{pattern}
|
||||
</Text>
|
||||
</Chip>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>
|
||||
Failed to save: {error}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for managing personal URL embed filter patterns for a room.
|
||||
* These patterns are stored per-user per-room and filter out URL previews
|
||||
* that match the patterns.
|
||||
*/
|
||||
export function PersonalEmbedFilters() {
|
||||
const room = useRoom();
|
||||
const [filters, setFilters] = useRoomEmbedFilters(room);
|
||||
|
||||
const [saveState, saveFilters] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (newFilters: EmbedFiltersContent) => {
|
||||
await setFilters(newFilters);
|
||||
},
|
||||
[setFilters]
|
||||
)
|
||||
);
|
||||
|
||||
const saving = saveState.status === AsyncStatus.Loading;
|
||||
|
||||
const handleAdd = async (pattern: string) => {
|
||||
const newFilters: EmbedFiltersContent = {
|
||||
disabledPatterns: [...filters.disabledPatterns, pattern],
|
||||
};
|
||||
await saveFilters(newFilters);
|
||||
};
|
||||
|
||||
const handleRemove = async (pattern: string) => {
|
||||
const newFilters: EmbedFiltersContent = {
|
||||
disabledPatterns: filters.disabledPatterns.filter((p) => p !== pattern),
|
||||
};
|
||||
await saveFilters(newFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Personal Embed Filters"
|
||||
description="Add regex patterns for URLs that should not show embeds for you only. Patterns are case-insensitive."
|
||||
/>
|
||||
<PatternListEditor
|
||||
patterns={filters.disabledPatterns}
|
||||
onAdd={handleAdd}
|
||||
onRemove={handleRemove}
|
||||
saving={saving}
|
||||
error={saveState.status === AsyncStatus.Error ? String(saveState.error) : undefined}
|
||||
/>
|
||||
</SequenceCard>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for managing room-wide URL embed filter patterns.
|
||||
* These patterns are set by room admins and apply to all users.
|
||||
*/
|
||||
export function RoomWideEmbedFilters() {
|
||||
const room = useRoom();
|
||||
const mx = useMatrixClient();
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const creators = useRoomCreators(room);
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
|
||||
const canEdit = permissions.stateEvent(StateEvent.RoomEmbedFilters, mx.getSafeUserId());
|
||||
const filters = useRoomWideEmbedFilters(room);
|
||||
const setRoomWideFilters = useSetRoomWideEmbedFilters(room);
|
||||
|
||||
const [saveState, saveFilters] = useAsyncCallback(
|
||||
useCallback(
|
||||
async (newFilters: EmbedFiltersContent) => {
|
||||
await setRoomWideFilters(newFilters);
|
||||
},
|
||||
[setRoomWideFilters]
|
||||
)
|
||||
);
|
||||
|
||||
const saving = saveState.status === AsyncStatus.Loading;
|
||||
|
||||
const handleAdd = async (pattern: string) => {
|
||||
const newFilters: EmbedFiltersContent = {
|
||||
disabledPatterns: [...filters.disabledPatterns, pattern],
|
||||
};
|
||||
await saveFilters(newFilters);
|
||||
};
|
||||
|
||||
const handleRemove = async (pattern: string) => {
|
||||
const newFilters: EmbedFiltersContent = {
|
||||
disabledPatterns: filters.disabledPatterns.filter((p) => p !== pattern),
|
||||
};
|
||||
await saveFilters(newFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Room-Wide Embed Filters"
|
||||
description={
|
||||
canEdit
|
||||
? 'Add regex patterns that apply to all users in this room. Patterns are case-insensitive.'
|
||||
: 'Embed filter patterns set by room admins. You need moderator permissions to edit.'
|
||||
}
|
||||
/>
|
||||
<PatternListEditor
|
||||
patterns={filters.disabledPatterns}
|
||||
onAdd={handleAdd}
|
||||
onRemove={handleRemove}
|
||||
disabled={!canEdit}
|
||||
saving={saving}
|
||||
error={saveState.status === AsyncStatus.Error ? String(saveState.error) : undefined}
|
||||
/>
|
||||
</SequenceCard>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined component showing both personal and room-wide embed filters.
|
||||
*/
|
||||
export function EmbedFilters() {
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<RoomWideEmbedFilters />
|
||||
<PersonalEmbedFilters />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './EmbedFilters';
|
||||
export * from './RoomAddress';
|
||||
export * from './RoomEncryption';
|
||||
export * from './RoomHistoryVisibility';
|
||||
|
||||
Reference in New Issue
Block a user