Track participants at room level to fix premature auto-reveal
All checks were successful
Build & Push Container Image / build (push) Successful in 8s
All checks were successful
Build & Push Container Image / build (push) Successful in 8s
Participants were tracked per-session, so each new issue started with 0 participants. The first user to join+vote saw 1/1 = all voted, triggering premature reveal. Now members are tracked on the room object and persist across issues. revealIfComplete compares votes against room member count. Also fixes: disconnect handler was dead code (Socket.IO v4 empties socket.rooms before firing disconnect) — replaced with disconnecting. Added manual "Reveal Votes" button and poker:reveal socket handler. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
062510b6c6
commit
2d78b9ff07
6 changed files with 247 additions and 190 deletions
BIN
frontend/public/logo-256.png
Normal file
BIN
frontend/public/logo-256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getSocket } from '../services/socket';
|
||||
|
||||
const CARDS = [
|
||||
|
|
@ -17,8 +17,7 @@ const CARDS = [
|
|||
{ value: '100', label: '100', color: '#dc2626' }
|
||||
];
|
||||
|
||||
export default function PokerRoom({ session, issue, user, onSaved }) {
|
||||
const [participants, setParticipants] = useState([]);
|
||||
export default function PokerRoom({ session, issue, user, members, roomId, onSaved }) {
|
||||
const [votedUserKeys, setVotedUserKeys] = useState([]);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const [votes, setVotes] = useState({});
|
||||
|
|
@ -29,15 +28,8 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
|||
const [error, setError] = useState('');
|
||||
|
||||
const socket = useMemo(() => getSocket(), []);
|
||||
const joinedRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
function onParticipants(payload) {
|
||||
if (payload.sessionId !== session.id) return;
|
||||
setParticipants(payload.participants || []);
|
||||
setVotedUserKeys(payload.votedUserKeys || []);
|
||||
}
|
||||
|
||||
function onVoteUpdate(payload) {
|
||||
if (payload.sessionId !== session.id) return;
|
||||
setVotedUserKeys(payload.votedUserKeys || []);
|
||||
|
|
@ -63,30 +55,18 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
|||
setError(payload?.error || 'Unexpected poker error');
|
||||
}
|
||||
|
||||
socket.on('poker:participants', onParticipants);
|
||||
socket.on('poker:vote-update', onVoteUpdate);
|
||||
socket.on('poker:revealed', onRevealed);
|
||||
socket.on('poker:saved', onSavedPayload);
|
||||
socket.on('poker:error', onError);
|
||||
|
||||
// Guard against React strict mode double-invoking effects
|
||||
if (joinedRef.current !== session.id) {
|
||||
joinedRef.current = session.id;
|
||||
socket.emit('poker:join', {
|
||||
sessionId: session.id,
|
||||
userKey: user.key,
|
||||
userName: user.name
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
socket.off('poker:participants', onParticipants);
|
||||
socket.off('poker:vote-update', onVoteUpdate);
|
||||
socket.off('poker:revealed', onRevealed);
|
||||
socket.off('poker:saved', onSavedPayload);
|
||||
socket.off('poker:error', onError);
|
||||
};
|
||||
}, [session.id, socket, user.key, user.name]);
|
||||
}, [session.id, socket]);
|
||||
|
||||
function handleVote(value) {
|
||||
if (revealed) return;
|
||||
|
|
@ -99,7 +79,11 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
|||
}
|
||||
|
||||
function handleKick(userKey) {
|
||||
socket.emit('poker:kick', { sessionId: session.id, userKey });
|
||||
socket.emit('poker:kick', { roomId, userKey });
|
||||
}
|
||||
|
||||
function handleReveal() {
|
||||
socket.emit('poker:reveal', { sessionId: session.id });
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
|
|
@ -145,7 +129,7 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
|||
{/* Individual votes */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(votes).map(([userKey, value]) => {
|
||||
const name = participants.find((p) => p.userKey === userKey)?.userName || userKey;
|
||||
const name = members.find((p) => p.userKey === userKey)?.userName || userKey;
|
||||
return (
|
||||
<span
|
||||
key={userKey}
|
||||
|
|
@ -176,28 +160,38 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{CARDS.map((card) => {
|
||||
const isSelected = myVote === card.value;
|
||||
return (
|
||||
<button
|
||||
key={card.value}
|
||||
onClick={() => handleVote(card.value)}
|
||||
className="aspect-[2/3] rounded-sm border-2 transition-all flex flex-col items-center justify-center gap-1 hover:scale-105 cursor-pointer bg-transparent"
|
||||
style={{
|
||||
borderColor: isSelected ? card.color : 'var(--card-border, #e2e8f0)',
|
||||
backgroundColor: isSelected ? `${card.color}15` : 'transparent'
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="text-base font-bold"
|
||||
style={{ color: isSelected ? card.color : 'var(--card-text, #475569)' }}
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{CARDS.map((card) => {
|
||||
const isSelected = myVote === card.value;
|
||||
return (
|
||||
<button
|
||||
key={card.value}
|
||||
onClick={() => handleVote(card.value)}
|
||||
className="aspect-[2/3] rounded-sm border-2 transition-all flex flex-col items-center justify-center gap-1 hover:scale-105 cursor-pointer bg-transparent"
|
||||
style={{
|
||||
borderColor: isSelected ? card.color : 'var(--card-border, #e2e8f0)',
|
||||
backgroundColor: isSelected ? `${card.color}15` : 'transparent'
|
||||
}}
|
||||
>
|
||||
{card.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<span
|
||||
className="text-base font-bold"
|
||||
style={{ color: isSelected ? card.color : 'var(--card-text, #475569)' }}
|
||||
>
|
||||
{card.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{votedUserKeys.length > 0 && (
|
||||
<button
|
||||
onClick={handleReveal}
|
||||
className="w-full bg-amber-500 hover:bg-amber-400 text-white font-semibold rounded-sm px-4 py-2 transition-colors cursor-pointer border-none text-sm"
|
||||
>
|
||||
Reveal Votes ({votedUserKeys.length}/{members.length})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -212,7 +206,7 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
|||
</div>
|
||||
<div className="flex-1 p-4 bg-slate-50 dark:bg-slate-800/50">
|
||||
<div className="flex flex-col gap-2">
|
||||
{participants.map((participant) => {
|
||||
{members.map((participant) => {
|
||||
const hasVoted = revealed
|
||||
? votes[participant.userKey] !== undefined
|
||||
: votedUserKeys.includes(participant.userKey);
|
||||
|
|
|
|||
|
|
@ -11,21 +11,33 @@ export default function Room({ room, user, dark, toggleDark, onBack }) {
|
|||
const [done, setDone] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [members, setMembers] = useState([]);
|
||||
const issuesRef = useRef([]);
|
||||
|
||||
const pokerUser = { key: user.jiraAccountId, name: user.displayName };
|
||||
const socket = useMemo(() => getSocket(), []);
|
||||
const activeSessionIdRef = useRef(null);
|
||||
|
||||
// Emit poker:leave when the Room unmounts (user truly leaves the room)
|
||||
// Room join/leave lifecycle + members listener
|
||||
useEffect(() => {
|
||||
function onMembers(payload) {
|
||||
if (payload.roomId !== room.id) return;
|
||||
setMembers(payload.members || []);
|
||||
}
|
||||
|
||||
function onReconnect() {
|
||||
socket.emit('room:join', { roomId: room.id });
|
||||
}
|
||||
|
||||
socket.on('room:members', onMembers);
|
||||
socket.io.on('reconnect', onReconnect);
|
||||
socket.emit('room:join', { roomId: room.id });
|
||||
|
||||
return () => {
|
||||
const sessionId = activeSessionIdRef.current;
|
||||
if (sessionId) {
|
||||
socket.emit('poker:leave', { sessionId });
|
||||
}
|
||||
socket.off('room:members', onMembers);
|
||||
socket.io.off('reconnect', onReconnect);
|
||||
socket.emit('room:leave', { roomId: room.id });
|
||||
};
|
||||
}, [socket]);
|
||||
}, [socket, room.id]);
|
||||
|
||||
useEffect(() => {
|
||||
loadIssues();
|
||||
|
|
@ -82,7 +94,6 @@ export default function Room({ room, user, dark, toggleDark, onBack }) {
|
|||
|
||||
const activeSessionRef = useRef(null);
|
||||
activeSessionRef.current = activeSession;
|
||||
activeSessionIdRef.current = activeSession?.session?.id || null;
|
||||
|
||||
const advanceToNext = useCallback((estimate) => {
|
||||
if (!activeSessionRef.current) return;
|
||||
|
|
@ -189,6 +200,8 @@ export default function Room({ room, user, dark, toggleDark, onBack }) {
|
|||
session={activeSession.session}
|
||||
issue={activeSession.issue}
|
||||
user={pokerUser}
|
||||
members={members}
|
||||
roomId={room.id}
|
||||
onSaved={advanceToNext}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue