From e1bcf61498464594a6849e4474acc0f1ae39d916 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 24 Jun 2026 15:06:37 +0200 Subject: [PATCH] fix: combat reveal toggle + eliminate poll flickering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ๐Ÿ‘/๐Ÿ”’ toggle to combat row in Scene Secrets โ€” players see combat panel only when GM reveals it (scene.reveal.combat). Stop flickering: background polls now run silent (no setLoading), and state updates are skipped when JSON content is unchanged, preventing unnecessary re-renders every 4s. Co-Authored-By: Claude Sonnet 4.6 --- src/components/MissionTab.jsx | 42 +++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/components/MissionTab.jsx b/src/components/MissionTab.jsx index 8b62296..a9152fc 100644 --- a/src/components/MissionTab.jsx +++ b/src/components/MissionTab.jsx @@ -177,9 +177,8 @@ export default function MissionTab({ authedPlayer }) { // โ”€โ”€โ”€ Data loading โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - const loadMissions = useCallback(async function loadMissions() { - setLoading(true); - setError(''); + const loadMissions = useCallback(async function loadMissions({ silent = false } = {}) { + if (!silent) { setLoading(true); setError(''); } try { if (isGM) { const [allRes, activeRes] = await Promise.all([ @@ -188,19 +187,18 @@ export default function MissionTab({ authedPlayer }) { ]); const all = Array.isArray(allRes.data) ? allRes.data.map(normalizeMission) : []; const active = normalizeMission(activeRes.data); - setMissions(all); - setActiveMission(active); - setSelectedMissionId(active?.id ? String(active.id) : (all[0]?.id ? String(all[0].id) : '')); + setMissions(prev => JSON.stringify(prev) !== JSON.stringify(all) ? all : prev); + setActiveMission(prev => JSON.stringify(prev) !== JSON.stringify(active) ? active : prev); + if (!silent) setSelectedMissionId(active?.id ? String(active.id) : (all[0]?.id ? String(all[0].id) : '')); } else { const activeRes = await axios.get('/api/missions/active/player'); - setMissions([]); - setActiveMission(normalizeMission(activeRes.data)); - setSelectedMissionId(''); + const active = normalizeMission(activeRes.data); + setActiveMission(prev => JSON.stringify(prev) !== JSON.stringify(active) ? active : prev); } } catch (e) { - setError(`Failed to load missions: ${e.response?.data?.error || e.message}`); + if (!silent) setError(`Failed to load missions: ${e.response?.data?.error || e.message}`); } finally { - setLoading(false); + if (!silent) setLoading(false); } }, [isGM]); @@ -208,7 +206,8 @@ export default function MissionTab({ authedPlayer }) { const loadRollFeed = useCallback(async function loadRollFeed() { try { const res = await axios.get('/api/missions/active/rolls/feed?limit=50'); - setRollFeed(Array.isArray(res.data) ? res.data : []); + const next = Array.isArray(res.data) ? res.data : []; + setRollFeed(prev => JSON.stringify(prev) !== JSON.stringify(next) ? next : prev); } catch (e) { // non-blocking } @@ -236,7 +235,7 @@ export default function MissionTab({ authedPlayer }) { useEffect(() => { if (isGM) return undefined; const timer = setInterval(() => { - loadMissions(); + loadMissions({ silent: true }); loadRollFeed(); }, 4000); return () => clearInterval(timer); @@ -291,6 +290,7 @@ export default function MissionTab({ authedPlayer }) { objectives: r.objectives ?? legacy, complications: r.complications ?? false, // never auto-revealed checks: r.checks || {}, + combat: r.combat ?? false, }; }, [currentScene]); @@ -407,7 +407,7 @@ export default function MissionTab({ authedPlayer }) { i === index ? { ...scene, ...updates } : scene ); await saveProgress(index, nextScenes); - await loadMissions(); + await loadMissions({ silent: true }); } async function updateCombatState(updates) { @@ -1208,10 +1208,10 @@ export default function MissionTab({ authedPlayer }) { {/* Combat Setup */}

Combat Setup

- {!isGM && !combatActive ? ( + {!isGM && !sceneReveal.combat ? (
โš”๏ธ
-

Combat briefing is classified.
Awaiting GM to initiate combat.

+

Combat briefing er klassificeret.
Afventer GM-ordre.

) : ( <> @@ -1255,7 +1255,7 @@ export default function MissionTab({ authedPlayer }) {
{/* โ”€โ”€ Combat state panels (initiative, conditions, action ref, fear) โ”€โ”€ */} - {combatActive && ( + {(combatActive || (!isGM && sceneReveal.combat)) && (
{/* Round counter + fear rating */} @@ -1786,6 +1786,14 @@ export default function MissionTab({ authedPlayer }) {
{sceneCombat.enemy || 'Custom/None'}
BS {sceneCombat.bs || 45} / WS {sceneCombat.ws || 45} ยท {sceneCombat.damage || '1d10+5'}
+ {/* Reveal toggle */} +