fix(social): robot blocks
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
Blocking an auto-match opponent who is secretly a pooled robot is recorded instead in a separate `robot_blocks` table. Now blocking behaves the same in that game (struck name, hidden composer) and lists the blocked opponent under the name you saw, but is recorded only against that game — the disguise holds, the shared robot is never globally blocked, and the matchmaker keeps pairing you with robots (so you can never block yourself out of opponents). - the shared robot account is never put in `blocks` - the matchmaker keeps it free and it is not blocked under its other per-game names - the blocked list and the in-game card still show it by joining that table; an unblock deletes the row
This commit is contained in:
@@ -17,8 +17,10 @@
|
||||
let busy = $state(false);
|
||||
let tick = $state(0);
|
||||
// The opponents the viewer has blocked: when every opponent is blocked the composer is hidden
|
||||
// (only the chat log remains), matching the backend guard that rejects messaging a blocked peer.
|
||||
// (only the chat log remains). blockedIds are blocked humans (by account); blockedRobotSeats are
|
||||
// the seats of blocked disguised robots in this game (a robot block is per game+seat).
|
||||
let blockedIds = $state(new Set<string>());
|
||||
let blockedRobotSeats = $state(new Set<number>());
|
||||
|
||||
const myId = $derived(app.session?.userId ?? '');
|
||||
const isMyTurn = $derived(
|
||||
@@ -40,7 +42,10 @@
|
||||
const v = view;
|
||||
if (!v) return false;
|
||||
const opponents = v.game.seats.filter((s) => s.seat !== v.seat && !!s.accountId);
|
||||
return opponents.length > 0 && opponents.every((s) => blockedIds.has(s.accountId));
|
||||
return (
|
||||
opponents.length > 0 &&
|
||||
opponents.every((s) => blockedIds.has(s.accountId) || blockedRobotSeats.has(s.seat))
|
||||
);
|
||||
});
|
||||
const nudgeCooldownSecs = 3600;
|
||||
// The nudge is one-per-hour-per-game and clears once the player chats (engagement); the
|
||||
@@ -80,7 +85,8 @@
|
||||
if (app.profile?.isGuest) return;
|
||||
try {
|
||||
const bl = await gateway.blocksList();
|
||||
blockedIds = new Set(bl.map((b) => b.accountId));
|
||||
blockedIds = new Set(bl.blocked.map((b) => b.accountId));
|
||||
blockedRobotSeats = new Set(bl.robots.filter((r) => r.gameId === id).map((r) => r.seat));
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
|
||||
+33
-21
@@ -892,8 +892,11 @@
|
||||
let requested = $state(new Set<string>());
|
||||
// `blocked` are the opponents the viewer has blocked: their 🤝 and ✖️ controls disappear and
|
||||
// their name is struck. Derived from the server so it is correct across reloads and live-updates
|
||||
// on a user_blocked / user_unblocked event.
|
||||
// on a user_blocked / user_unblocked event. `blockedRobotSeats` are the seat indices of blocked
|
||||
// disguised-robot opponents in THIS game (a robot block is recorded per game+seat, not by
|
||||
// account, so it never touches the shared robot account or other games).
|
||||
let blocked = $state(new Set<string>());
|
||||
let blockedRobotSeats = $state(new Set<number>());
|
||||
// Per-seat "confirming" flags for the 🤝 → ✅ and ✖️ → ✅ tap-to-confirm (TapConfirm writes them);
|
||||
// while set, that seat's card shows "Add friend?" / "Block?" in place of the score, and the
|
||||
// opposite control is hidden so the two never overlap. Reset when history closes.
|
||||
@@ -913,17 +916,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
// loadBlocked refreshes the blocked set for a non-guest. Best-effort.
|
||||
// loadBlocked refreshes the blocked sets for a non-guest: blocked humans by account, plus the
|
||||
// seats of any blocked disguised robots in this game. Best-effort.
|
||||
async function loadBlocked() {
|
||||
if (app.profile?.isGuest) return;
|
||||
try {
|
||||
const bl = await gateway.blocksList();
|
||||
blocked = new Set(bl.map((b) => b.accountId));
|
||||
blocked = new Set(bl.blocked.map((b) => b.accountId));
|
||||
blockedRobotSeats = new Set(bl.robots.filter((r) => r.gameId === id).map((r) => r.seat));
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
|
||||
// seatBlocked reports whether the viewer has blocked this seat — a human (by account) or a
|
||||
// disguised robot (by this game's seat). It drives the struck name and hidden controls.
|
||||
function seatBlocked(s: { accountId: string; seat: number }): boolean {
|
||||
return blocked.has(s.accountId) || blockedRobotSeats.has(s.seat);
|
||||
}
|
||||
|
||||
// addFriend and blockUser apply the new relationship optimistically (so the controls and score
|
||||
// caption update the instant the confirm fires) and roll back to the prior state if the command
|
||||
// fails to reach the server. The confirming user_blocked / user_added event then just reconciles
|
||||
@@ -940,13 +951,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function blockUser(accountId: string) {
|
||||
const had = blocked.has(accountId);
|
||||
blocked = new Set([...blocked, accountId]);
|
||||
async function blockUser(s: { accountId: string; seat: number }) {
|
||||
// Optimistically mark this seat blocked (covers the struck name + hidden controls for both a
|
||||
// human and a disguised robot until the server confirms). The block carries the game id so a
|
||||
// robot opponent is recorded as a per-game block; the user_blocked event then reconciles.
|
||||
const hadSeat = blockedRobotSeats.has(s.seat);
|
||||
blockedRobotSeats = new Set([...blockedRobotSeats, s.seat]);
|
||||
try {
|
||||
await gateway.block(accountId);
|
||||
await gateway.block(s.accountId, id);
|
||||
} catch (e) {
|
||||
if (!had) blocked = new Set([...blocked].filter((id) => id !== accountId));
|
||||
if (!hadSeat) blockedRobotSeats = new Set([...blockedRobotSeats].filter((x) => x !== s.seat));
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
@@ -975,26 +989,24 @@
|
||||
// canAddFriend reports whether a seat shows the 🤝: a non-guest viewing a seated opponent
|
||||
// (not the still-empty seat of an open game) who is not yet a friend (an already-requested
|
||||
// opponent still shows it, but disabled).
|
||||
function canAddFriend(accountId: string): boolean {
|
||||
function canAddFriend(s: { accountId: string; seat: number }): boolean {
|
||||
// Never offer add-friend against an AI opponent, an existing friend, or a blocked player.
|
||||
if (view?.game.vsAi) return false;
|
||||
return (
|
||||
!!accountId &&
|
||||
!!s.accountId &&
|
||||
!app.profile?.isGuest &&
|
||||
accountId !== app.session?.userId &&
|
||||
!friends.has(accountId) &&
|
||||
!blocked.has(accountId)
|
||||
s.accountId !== app.session?.userId &&
|
||||
!friends.has(s.accountId) &&
|
||||
!seatBlocked(s)
|
||||
);
|
||||
}
|
||||
|
||||
// canBlock reports whether a seat shows the ✖️ block control: like canAddFriend, but a friend
|
||||
// may still be blocked (the block overrides the friendship), so it omits the friend exclusion.
|
||||
// An already-blocked opponent hides it (both controls go, and the name is struck).
|
||||
function canBlock(accountId: string): boolean {
|
||||
function canBlock(s: { accountId: string; seat: number }): boolean {
|
||||
if (view?.game.vsAi) return false;
|
||||
return (
|
||||
!!accountId && !app.profile?.isGuest && accountId !== app.session?.userId && !blocked.has(accountId)
|
||||
);
|
||||
return !!s.accountId && !app.profile?.isGuest && s.accountId !== app.session?.userId && !seatBlocked(s);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1044,22 +1056,22 @@
|
||||
{#if app.chatUnread[id]}<span class="unread-dot sbadge-dot"></span>{/if}
|
||||
{#each view.game.seats as s (s.seat)}
|
||||
<div class="seat" class:turn={view.game.toMove === s.seat && !gameOver} class:win={s.isWinner}>
|
||||
<div class="nm" class:struck={blocked.has(s.accountId)}>{seatName(s)}</div>
|
||||
<div class="nm" class:struck={seatBlocked(s)}>{seatName(s)}</div>
|
||||
<div class="sc" class:blockprompt={blockConfirm[s.seat]}>
|
||||
{#if blockConfirm[s.seat]}{t('game.blockShort')}{:else if addConfirm[s.seat]}{t('game.addFriendShort')}{:else}{s.score}{/if}
|
||||
</div>
|
||||
{#if historyShown && canBlock(s.accountId) && !addConfirm[s.seat]}
|
||||
{#if historyShown && canBlock(s) && !addConfirm[s.seat]}
|
||||
<span class="blockuser">
|
||||
<TapConfirm
|
||||
label={t('friends.blockFromGame')}
|
||||
onConfirming={(v) => (blockConfirm[s.seat] = v)}
|
||||
onconfirm={() => blockUser(s.accountId)}
|
||||
onconfirm={() => blockUser(s)}
|
||||
>
|
||||
<span class="fico">✖️</span>
|
||||
</TapConfirm>
|
||||
</span>
|
||||
{/if}
|
||||
{#if historyShown && canAddFriend(s.accountId) && !blockConfirm[s.seat]}
|
||||
{#if historyShown && canAddFriend(s) && !blockConfirm[s.seat]}
|
||||
<span class="addfriend">
|
||||
<TapConfirm
|
||||
label={t('friends.addFromGame')}
|
||||
|
||||
Reference in New Issue
Block a user