Add poker:leave on unmount and poker:kick to remove ghost participants
All checks were successful
Build & Push Container Image / build (push) Successful in 9s
All checks were successful
Build & Push Container Image / build (push) Successful in 9s
Frontend now emits poker:leave when PokerRoom unmounts, preventing ghost participants. Also adds poker:kick socket event so any session participant can remove a stale user — shows a small X button next to each participant. Fixes deadlocked sessions where a disconnected user blocks reveal (votes.size can never equal participants.size). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a7aac985d2
commit
3051119405
2 changed files with 45 additions and 7 deletions
|
|
@ -343,6 +343,28 @@ io.on('connection', (socket) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
throttled('poker:kick', async ({ sessionId, userKey }) => {
|
||||||
|
try {
|
||||||
|
if (!sessionId || !userKey) {
|
||||||
|
socket.emit('poker:error', { error: 'sessionId and userKey are required.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!await canAccessSession(sessionId, socket.user.jiraCloudId)) {
|
||||||
|
socket.emit('poker:error', { error: 'Session not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await leaveSession({
|
||||||
|
sessionId,
|
||||||
|
tenantCloudId: socket.user.jiraCloudId,
|
||||||
|
userKey
|
||||||
|
});
|
||||||
|
await emitSessionState(sessionId, socket.user.jiraCloudId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[socket] poker:kick failed:', error);
|
||||||
|
socket.emit('poker:error', { error: safeError(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
throttled('poker:leave', async ({ sessionId }) => {
|
throttled('poker:leave', async ({ sessionId }) => {
|
||||||
try {
|
try {
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
socket.emit('poker:leave', { sessionId: session.id });
|
||||||
socket.off('poker:participants', onParticipants);
|
socket.off('poker:participants', onParticipants);
|
||||||
socket.off('poker:vote-update', onVoteUpdate);
|
socket.off('poker:vote-update', onVoteUpdate);
|
||||||
socket.off('poker:revealed', onRevealed);
|
socket.off('poker:revealed', onRevealed);
|
||||||
|
|
@ -98,6 +99,10 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKick(userKey) {
|
||||||
|
socket.emit('poker:kick', { sessionId: session.id, userKey });
|
||||||
|
}
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
const estimate = Number(manualEstimate || suggestedEstimate);
|
const estimate = Number(manualEstimate || suggestedEstimate);
|
||||||
if (!Number.isFinite(estimate)) {
|
if (!Number.isFinite(estimate)) {
|
||||||
|
|
@ -229,13 +234,24 @@ export default function PokerRoom({ session, issue, user, onSaved }) {
|
||||||
{isCurrentUser && <span className="text-slate-400"> (you)</span>}
|
{isCurrentUser && <span className="text-slate-400"> (you)</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm shrink-0 ml-2">
|
<div className="flex items-center gap-2 shrink-0 ml-2">
|
||||||
{revealed && vote !== null && vote !== undefined ? (
|
<span className="text-sm">
|
||||||
<span className="font-semibold text-purple-600 dark:text-purple-400">{vote}</span>
|
{revealed && vote !== null && vote !== undefined ? (
|
||||||
) : hasVoted ? (
|
<span className="font-semibold text-purple-600 dark:text-purple-400">{vote}</span>
|
||||||
<span className="text-green-600 dark:text-green-400">✓</span>
|
) : hasVoted ? (
|
||||||
) : (
|
<span className="text-green-600 dark:text-green-400">✓</span>
|
||||||
<span className="text-slate-400">...</span>
|
) : (
|
||||||
|
<span className="text-slate-400">...</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{!isCurrentUser && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleKick(participant.userKey)}
|
||||||
|
className="text-slate-400 hover:text-red-500 dark:hover:text-red-400 text-xs cursor-pointer bg-transparent border-none p-0"
|
||||||
|
title="Remove participant"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue