mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
waui
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import ChatMessageRow from './ChatMessageRow.jsx';
|
||||
import ChatTypingRow from './ChatTypingRow.jsx';
|
||||
import ButtonBoxTile from './ButtonBoxTile.jsx';
|
||||
|
||||
const LIFETIME_MS = 3000;
|
||||
const BUTTONBOX_LIFETIME_MS = 3500;
|
||||
const DEFAULT_COLOR = '#2196f3';
|
||||
|
||||
function buildKey(alert) {
|
||||
@@ -13,9 +16,37 @@ function buildKey(alert) {
|
||||
}
|
||||
|
||||
export default function AlertFeed({ scale = 1 }) {
|
||||
const { alerts } = useSession();
|
||||
const { alerts, session, pushAlert } = useSession();
|
||||
const socket = useSocket();
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const latest = useMemo(() => alerts.slice(-3).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]);
|
||||
|
||||
useEffect(() => {
|
||||
function onButtonIncrement(payload = {}) {
|
||||
const buttonId = Number(payload.buttonId);
|
||||
if (!Number.isFinite(buttonId) || buttonId < 1 || buttonId > 4) return;
|
||||
const buttons = Array.isArray(session?.buttonBox?.buttons) ? session.buttonBox.buttons : [];
|
||||
const button = buttons.find((entry) => Number(entry?.id) === buttonId) || {};
|
||||
const count = Number.isFinite(payload.count) ? payload.count : Number(button.count) || 0;
|
||||
const goal = Number.isFinite(button.goal) ? button.goal : 0;
|
||||
const rewardNumber = Number.isFinite(button.rewardNumber) ? button.rewardNumber : '?';
|
||||
const rewardName =
|
||||
typeof button.rewardName === 'string' && button.rewardName.trim()
|
||||
? button.rewardName.trim()
|
||||
: 'Unassigned';
|
||||
pushAlert({
|
||||
id: `buttonbox-active-${buttonId}`,
|
||||
kind: 'buttonbox-active',
|
||||
lifetimeMs: BUTTONBOX_LIFETIME_MS,
|
||||
payload: { buttonId, count, goal, rewardNumber, rewardName },
|
||||
});
|
||||
}
|
||||
socket.on('buttonBox:increment', onButtonIncrement);
|
||||
return () => {
|
||||
socket.off('buttonBox:increment', onButtonIncrement);
|
||||
};
|
||||
}, [pushAlert, session?.buttonBox?.buttons, socket]);
|
||||
|
||||
const latest = useMemo(() => alerts.slice(-12).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latest.length) return undefined;
|
||||
@@ -23,12 +54,15 @@ export default function AlertFeed({ scale = 1 }) {
|
||||
return () => clearInterval(interval);
|
||||
}, [latest.length]);
|
||||
|
||||
const visible = latest
|
||||
.map((item) => ({
|
||||
...item,
|
||||
age: now - (item.alert.receivedAt ?? item.alert.timestamp ?? 0),
|
||||
}))
|
||||
.filter((item) => item.age <= LIFETIME_MS);
|
||||
const visibleByKey = new Map();
|
||||
latest.forEach((item) => {
|
||||
const age = now - (item.alert.receivedAt ?? item.alert.timestamp ?? 0);
|
||||
const lifetimeMs = Number.isFinite(item.alert.lifetimeMs) ? item.alert.lifetimeMs : LIFETIME_MS;
|
||||
if (age <= lifetimeMs) {
|
||||
visibleByKey.set(item.key, { ...item, age });
|
||||
}
|
||||
});
|
||||
const visible = Array.from(visibleByKey.values()).slice(-3);
|
||||
|
||||
if (!visible.length) return null;
|
||||
|
||||
@@ -66,6 +100,21 @@ function hexToRgb(hex) {
|
||||
}
|
||||
|
||||
function AlertToast({ alert }) {
|
||||
if (alert.kind === 'buttonbox-active' && alert.payload) {
|
||||
const payload = alert.payload;
|
||||
return (
|
||||
<div className="pointer-events-none w-[12.5rem]">
|
||||
<ButtonBoxTile
|
||||
buttonId={payload.buttonId}
|
||||
count={payload.count}
|
||||
goal={payload.goal}
|
||||
rewardNumber={payload.rewardNumber}
|
||||
rewardName={payload.rewardName}
|
||||
className="bg-cyan-900/45"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (alert.kind === 'chat' && alert.payload) {
|
||||
return <ChatMessageRow message={alert.payload} />;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { AUDIO_SETTINGS_DEFAULTS } from '../settings/namespaces.js';
|
||||
import ButtonBoxTile from './ButtonBoxTile.jsx';
|
||||
|
||||
const FLASH_MS = 420;
|
||||
const REWARD_FLASH_MS = 1200;
|
||||
@@ -139,20 +140,15 @@ export default function ButtonBoxPanel() {
|
||||
const rewardActive = Boolean(rewardFlash[id]);
|
||||
|
||||
return (
|
||||
<article
|
||||
<ButtonBoxTile
|
||||
key={id}
|
||||
className={[
|
||||
'surface px-0.5 py-0.5 text-center',
|
||||
incActive ? 'bg-cyan-900/45' : '',
|
||||
rewardActive ? 'bg-fuchsia-900/45' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<p className="text-xs text-slate-400">Button {id}</p>
|
||||
<p className="text-sm font-semibold text-white">{count} / {goal}</p>
|
||||
<p className="truncate text-[0.7rem] text-slate-300">#{rewardNumber} {rewardName}</p>
|
||||
</article>
|
||||
buttonId={id}
|
||||
count={count}
|
||||
goal={goal}
|
||||
rewardNumber={rewardNumber}
|
||||
rewardName={rewardName}
|
||||
className={[incActive ? 'bg-cyan-900/45' : '', rewardActive ? 'bg-fuchsia-900/45' : ''].filter(Boolean).join(' ')}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export default function ButtonBoxTile({
|
||||
buttonId,
|
||||
count,
|
||||
goal,
|
||||
rewardNumber,
|
||||
rewardName,
|
||||
className = '',
|
||||
}) {
|
||||
return (
|
||||
<article className={['surface px-0.5 py-0.5 text-center', className].filter(Boolean).join(' ')}>
|
||||
<p className="text-xs text-slate-400">Button {buttonId}</p>
|
||||
<p className="text-sm font-semibold text-white">{count} / {goal}</p>
|
||||
<p className="truncate text-[0.7rem] text-slate-300">#{rewardNumber} {rewardName}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -177,8 +177,10 @@ function SpectatorContent() {
|
||||
{isPortraitLayout ? (
|
||||
<div className={`${topBarItemClass} ${portraitItemHeight} flex flex-col gap-0.5`}>
|
||||
<CommunityGoalBanner layout="desktop" dismissable={false} className="text-sm" />
|
||||
<div className="min-h-0 flex-1">
|
||||
<RawUserPilePanel hideNicknameForm hideHeader compact fillHeight className="h-full" />
|
||||
<div className="min-h-0 overflow-y-auto">
|
||||
<div className="panel h-full">
|
||||
<RoverQueuesPanel title="Rovers" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -187,9 +189,13 @@ function SpectatorContent() {
|
||||
</div>
|
||||
)}
|
||||
<div className={`${topBarItemClass} ${isPortraitLayout ? `${portraitItemHeight} overflow-y-auto` : ''}`}>
|
||||
<div className="panel h-full">
|
||||
<RoverQueuesPanel title="Rovers" />
|
||||
</div>
|
||||
{isPortraitLayout ? (
|
||||
<RawUserPilePanel hideNicknameForm hideHeader compact fillHeight className="h-full" />
|
||||
) : (
|
||||
<div className="panel h-full">
|
||||
<RoverQueuesPanel title="Rovers" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isPortraitLayout ? (
|
||||
<div className={`${topBarItemClass} flex-1 min-h-0`}>
|
||||
|
||||
Reference in New Issue
Block a user