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:
@@ -23,7 +23,10 @@ import {
|
||||
Header,
|
||||
config,
|
||||
Spinner,
|
||||
color,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
@@ -43,6 +46,8 @@ import { ModalWide } from '../../../styles/Modal.css';
|
||||
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
||||
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
||||
import { useCapabilities } from '../../../hooks/useCapabilities';
|
||||
import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
|
||||
import { useUserColor } from '../../../hooks/useUserColor';
|
||||
|
||||
type ProfileProps = {
|
||||
profile: UserProfile;
|
||||
@@ -303,6 +308,126 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Color picker component for user's profile color
|
||||
* Stored in avatar image metadata, visible to other Paarrot users
|
||||
*/
|
||||
function ProfileColor() {
|
||||
const [userColor, setUserColor, loading] = useUserColor();
|
||||
const [localColor, setLocalColor] = useState(userColor ?? '#3b82f6');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (userColor) {
|
||||
setLocalColor(userColor);
|
||||
}
|
||||
}, [userColor]);
|
||||
|
||||
const handleColorChange = (newColor: string) => {
|
||||
setLocalColor(newColor);
|
||||
setError(undefined);
|
||||
};
|
||||
|
||||
const handleSaveColor = async () => {
|
||||
setSaving(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
await setUserColor(localColor);
|
||||
} catch (e) {
|
||||
setError('Failed to save. Make sure you have an avatar set.');
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleRemoveColor = async () => {
|
||||
setSaving(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
await setUserColor(undefined);
|
||||
setLocalColor('#3b82f6');
|
||||
} catch (e) {
|
||||
setError('Failed to remove color.');
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingTile
|
||||
title={
|
||||
<Text as="span" size="L400">
|
||||
Profile Color
|
||||
</Text>
|
||||
}
|
||||
description="Embedded in your avatar. Other Paarrot users will see this color."
|
||||
after={
|
||||
<Box alignItems="Center" gap="200">
|
||||
{loading ? (
|
||||
<Text size="T300" style={{ opacity: 0.7 }}>Loading...</Text>
|
||||
) : (
|
||||
<HexColorPickerPopOut
|
||||
picker={
|
||||
<Box direction="Column" gap="200">
|
||||
<HexColorPicker color={localColor} onChange={handleColorChange} />
|
||||
<Box gap="100" alignItems="Center">
|
||||
<Input
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
style={{ width: toRem(100) }}
|
||||
value={localColor}
|
||||
onChange={(e) => {
|
||||
setLocalColor(e.target.value);
|
||||
setError(undefined);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={handleSaveColor}
|
||||
disabled={saving}
|
||||
>
|
||||
<Text size="B300">{saving ? 'Saving...' : 'Save'}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
{error && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>{error}</Text>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
onRemove={userColor ? handleRemoveColor : undefined}
|
||||
>
|
||||
{(onOpen) => (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={onOpen}
|
||||
disabled={saving}
|
||||
before={
|
||||
<Box
|
||||
style={{
|
||||
width: toRem(16),
|
||||
height: toRem(16),
|
||||
borderRadius: toRem(4),
|
||||
backgroundColor: userColor ?? localColor,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Text size="T300">{userColor ? 'Change' : 'Choose'}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</HexColorPickerPopOut>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Profile() {
|
||||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId()!;
|
||||
@@ -318,6 +443,7 @@ export function Profile() {
|
||||
gap="400"
|
||||
>
|
||||
<ProfileAvatar userId={userId} profile={profile} />
|
||||
<ProfileColor />
|
||||
<ProfileDisplayName userId={userId} profile={profile} />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
|
||||
@@ -23,6 +23,8 @@ import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
|
||||
/** Represents an audio device (microphone or speaker) */
|
||||
interface AudioDevice {
|
||||
@@ -447,6 +449,7 @@ export function Audio({ requestClose }: AudioProps) {
|
||||
const [speakers, setSpeakers] = useState<AudioDevice[]>([]);
|
||||
const [settings, setSettings] = useState<AudioSettings>(loadAudioSettings);
|
||||
const [permissionStatus, setPermissionStatus] = useState<'granted' | 'denied' | 'prompt'>('prompt');
|
||||
const [showRemoteCursor, setShowRemoteCursor] = useSetting(settingsAtom, 'showRemoteCursor');
|
||||
|
||||
useEffect(() => {
|
||||
const loadDevices = async () => {
|
||||
@@ -797,6 +800,17 @@ export function Audio({ requestClose }: AudioProps) {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Show Remote Cursor"
|
||||
description="Display other participants' cursors on shared screens. Both users must have this enabled."
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={showRemoteCursor}
|
||||
onChange={setShowRemoteCursor}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard
|
||||
|
||||
@@ -52,8 +52,6 @@ import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
|
||||
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
|
||||
import { useDateFormatItems } from '../../../hooks/useDateFormat';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
|
||||
import { useUserColor } from '../../../hooks/useUserColor';
|
||||
|
||||
type ThemeSelectorProps = {
|
||||
themeNames: Record<string, string>;
|
||||
@@ -431,131 +429,10 @@ function Appearance() {
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
|
||||
</SequenceCard>
|
||||
|
||||
<UserColorPicker />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Color picker component for user's profile color
|
||||
* Stored in avatar PNG metadata, visible to other Paarrot users
|
||||
*/
|
||||
function UserColorPicker() {
|
||||
const [userColor, setUserColor, loading] = useUserColor();
|
||||
const [localColor, setLocalColor] = useState(userColor ?? '#3b82f6');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
// Update local color when avatar color changes
|
||||
useEffect(() => {
|
||||
if (userColor) {
|
||||
setLocalColor(userColor);
|
||||
}
|
||||
}, [userColor]);
|
||||
|
||||
const handleColorChange = (newColor: string) => {
|
||||
setLocalColor(newColor);
|
||||
setError(undefined);
|
||||
};
|
||||
|
||||
const handleSaveColor = async () => {
|
||||
setSaving(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
await setUserColor(localColor);
|
||||
} catch (e) {
|
||||
setError('Failed to save. Make sure you have an avatar set.');
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleRemoveColor = async () => {
|
||||
setSaving(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
await setUserColor(undefined);
|
||||
setLocalColor('#3b82f6');
|
||||
} catch (e) {
|
||||
setError('Failed to remove color.');
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Profile Color"
|
||||
description="Embedded in your avatar. Other Paarrot users will see this color."
|
||||
after={
|
||||
<Box alignItems="Center" gap="200">
|
||||
{loading ? (
|
||||
<Text size="T300" style={{ opacity: 0.7 }}>Loading...</Text>
|
||||
) : (
|
||||
<HexColorPickerPopOut
|
||||
picker={
|
||||
<Box direction="Column" gap="200">
|
||||
<HexColorPicker color={localColor} onChange={handleColorChange} />
|
||||
<Box gap="100" alignItems="Center">
|
||||
<Input
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
style={{ width: toRem(100) }}
|
||||
value={localColor}
|
||||
onChange={(e) => {
|
||||
setLocalColor(e.target.value);
|
||||
setError(undefined);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={handleSaveColor}
|
||||
disabled={saving}
|
||||
>
|
||||
<Text size="B300">{saving ? 'Saving...' : 'Save'}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
{error && (
|
||||
<Text size="T200" style={{ color: color.Critical.Main }}>{error}</Text>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
onRemove={userColor ? handleRemoveColor : undefined}
|
||||
>
|
||||
{(onOpen) => (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={onOpen}
|
||||
disabled={saving}
|
||||
before={
|
||||
<Box
|
||||
style={{
|
||||
width: toRem(16),
|
||||
height: toRem(16),
|
||||
borderRadius: toRem(4),
|
||||
backgroundColor: userColor ?? localColor,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Text size="T300">{userColor ? 'Change' : 'Choose'}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</HexColorPickerPopOut>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
);
|
||||
}
|
||||
|
||||
type DateHintProps = {
|
||||
hasChanges: boolean;
|
||||
handleReset: () => void;
|
||||
@@ -943,6 +820,32 @@ function Editor() {
|
||||
);
|
||||
}
|
||||
|
||||
function Spaces() {
|
||||
const [autoJoinSpaceRooms, setAutoJoinSpaceRooms] = useSetting(
|
||||
settingsAtom,
|
||||
'autoJoinSpaceRooms'
|
||||
);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Spaces</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Auto-Join Space Rooms"
|
||||
description="Automatically join all accessible rooms when joining a space, with notifications set to Mentions & Keywords."
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={autoJoinSpaceRooms}
|
||||
onChange={setAutoJoinSpaceRooms}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectMessageLayout() {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout');
|
||||
@@ -1207,6 +1110,7 @@ export function General({ requestClose }: GeneralProps) {
|
||||
<Appearance />
|
||||
<DateAndTime />
|
||||
<Editor />
|
||||
<Spaces />
|
||||
<Messages />
|
||||
</Box>
|
||||
</PageContent>
|
||||
|
||||
Reference in New Issue
Block a user