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
+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],
);