diff --git a/ui/e2e/social.spec.ts b/ui/e2e/social.spec.ts
index 6744c15..64185ea 100644
--- a/ui/e2e/social.spec.ts
+++ b/ui/e2e/social.spec.ts
@@ -116,6 +116,9 @@ test('finished game draws an inert footer and trims live-only controls', async (
await page.getByRole('button', { name: 'Chat' }).click(); // 💬
await expect(page.locator('.pane')).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Dictionary' })).toHaveCount(0);
+ // A finished game is read-only: neither the message field nor the nudge is offered.
+ await expect(page.getByRole('button', { name: 'Send' })).toHaveCount(0);
+ await expect(page.getByRole('button', { name: 'Nudge' })).toHaveCount(0);
});
test('lobby: hiding a finished game removes it (kebab → ❌), keeping the others', async ({ page }) => {
diff --git a/ui/src/game/Chat.svelte b/ui/src/game/Chat.svelte
index 1d88146..1b47305 100644
--- a/ui/src/game/Chat.svelte
+++ b/ui/src/game/Chat.svelte
@@ -9,6 +9,7 @@
busy,
myTurn = false,
canSend = false,
+ canNudge = false,
waiting = false,
nudgeOnCooldown = false,
onsend,
@@ -17,16 +18,18 @@
messages: ChatMessage[];
myId: string;
busy: boolean;
- // The input area has three turn-driven states: on your own turn the message field +
- // send show while you may still post (canSend); once the one allowed message is sent a
- // short caption replaces them until the next turn; on the opponent's turn only the
- // nudge button shows (nudging your own turn makes no sense — there is no one to hurry).
- // While the hourly nudge cooldown is active the nudge is disabled with an "awaiting
- // reply" caption.
+ // The input area is turn-driven, in priority order: while you may still post (canSend)
+ // the message field + send show; once your one allowed message is sent a short caption
+ // replaces them until the next turn (myTurn); on the opponent's turn of an active game
+ // the nudge button shows (canNudge) — disabled with an "awaiting reply" caption while the
+ // hourly cooldown is active; a finished game offers nothing (it is read-only).
myTurn?: boolean;
// canSend is true while the player may still post this turn (own turn and the per-turn
// limit not yet reached); it gates the message field.
canSend?: boolean;
+ // canNudge is true on the opponent's turn of an active game; it gates the nudge button
+ // (nudging your own turn or a finished game makes no sense).
+ canNudge?: boolean;
// waiting is true while an auto-match game still has no opponent: both send and nudge
// are disabled (there is no one to message or hurry yet).
waiting?: boolean;
@@ -71,7 +74,7 @@
{t('chat.sentThisTurn')}
- {:else}
+ {:else if canNudge}
{nudgeOnCooldown ? t('chat.awaitingReply') : ''}
diff --git a/ui/src/game/ChatScreen.svelte b/ui/src/game/ChatScreen.svelte
index ba5a527..551d045 100644
--- a/ui/src/game/ChatScreen.svelte
+++ b/ui/src/game/ChatScreen.svelte
@@ -26,6 +26,9 @@
// the turn advances. canSend hides the field once the limit is reached (and always on
// the opponent's turn).
const canSend = $derived(canSendChat(isMyTurn, view ? sentThisTurn(messages, myId, view.game.lastActivityUnix) : 0));
+ // The nudge is offered only on the opponent's turn of an active game (you hurry the player
+ // who is to move). A finished game is read-only: neither chat nor nudge is offered.
+ const canNudge = $derived(!!view && view.game.status === 'active' && view.game.toMove !== view.seat);
// While the auto-match game still has no opponent, chat and nudge are both disabled.
const waiting = $derived(!!view && view.game.status === 'open');
const nudgeCooldownSecs = 3600;
@@ -110,4 +113,4 @@
}
-
+