mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
fix spectator page, add discord ping filtering
This commit is contained in:
@@ -25,7 +25,7 @@ function displayName(message) {
|
||||
return message.nickname || message.socketId?.slice(0, 6) || 'unknown';
|
||||
}
|
||||
|
||||
export default function ChatPanel() {
|
||||
export default function ChatPanel({ hideInput = false, hideSpectatorNotice = false }) {
|
||||
const { session } = useSession();
|
||||
const { messages, sendMessage, registerInputRef, onInputFocus, onInputBlur, blurChat } = useChat();
|
||||
const [draft, setDraft] = useState('');
|
||||
@@ -42,7 +42,7 @@ export default function ChatPanel() {
|
||||
|
||||
async function handleSend(event) {
|
||||
event.preventDefault();
|
||||
if (!canChat) return;
|
||||
if (!canChat || hideInput) return;
|
||||
const clean = draft.trim();
|
||||
if (!clean) return;
|
||||
setSending(true);
|
||||
@@ -58,12 +58,12 @@ export default function ChatPanel() {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-base">
|
||||
<section className="panel-section flex h-full flex-col space-y-0.5 text-base">
|
||||
{/* <div className="flex items-center justify-between text-sm text-slate-400">
|
||||
<span>Chat</span>
|
||||
<span className="text-xs text-slate-500">{sorted.length}</span>
|
||||
</div> */}
|
||||
<div className="surface h-48 overflow-y-auto space-y-0.25" ref={listRef}>
|
||||
<div className="surface flex-1 overflow-y-auto space-y-0.25" ref={listRef}>
|
||||
{sorted.length === 0 ? (
|
||||
<p className="text-sm text-slate-500">No messages yet.</p>
|
||||
) : (
|
||||
@@ -99,21 +99,23 @@ export default function ChatPanel() {
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<form className="flex gap-0.5" onSubmit={handleSend}>
|
||||
<input
|
||||
className="field-input flex-1"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onFocus={onInputFocus}
|
||||
onBlur={onInputBlur}
|
||||
ref={(el) => registerInputRef(el)}
|
||||
placeholder={canChat ? 'Type a message…' : 'Spectators cannot chat'}
|
||||
disabled={!canChat}
|
||||
/>
|
||||
<button type="submit" disabled={!canChat || sending} className="button-dark disabled:opacity-50">
|
||||
{sending ? '...' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
{!hideInput && (
|
||||
<form className="flex gap-0.5" onSubmit={handleSend}>
|
||||
<input
|
||||
className="field-input flex-1"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onFocus={onInputFocus}
|
||||
onBlur={onInputBlur}
|
||||
ref={(el) => registerInputRef(el)}
|
||||
placeholder={canChat ? 'Type a message…' : hideSpectatorNotice ? '' : 'Spectators cannot chat'}
|
||||
disabled={!canChat}
|
||||
/>
|
||||
<button type="submit" disabled={!canChat || sending} className="button-dark disabled:opacity-50">
|
||||
{sending ? '...' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import AuthPanel from './AuthPanel.jsx';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import DiscordInviteButton from './DiscordInviteButton.jsx';
|
||||
import ChatPanel from './ChatPanel.jsx';
|
||||
import NicknameForm from './NicknameForm.jsx';
|
||||
|
||||
const PRIVILEGED_ROLES = new Set(['admin', 'lockdown', 'lockdown-admin']);
|
||||
const RESTRICTED_MODES = new Set(['admin', 'lockdown']);
|
||||
@@ -46,6 +48,9 @@ export default function ModeGateOverlay() {
|
||||
<div className='w-full justify-center items-center'>
|
||||
<DiscordInviteButton text='Join our Discord server for updates!'/>
|
||||
</div>
|
||||
You can use the chat from here though :3
|
||||
<ChatPanel />
|
||||
<NicknameForm />
|
||||
{/* <p className="text-xs text-slate-500">
|
||||
Your controls are paused until access is granted. You will automatically regain the interface once the mode
|
||||
changes or after a successful login.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
|
||||
export default function NicknameForm({ compact = false }) {
|
||||
const { session, setNickname } = useSession();
|
||||
const { value, save } = useSettingsNamespace('profile', { nickname: '' });
|
||||
const [nicknameInput, setNicknameInput] = useState(value.nickname || '');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const canSetNickname = session?.role !== 'spectator';
|
||||
|
||||
useEffect(() => {
|
||||
setNicknameInput(value.nickname || '');
|
||||
}, [value.nickname]);
|
||||
|
||||
async function handleSave(event) {
|
||||
event.preventDefault();
|
||||
if (!canSetNickname) return;
|
||||
const trimmed = (nicknameInput || '').trim().slice(0, 32);
|
||||
if (!trimmed) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await setNickname(trimmed);
|
||||
save({ nickname: trimmed });
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex gap-0.5" onSubmit={handleSave}>
|
||||
<input
|
||||
className="field-input flex-1"
|
||||
value={nicknameInput}
|
||||
onChange={(e) => setNicknameInput(e.target.value)}
|
||||
maxLength={32}
|
||||
placeholder="Enter a nickname"
|
||||
disabled={!canSetNickname}
|
||||
/>
|
||||
<button type="submit" disabled={!canSetNickname || saving} className="button-dark disabled:opacity-50">
|
||||
{saving ? 'Saving…' : compact ? 'Set' : 'Save'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,12 @@ function normalizeOrientation(value, fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export default function RoomCameraPanel({ defaultOrientation = 'horizontal', orientation: forcedOrientation }) {
|
||||
export default function RoomCameraPanel({
|
||||
defaultOrientation = 'horizontal',
|
||||
orientation: forcedOrientation,
|
||||
hideLayoutToggle = false,
|
||||
hideHeader = false,
|
||||
}) {
|
||||
const { session } = useSession();
|
||||
const cameras = session?.roomCameras || [];
|
||||
const sourceDescriptors = cameras.map((camera) => ({ type: 'room', id: camera.id, key: `room:${camera.id}` }));
|
||||
@@ -34,7 +39,7 @@ export default function RoomCameraPanel({ defaultOrientation = 'horizontal', ori
|
||||
: orientation;
|
||||
const containerClass =
|
||||
effectiveOrientation === 'vertical' ? 'flex flex-col gap-0.5' : 'grid gap-0.5 md:grid-cols-2';
|
||||
const showLayoutToggle = !forcedOrientation && cameras.length > 0;
|
||||
const showLayoutToggle = !hideLayoutToggle && !forcedOrientation && cameras.length > 0;
|
||||
|
||||
if (cameras.length === 0) {
|
||||
return <EmptyState />;
|
||||
@@ -42,29 +47,31 @@ export default function RoomCameraPanel({ defaultOrientation = 'horizontal', ori
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-base">
|
||||
<header className="flex flex-wrap items-center justify-between gap-0.5 text-sm text-slate-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<p>Room cameras</p>
|
||||
<span className="text-xs text-slate-500">{cameras.length}</span>
|
||||
</div>
|
||||
{showLayoutToggle && (
|
||||
<div className="flex items-center gap-0.5 text-xs">
|
||||
<span className="text-slate-500">Layout:</span>
|
||||
<div className="inline-flex overflow-hidden rounded border border-slate-700">
|
||||
{ORIENTATIONS.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={`px-1 py-0.5 ${effectiveOrientation === option ? 'bg-slate-600 text-white' : 'bg-transparent text-slate-400 hover:text-white'}`}
|
||||
onClick={() => setOrientation(option)}
|
||||
>
|
||||
{option === 'vertical' ? 'Vertical' : 'Grid'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!hideHeader && (
|
||||
<header className="flex flex-wrap items-center justify-between gap-0.5 text-sm text-slate-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<p>Room cameras</p>
|
||||
<span className="text-xs text-slate-500">{cameras.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
{showLayoutToggle && (
|
||||
<div className="flex items-center gap-0.5 text-xs">
|
||||
<span className="text-slate-500">Layout:</span>
|
||||
<div className="inline-flex overflow-hidden rounded border border-slate-700">
|
||||
{ORIENTATIONS.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={`px-1 py-0.5 ${effectiveOrientation === option ? 'bg-slate-600 text-white' : 'bg-transparent text-slate-400 hover:text-white'}`}
|
||||
onClick={() => setOrientation(option)}
|
||||
>
|
||||
{option === 'vertical' ? 'Vertical' : 'Grid'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
<div className={containerClass}>
|
||||
{cameras.map((camera) => {
|
||||
const key = `room:${camera.id}`;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import NicknameForm from './NicknameForm.jsx';
|
||||
|
||||
function roleColors(role) {
|
||||
switch (role) {
|
||||
@@ -25,11 +26,9 @@ function formatLabel(user, selfId) {
|
||||
return base;
|
||||
}
|
||||
|
||||
export default function UserListPanel() {
|
||||
export default function UserListPanel({ hideNicknameForm = false, hideHeader = false }) {
|
||||
const { session, setNickname } = useSession();
|
||||
const { value, save } = useSettingsNamespace('profile', { nickname: '' });
|
||||
const [nicknameInput, setNicknameInput] = useState(value.nickname || '');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { value } = useSettingsNamespace('profile', { nickname: '' });
|
||||
const lastSyncedSocketRef = useRef(null);
|
||||
const socket = useSocket();
|
||||
const canSetNickname = session?.role !== 'spectator';
|
||||
@@ -39,20 +38,17 @@ export default function UserListPanel() {
|
||||
const turnQueues = session?.turnQueues || {};
|
||||
const roster = session?.roster || [];
|
||||
|
||||
useEffect(() => {
|
||||
setNicknameInput(value.nickname || '');
|
||||
}, [value.nickname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canSetNickname) return;
|
||||
if (!session?.socketId) return;
|
||||
const nicknameInput = value.nickname || '';
|
||||
if (!nicknameInput) return;
|
||||
if (session.socketId === lastSyncedSocketRef.current) return;
|
||||
const currentId = session.socketId;
|
||||
setNickname(nicknameInput).then(() => {
|
||||
lastSyncedSocketRef.current = currentId;
|
||||
}).catch(() => {});
|
||||
}, [canSetNickname, nicknameInput, session?.socketId, setNickname]);
|
||||
}, [canSetNickname, session?.socketId, setNickname, value.nickname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return undefined;
|
||||
@@ -95,50 +91,25 @@ export default function UserListPanel() {
|
||||
return Math.ceil(ms / 1000);
|
||||
}, []);
|
||||
|
||||
async function handleSave(event) {
|
||||
event.preventDefault();
|
||||
if (!canSetNickname) return;
|
||||
const trimmed = (nicknameInput || '').trim().slice(0, 32);
|
||||
if (!trimmed) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await setNickname(trimmed);
|
||||
save({ nickname: trimmed });
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-base">
|
||||
<div className="space-y-0.5">
|
||||
{/* <p className="text-sm text-slate-400">Nickname</p> */}
|
||||
<form className="flex gap-0.5" onSubmit={handleSave}>
|
||||
<input
|
||||
className="field-input flex-1"
|
||||
value={nicknameInput}
|
||||
onChange={(e) => setNicknameInput(e.target.value)}
|
||||
maxLength={32}
|
||||
placeholder="Enter a nickname"
|
||||
disabled={!canSetNickname}
|
||||
/>
|
||||
<button type="submit" disabled={!canSetNickname || saving} className="button-dark disabled:opacity-50">
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</form>
|
||||
{!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>}
|
||||
</div>
|
||||
<section className="panel-section flex h-full flex-col space-y-0.5 text-base">
|
||||
{!hideNicknameForm && (
|
||||
<div className="space-y-0.5">
|
||||
<NicknameForm />
|
||||
{!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center justify-between text-sm text-slate-400">
|
||||
<span>{isTurnsMode ? 'Turn queues' : 'Users'}</span>
|
||||
<span className="text-xs text-slate-500">
|
||||
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="surface h-48 overflow-y-auto space-y-0.25">
|
||||
{!hideHeader && (
|
||||
<div className="flex items-center justify-between text-sm text-slate-400">
|
||||
<span>{isTurnsMode ? 'Turn queues' : 'Users'}</span>
|
||||
<span className="text-xs text-slate-500">
|
||||
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="surface flex-1 min-h-[12rem] overflow-y-auto space-y-0.25">
|
||||
{isTurnsMode ? (
|
||||
Object.keys(turnQueues || {}).length === 0 ? (
|
||||
<p className="text-sm text-slate-500">No turn queues yet.</p>
|
||||
|
||||
@@ -87,9 +87,13 @@ function RoverRow({ roster, frames, videoSources, session }) {
|
||||
function SecondaryRow() {
|
||||
return (
|
||||
<section className="grid grid-cols-1 gap-0.5 lg:grid-cols-[2fr_1fr_1fr]">
|
||||
<RoomCameraPanel defaultOrientation="horizontal" />
|
||||
<UserListPanel />
|
||||
<ChatPanel />
|
||||
<RoomCameraPanel defaultOrientation="horizontal" hideLayoutToggle hideHeader />
|
||||
<div className="flex flex-col">
|
||||
<UserListPanel hideNicknameForm hideHeader />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<ChatPanel hideInput hideSpectatorNotice />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user