fix(call-to-play): expire elapsed calls clearly
Deadline completion previously shared the green Ready presentation with a full roster and remained visible forever. An abandoned call therefore looked ready to launch and required its creator to return and cancel it. Give elapsed calls a distinct Time's up state and a five-minute grace period in which the creator can start or extend them. After that, both the reducer and peer store remove the call as a unit. Filled calls remain ready until their deadline, and active calls continue to retain complete history for late joiners. Test Plan: - `just fmt` -- passed - `just clippy` -- passed - `just test` -- passed, 147 peer tests - `just frontend-test` -- passed, 22 tests - `just build` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
isReady,
|
||||
readyCountOf,
|
||||
statusOf,
|
||||
type CallToPlayStatus,
|
||||
} from '../../lib/callToPlay';
|
||||
import { Game, Nomination } from '../../lib/types';
|
||||
|
||||
@@ -25,9 +26,23 @@ const LABEL = {
|
||||
call: 'Call to Play',
|
||||
soon: 'Starting soon',
|
||||
ready: 'Ready',
|
||||
expired: 'Time’s up',
|
||||
} as const;
|
||||
|
||||
const RANK = { ready: 0, soon: 1, call: 2, scheduled: 2, started: 3 } as const;
|
||||
type TickerStatus = Exclude<CallToPlayStatus, 'started'>;
|
||||
|
||||
const RANK: Record<TickerStatus, number> = {
|
||||
expired: 0,
|
||||
ready: 1,
|
||||
soon: 2,
|
||||
call: 3,
|
||||
scheduled: 3,
|
||||
};
|
||||
|
||||
const tickerStatusOf = (nomination: Nomination, now: number): TickerStatus | null => {
|
||||
const status = statusOf(nomination, now);
|
||||
return status === 'started' ? null : status;
|
||||
};
|
||||
|
||||
const MiniBubbles = ({ nomination, now }: { nomination: Nomination; now: number }) => {
|
||||
const entries = Object.entries(nomination.participants);
|
||||
@@ -63,20 +78,19 @@ const MiniBubbles = ({ nomination, now }: { nomination: Nomination; now: number
|
||||
export const CallToPlayTicker = ({ nominations, games, accent, onOpen }: Props) => {
|
||||
const now = Date.now();
|
||||
const gameById = new Map(games.map(game => [game.id, game]));
|
||||
const active = nominations
|
||||
.filter(nomination => nomination.state !== 'started')
|
||||
.sort((left, right) =>
|
||||
RANK[statusOf(left, now)] - RANK[statusOf(right, now)]
|
||||
|| left.deadline - right.deadline
|
||||
);
|
||||
const active = nominations.flatMap(nomination => {
|
||||
const status = tickerStatusOf(nomination, now);
|
||||
return status === null ? [] : [{ nomination, status }];
|
||||
}).sort((left, right) =>
|
||||
RANK[left.status] - RANK[right.status]
|
||||
|| left.nomination.deadline - right.nomination.deadline
|
||||
);
|
||||
if (active.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="ctp-ticker-stack">
|
||||
{active.map(nomination => {
|
||||
{active.map(({ nomination, status }) => {
|
||||
const game = gameById.get(nomination.gameId);
|
||||
const status = statusOf(nomination, now);
|
||||
if (status === 'started') return null;
|
||||
const ready = readyCountOf(nomination, now);
|
||||
const total = Object.keys(nomination.participants).length;
|
||||
const remaining = Math.max(0, nomination.deadline - now);
|
||||
@@ -86,6 +100,8 @@ export const CallToPlayTicker = ({ nominations, games, accent, onOpen }: Props)
|
||||
: `${ready}/${nomination.maxPlayers} ready`;
|
||||
const time = status === 'ready'
|
||||
? 'waiting to start'
|
||||
: status === 'expired'
|
||||
? `${formatCountdownShort(now - nomination.deadline)} ago · waiting for caller`
|
||||
: status === 'scheduled'
|
||||
? `${formatClock(nomination.scheduledFor!)} · ${formatUntil(nomination.scheduledFor! - now)}`
|
||||
: nomination.scheduledFor !== null
|
||||
|
||||
Reference in New Issue
Block a user