add turn alerts client-side

This commit is contained in:
legop3
2025-11-22 20:26:21 -05:00
parent 988554dc5f
commit a9f2d62d30
6 changed files with 69 additions and 7 deletions
File diff suppressed because one or more lines are too long
Binary file not shown.
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-TQSYlxub.js"></script> <script type="module" crossorigin src="/assets/index-BW9FtSio.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CkWekXQn.css"> <link rel="stylesheet" crossorigin href="/assets/index-CkWekXQn.css">
</head> </head>
<body> <body>
+2
View File
@@ -17,6 +17,7 @@ import RightPaneTabs from './components/RightPaneTabs.jsx';
import ModeGateOverlay from './components/ModeGateOverlay.jsx'; import ModeGateOverlay from './components/ModeGateOverlay.jsx';
import SessionSnapshot from './components/SessionSnapshot.jsx'; import SessionSnapshot from './components/SessionSnapshot.jsx';
import HomeAssistantControls from './components/HomeAssistantControls.jsx'; import HomeAssistantControls from './components/HomeAssistantControls.jsx';
import TurnAlertListener from './components/TurnAlertListener.jsx';
import UserListPanel from './components/UserListPanel.jsx'; import UserListPanel from './components/UserListPanel.jsx';
import ChatPanel from './components/ChatPanel.jsx'; import ChatPanel from './components/ChatPanel.jsx';
import FullscreenPrompt from './components/FullscreenPrompt.jsx'; import FullscreenPrompt from './components/FullscreenPrompt.jsx';
@@ -141,6 +142,7 @@ function App() {
{renderedLayout} {renderedLayout}
</main> </main>
<AlertFeed /> <AlertFeed />
<TurnAlertListener />
<ModeGateOverlay /> <ModeGateOverlay />
<FullscreenPrompt <FullscreenPrompt
visible={fullscreenVisible} visible={fullscreenVisible}
@@ -0,0 +1,55 @@
import { useEffect, useMemo, useRef } from 'react';
import turnSound from '../assets/turn_alert.mp3';
import { useSession } from '../context/SessionContext.jsx';
function useAudio(src) {
const audioRef = useRef(null);
useEffect(() => {
audioRef.current = new Audio(src);
audioRef.current.load();
}, [src]);
const play = () => {
if (!audioRef.current) return;
audioRef.current.currentTime = 0;
audioRef.current.play().catch(() => {});
};
return play;
}
export default function TurnAlertListener() {
const { session, pushAlert } = useSession();
const playSound = useAudio(turnSound);
const seenRoversRef = useRef(new Set());
const assignments = useMemo(() => session?.turnQueues || {}, [session?.turnQueues]);
const socketId = session?.socketId || null;
useEffect(() => {
if (!socketId) return;
const newlyMine = [];
Object.entries(assignments).forEach(([roverId, info]) => {
if (!info || !info.current || info.current !== socketId) return;
if (!seenRoversRef.current.has(roverId)) {
newlyMine.push(roverId);
seenRoversRef.current.add(roverId);
}
});
Object.keys(assignments).forEach((roverId) => {
if (!assignments[roverId] || assignments[roverId].current !== socketId) {
seenRoversRef.current.delete(roverId);
}
});
if (newlyMine.length === 0) return;
newlyMine.forEach((roverId) => {
const roverName = session?.roster?.find((r) => String(r.id) === String(roverId))?.name || roverId;
pushAlert({
title: 'Your turn!',
message: `You now control ${roverName}.`,
color: 'success',
});
});
playSound();
}, [assignments, playSound, pushAlert, session?.roster, socketId]);
return null;
}
+5
View File
@@ -96,6 +96,11 @@ export function SessionProvider({ children }) {
homeAssistantSetState: (entityId, state) => homeAssistantSetState: (entityId, state) =>
emitWithAck('homeAssistant:setState', { entityId, state }), emitWithAck('homeAssistant:setState', { entityId, state }),
setNickname: (nickname) => emitWithAck('nickname:set', { nickname }), setNickname: (nickname) => emitWithAck('nickname:set', { nickname }),
pushAlert: (alert) =>
setAlerts((prev) => [
...prev.slice(-49),
{ ...alert, receivedAt: Date.now(), id: alert.id || Math.random().toString(36).slice(2) },
]),
}), }),
[emitWithAck], [emitWithAck],
); );