better popuping vs not

This commit is contained in:
legop3
2026-06-07 17:02:54 -04:00
parent 3cff250ba0
commit b2bd3a5e41
12 changed files with 217 additions and 155 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BysYkDl7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dr65gZI2.css">
<script type="module" crossorigin src="/assets/index-DZt_FmKq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BJwNeHyz.css">
</head>
<body>
<div id="root"></div>
@@ -55,7 +55,7 @@ function createBusEventHandler(deps) {
await sendToChannel(channelId, `${prefix}${content || ''}`.trim(), { embeds: payloadEmbeds, files: Array.isArray(files) ? files : undefined }, { parse: [], roles: pingRoleId ? [pingRoleId] : [] }, !pingRoleId);
}
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '', includeSidebar = true, jobId = null) {
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '', includeSidebar = true, jobId = null, requestedBy = null) {
if (!channelId) throw new Error('Replay channel not configured');
const job = createReplayJob({
id: jobId,
@@ -64,6 +64,7 @@ function createBusEventHandler(deps) {
title: explicitTitle,
sources,
includeSidebar,
requestedBy,
});
jobStatus.emit(job, 'accepted', { message: buildAcceptedMessage(job) });
const progressMessage = await sendToChannel(channelId, buildAcceptedMessage(job), {}, DEFAULT_ALLOWED_MENTIONS);
@@ -108,6 +109,7 @@ function createBusEventHandler(deps) {
payload?.title || '',
payload?.includeSidebar !== false,
payload?.jobId || null,
payload?.requestedBy || null,
).catch((err) => {
logger.warn('Replay send failed', { error: err.message });
});
@@ -115,6 +115,7 @@ function buildDiscordReplayMediaPayload({ message, attachment, job }) {
status: 'ready',
title: job.title,
requester: job.requester,
requestedBy: job.requestedBy || null,
url: attachment.url,
proxyUrl: attachment.proxyURL || attachment.proxyUrl || null,
messageUrl: message?.url || null,
@@ -131,7 +132,7 @@ function buildDiscordReplayMediaPayload({ message, attachment, job }) {
};
}
function createReplayJob({ id = null, requester = 'Discord', source = 'discord', title = '', sources = [], includeSidebar = true } = {}) {
function createReplayJob({ id = null, requester = 'Discord', source = 'discord', title = '', sources = [], includeSidebar = true, requestedBy = null } = {}) {
const normalizedSources = normalizeReplaySources(sources);
const resolvedTitle = buildReplayTitle({ explicitTitle: title, sources: normalizedSources });
return {
@@ -141,6 +142,7 @@ function createReplayJob({ id = null, requester = 'Discord', source = 'discord',
title: resolvedTitle,
sources: normalizedSources,
includeSidebar: includeSidebar !== false,
requestedBy: requestedBy && typeof requestedBy === 'object' ? { ...requestedBy } : null,
createdAt: nowTs(),
};
}
@@ -152,6 +154,7 @@ function createJobStatusEmitter({ io, logger, sanitizeMentions }) {
status,
title: job.title,
requester: job.requester,
requestedBy: job.requestedBy || null,
sources: normalizeReplaySources(job.sources),
message: extra.message ? sanitizeMentions(extra.message) : undefined,
media: extra.media || undefined,
@@ -9,7 +9,7 @@ const SEGMENT_ROOT = path.join(resolveDataDir(), 'replay-segments');
const SEGMENT_SECONDS = Math.max(1, Number.parseInt(process.env.REPLAY_SEGMENT_SECONDS || '1', 10));
const BUFFER_SECONDS = Math.max(20, Number.parseInt(process.env.REPLAY_BUFFER_SECONDS || '45', 10));
const CLEANUP_INTERVAL_MS = Math.max(500, Number.parseInt(process.env.REPLAY_TICK_MS || '2000', 10));
const BUILD_DURATION_MS = Math.max(5000, Number.parseInt(process.env.REPLAY_DURATION_MS || '35000', 10));
const BUILD_DURATION_MS = Math.max(5000, Number.parseInt(process.env.REPLAY_DURATION_MS || '20000', 10));
const BUILD_GUARD_MS = Math.max(200, Number.parseInt(process.env.REPLAY_GUARD_MS || '1200', 10));
const TARGET_FPS = Math.max(10, Number.parseInt(process.env.REPLAY_TARGET_FPS || '30', 10));
const MAX_WIDTH = Math.max(320, Number.parseInt(process.env.REPLAY_MAX_WIDTH || '1280', 10));
+3 -3
View File
@@ -279,8 +279,8 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
const fullscreenButtonSide = swapMobileControlColumns ? 'left' : 'right';
const showFloatingFullscreenButton = !isDesktop && (fullscreenIsIOS || fullscreenNativeSupported);
const latestReplay = useSessionSelector((state) => state.latestReplay);
const { clearLatestReplay } = useSessionActions();
const latestRequestedReplay = useSessionSelector((state) => state.latestRequestedReplay);
const { clearReplayModal } = useSessionActions();
const [helpVisible, setHelpVisible] = useState(false);
const [quickstartVisible, setQuickstartVisible] = useState(false);
@@ -363,7 +363,7 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
onOpenHelp={openHelpFromQuickstart}
onClose={closeQuickstart}
/>
<ReplayReadyPopup replay={latestReplay} onClose={clearLatestReplay} />
<ReplayReadyPopup replay={latestRequestedReplay} onClose={clearReplayModal} />
{showFloatingFullscreenButton ? (
<FloatingFullscreenButton
side={fullscreenButtonSide}
@@ -15,10 +15,11 @@ function formatBytes(value) {
return `${(size / 1024 / 1024).toFixed(1)} MB`;
}
export default function ReplayReadyPopup({ replay, onClose }) {
export default function ReplayReadyPopup({ replay, onClose, variant = 'modal' }) {
const videoUrl = normalizeUrl(replay?.url);
if (!videoUrl) return null;
const isPanel = variant === 'panel';
const title = String(replay?.title || 'Replay').trim() || 'Replay';
const messageUrl = normalizeUrl(replay?.messageUrl);
const meta = formatBytes(replay?.size) || null;
@@ -38,12 +39,35 @@ export default function ReplayReadyPopup({ replay, onClose }) {
</div>
);
return (
<div
className="fixed inset-0 z-[110] flex items-center justify-center bg-black/80 p-1"
onClick={onClose}
role="presentation"
const card = (
<CardFrame
title={title}
meta={meta}
actions={actions}
fillHeight={isPanel}
clipOverflow={false}
bodyClassName={`${isPanel ? 'flex min-h-0 flex-1 flex-col' : ''} space-y-0.5 p-0.5 text-sm text-slate-200`}
>
<div className={`${isPanel ? 'min-h-0 flex-1' : ''} overflow-hidden rounded bg-black`}>
<video
key={videoUrl}
src={videoUrl}
controls
autoPlay
preload="auto"
playsInline
className={`${isPanel ? 'h-full min-h-[10rem]' : 'aspect-video max-h-[72vh]'} w-full bg-black`}
/>
</div>
</CardFrame>
);
if (isPanel) {
return <div className="flex h-full min-h-[14rem] flex-col">{card}</div>;
}
return (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/80 p-1" onClick={onClose} role="presentation">
<div
className="pointer-events-auto w-full max-w-4xl"
onClick={(event) => {
@@ -52,19 +76,7 @@ export default function ReplayReadyPopup({ replay, onClose }) {
}}
role="presentation"
>
<CardFrame title={title} meta={meta} actions={actions} clipOverflow={false} bodyClassName="space-y-0.5 p-0.5 text-sm text-slate-200">
<div className="overflow-hidden rounded bg-black">
<video
key={videoUrl}
src={videoUrl}
controls
autoPlay
preload="auto"
playsInline
className="aspect-video max-h-[72vh] w-full bg-black"
/>
</div>
</CardFrame>
{card}
</div>
</div>
);
@@ -6,6 +6,7 @@ import { useSessionActions, useSessionSelector } from '../../context/SessionCont
import { useSettingsNamespace } from '../../settings/index.js';
import CardFrame from '../CardFrame/index.jsx';
import RoverLabel from '../RoverLabel/index.jsx';
import ReplayReadyPopup from './ReplayReadyPopup.jsx';
function normalizeSources(list = []) {
return list
@@ -26,8 +27,10 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
const mode = useSessionSelector((state) => state.session?.mode || null);
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const selfSocketId = useSessionSelector((state) => state.session?.socketId || null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const replayState = useSessionSelector((state) => state.session?.replay || null);
const latestReplay = useSessionSelector((state) => state.latestReplay);
const { triggerReplay } = useSessionActions();
const sources = normalizeSources(replaySources || []);
const { value: settings, save: saveSettings } = useSettingsNamespace('replaySources', {});
@@ -40,9 +43,24 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
const [includeSidebar, setIncludeSidebar] = useState(true);
const [remainingMs, setRemainingMs] = useState(0);
const [activeJobId, setActiveJobId] = useState(null);
const [dismissedPanelReplayId, setDismissedPanelReplayId] = useState(null);
const activeReplayJob = useSessionSelector((state) => (
activeJobId ? state.replayJobs?.[activeJobId] || null : null
));
const latestReplayJobId = latestReplay?.jobId || null;
const latestReplayRequesterSocketId = latestReplay?.requestedBy?.socketId || null;
const latestReplayRequestedBySelf = Boolean(
latestReplayJobId &&
selfSocketId &&
latestReplayRequesterSocketId &&
String(latestReplayRequesterSocketId) === String(selfSocketId),
);
const showPanelReplay = Boolean(
latestReplay?.url &&
latestReplayJobId &&
!latestReplayRequestedBySelf &&
dismissedPanelReplayId !== latestReplayJobId,
);
const defaults = useMemo(() => {
const roverId = assignmentRoverId;
@@ -173,6 +191,16 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
const listWrapClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : '';
if (showPanelReplay) {
return (
<ReplayReadyPopup
replay={latestReplay}
variant="panel"
onClose={() => setDismissedPanelReplayId(latestReplayJobId)}
/>
);
}
return (
<CardFrame title="Replay Sources" fillHeight={fillHeight} bodyClassName="space-y-0.5 text-sm">
<div className={`grid gap-0.5 md:grid-cols-2 ${listWrapClass}`}>
+18 -1
View File
@@ -17,6 +17,7 @@ const INITIAL_STATE = {
alerts: [],
replayJobs: {},
latestReplay: null,
latestRequestedReplay: null,
};
const SessionContext = createContext(null);
@@ -155,6 +156,9 @@ export function SessionProvider({ children }) {
if (!payload?.jobId || !payload?.url) return;
setState((prev) => {
const previous = prev.replayJobs?.[payload.jobId] || {};
const selfSocketId = String(prev.session?.socketId || '').trim();
const requesterSocketId = String(payload?.requestedBy?.socketId || '').trim();
const requestedByThisBrowser = Boolean(selfSocketId && requesterSocketId && selfSocketId === requesterSocketId);
const nextJob = {
...previous,
...payload,
@@ -168,12 +172,18 @@ export function SessionProvider({ children }) {
...(prev.replayJobs || {}),
[payload.jobId]: nextJob,
},
// Only the current replay popup is retained. Discord is the media host, so
// Only the latest replay media is retained. Discord is the media host, so
// this state is intentionally short-lived and does not become a replay library.
latestReplay: {
...payload,
receivedAt: Date.now(),
},
latestRequestedReplay: requestedByThisBrowser
? {
...payload,
receivedAt: Date.now(),
}
: prev.latestRequestedReplay,
};
});
}
@@ -307,6 +317,13 @@ export function SessionProvider({ children }) {
})),
clearLatestReplay: () =>
setState((prev) => (prev.latestReplay ? { ...prev, latestReplay: null } : prev)),
showReplayModal: (replay) =>
setState((prev) => ({
...prev,
latestRequestedReplay: replay ? { ...replay, receivedAt: Date.now() } : null,
})),
clearReplayModal: () =>
setState((prev) => (prev.latestRequestedReplay ? { ...prev, latestRequestedReplay: null } : prev)),
}),
[emitWithAck, setState],
);