import { useState, useEffect } from 'react'; import { Modal } from '../ui/Modal'; import { useUIStore } from '../../stores/uiStore'; import { useSpaceStore } from '../../stores/spaceStore'; import { useSettingsStore } from '../../stores/settingsStore'; import { Avatar } from '../ui/Avatar'; import { api } from '../../api/client'; import { hasPermissionBit, PermissionBits } from '../../utils/permissions'; import { OverviewPanel } from './spaceSettingsPanels/OverviewPanel'; import { MembersPanel } from './spaceSettingsPanels/MembersPanel'; import { RolesPanel } from './spaceSettingsPanels/RolesPanel'; import type { InstanceStreamingLimits, SpaceVisibility, JoinRequest } from '@backspace/shared'; const VALID_RESOLUTIONS = [540, 720, 1080] as const; const VALID_FRAMERATES = [30, 45, 60] as const; function formatKbps(kbps: number): string { return kbps >= 1000 ? `${(kbps / 1000).toFixed(kbps % 1000 === 0 ? 0 : 1)} Mbps` : `${kbps} kbps`; } function StreamingLimitsPanel() { const limits = useSettingsStore((s) => s.streamingLimits); const updateStreamingLimits = useSettingsStore((s) => s.updateStreamingLimits); const [draft, setDraft] = useState(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(''); const [saveSuccess, setSaveSuccess] = useState(false); useEffect(() => { if (limits) setDraft({ ...limits }); }, [limits]); if (!draft) return
Loading settings...
; const hasChanges = JSON.stringify(draft) !== JSON.stringify(limits); const handleSave = async () => { setSaving(true); setSaveError(''); setSaveSuccess(false); try { await updateStreamingLimits(draft); setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 2000); } catch (err) { setSaveError(err instanceof Error ? err.message : 'Failed to save'); } finally { setSaving(false); } }; const handleReset = () => { if (limits) setDraft({ ...limits }); setSaveError(''); }; const toggleResolution = (res: number) => { const current = new Set(draft.allowedResolutions); if (current.has(res)) { if (current.size <= 1) return; // Must have at least one current.delete(res); } else { current.add(res); } setDraft({ ...draft, allowedResolutions: Array.from(current).sort((a, b) => a - b) }); }; const toggleFramerate = (fps: number) => { const current = new Set(draft.allowedFramerates); if (current.has(fps)) { if (current.size <= 1) return; current.delete(fps); } else { current.add(fps); } setDraft({ ...draft, allowedFramerates: Array.from(current).sort((a, b) => a - b) }); }; const pillBase = 'px-3 py-1.5 rounded text-[13px] font-medium transition-colors cursor-pointer select-none'; const pillOn = 'bg-accent-primary text-white'; const pillOff = 'bg-surface-elevated text-txt-secondary hover:bg-interactive-hover'; return (
These limits apply to all users on this instance. Users can pick values within these bounds.
{/* Bitrate Range */}
Bitrate Range
setDraft({ ...draft, minBitrateKbps: Number(e.target.value) })} className="w-full h-1.5 accent-accent-primary cursor-pointer appearance-none bg-interactive-muted rounded-full [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-0" />
{formatKbps(draft.minBitrateKbps)}
setDraft({ ...draft, maxBitrateKbps: Number(e.target.value) })} className="w-full h-1.5 accent-accent-primary cursor-pointer appearance-none bg-interactive-muted rounded-full [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-0" />
{formatKbps(draft.maxBitrateKbps)}
{/* Bitrate Step */}
Slider Step
{ const v = Number(e.target.value); if (v >= 50 && v <= 5000) setDraft({ ...draft, bitrateStepKbps: v }); }} className="w-24 px-2 py-1 bg-surface-input rounded text-sm text-txt-primary outline-none focus:ring-1 focus:ring-accent-primary" /> kbps
{/* Allowed Resolutions */}
Allowed Resolutions
{VALID_RESOLUTIONS.map((res) => ( ))}
{/* Allowed Frame Rates */}
Allowed Frame Rates
{VALID_FRAMERATES.map((fps) => ( ))}
{/* Save / Reset */} {saveError && (
{saveError}
)} {saveSuccess && (
Settings saved
)} {hasChanges && (
)}
); } function DiscoveryPanel({ spaceId }: { spaceId: string }) { const spaces = useSpaceStore((s) => s.spaces); const updateSpace = useSpaceStore((s) => s.updateSpace); const discoveryEnabled = useSettingsStore((s) => s.streamingLimits?.discoveryEnabled ?? true); const space = spaces.find(s => s.id === spaceId); const [visibility, setVisibility] = useState( (space?.visibility as SpaceVisibility) ?? 'private' ); const [description, setDescription] = useState(space?.description ?? ''); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(''); const [saveSuccess, setSaveSuccess] = useState(false); useEffect(() => { if (space) { setVisibility((space.visibility as SpaceVisibility) ?? 'private'); setDescription(space.description ?? ''); } }, [space]); if (!space) return null; const hasChanges = visibility !== ((space.visibility as SpaceVisibility) ?? 'private') || description !== (space.description ?? ''); const handleSave = async () => { setSaving(true); setSaveError(''); setSaveSuccess(false); try { await api.spaces.update(spaceId, { visibility, description: description.trim() }); setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 2000); } catch (err) { setSaveError(err instanceof Error ? err.message : 'Failed to save'); } finally { setSaving(false); } }; const handleReset = () => { setVisibility((space.visibility as SpaceVisibility) ?? 'private'); setDescription(space.description ?? ''); setSaveError(''); }; const visibilityOptions: { value: SpaceVisibility; label: string; desc: string }[] = [ { value: 'private', label: 'Private', desc: 'Only people with an invite link can join' }, { value: 'request', label: 'Request to Join', desc: 'Visible in Explore — people can request to join' }, { value: 'public', label: 'Public', desc: 'Visible in Explore — anyone can join instantly' }, ]; return (
{!discoveryEnabled && (
Space discovery is disabled by the instance administrator. Changing visibility will have no effect until discovery is re-enabled.
)}
Visibility
{visibilityOptions.map((opt) => ( ))}
Description