feat(social): asymmetric per-user block, in-game block control, admin lists
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Make a per-user block one-directional and non-destructive: the blocker stops
receiving everything from the blocked user (chat, nudge, friend requests,
invitations) and the matchmaker never pairs them, while the blocked user
notices nothing — their sends still persist by the normal rules but are never
delivered or surfaced (born-read). A block no longer deletes the friendship
(an unblock cleanly restores it) and instant-reads any unread the blocked user
had left for the blocker.
- backend: a directional blockExists guard across chat/nudge/friends/invitations
(store-but-hide for the blocked->blocker direction, refuse blocker->blocked);
the matchmaker excludes a block-related pair (both directions) from auto-match;
user_blocked/user_unblocked notifications to the blocker only (in-app only).
- ui: the opponent score card gains a block ✖️ control mirroring add-friend
(red "Block?" confirm, mutual-hide while confirming, struck name, hidden chat
composer when blocked); optimistic apply + event confirm + rollback for both.
- admin: the user card gains cross-linked blocks / blocked-by / friends lists.
- docs: FUNCTIONAL(+ru), ARCHITECTURE §10 + decision record, UI_DESIGN, PRERELEASE.
This commit is contained in:
+21
-8
@@ -470,7 +470,9 @@ disguised robot stays indistinguishable from a person.
|
||||
(or joins a different player's) rather than returning the caller's own, so choosing
|
||||
"random opponent" again always starts a new search (bounded by the simultaneous
|
||||
quick-game cap, §9). Matchmaking state is therefore the **open games in the database** (not
|
||||
an in-memory pool), so it survives a restart and stays anonymous (no block check);
|
||||
an in-memory pool), so it survives a restart and stays anonymous beyond one filter — a
|
||||
player is never paired into a game whose waiting opponent they have a per-user block with,
|
||||
in either direction (the enqueue excludes the caller's `BlockedWith` set);
|
||||
concurrent enqueues for one bucket are serialised by a transaction-scoped advisory
|
||||
lock so two callers pair rather than each opening a game. A background **reaper**
|
||||
seats a pooled robot (§7) in any open game whose wait window — a fixed **90 s** plus
|
||||
@@ -506,12 +508,19 @@ disguised robot stays indistinguishable from a person.
|
||||
expires after **30 days** and may be re-sent), or **decline** — a decline is
|
||||
remembered (`status='declined'`) and blocks further requests from that sender,
|
||||
unless they hand them a code, which overrides it. The requester's own cancel still
|
||||
deletes the row; blocking someone severs an existing friendship. (Discovery by
|
||||
friend list or platform deep-link is future work.)
|
||||
deletes the row. (Discovery by friend list or platform deep-link is future work.)
|
||||
- **Block**: two independent **global** account toggles (`block_chat`,
|
||||
`block_friend_requests`) **plus** a **per-user block list**. A per-user block is
|
||||
applied mutually: it hides the pair's chat from each other and refuses friend
|
||||
requests and game invitations between them.
|
||||
**asymmetric and non-destructive**: the blocker stops receiving everything **from** the
|
||||
blocked user — chat, nudges, friend requests, game invitations, and auto-match (§6) never
|
||||
pairs them — while the blocked user notices nothing. Their sends still persist by the normal
|
||||
rules but are never delivered or surfaced to the blocker (a directional `blockExists` check
|
||||
drives this: the blocker filters/refuses, the blocked is silently suppressed and born-read),
|
||||
and applying a block also marks read any unread the blocked user had left for the blocker. It
|
||||
**overrides but does not delete** a friendship (an unblock cleanly restores it), so a pair may
|
||||
be friends **and** blocked at once; the admin user card shows the full truth (blocks,
|
||||
blocked-by, friends) regardless of this suppression. Block/unblock emit a `user_blocked` /
|
||||
`user_unblocked` notification to the blocker only (§10).
|
||||
- **Friend games**: formed by **invitation → accept** (an `game_invitations`
|
||||
record with one row per invitee). The 2–4 player game starts once **every**
|
||||
invitee accepts; any decline cancels the invitation, and a pending invitation
|
||||
@@ -693,7 +702,10 @@ opponent card and the resign/chat controls update **in place**; **game-over** ca
|
||||
carry the changed account / invitation, so the client patches its lobby lists in place: **invitation**
|
||||
and **invitation-update** carry the full invitation, and the client upserts a still-pending one and
|
||||
drops a terminal one (started, declined, cancelled, expired) — the invitations list is a delta channel
|
||||
too, fresh from any screen without a refetch. The move-commit **response** (`submit_play` / `pass` /
|
||||
too, fresh from any screen without a refetch. The **user-blocked** / **user-unblocked** sub-kinds
|
||||
confirm a per-user block change to the **blocker only** (never the blocked user), carrying the other
|
||||
account, so the blocker's open game screens re-derive the block / add-friend controls and the struck
|
||||
name in place across sessions. The move-commit **response** (`submit_play` / `pass` /
|
||||
`exchange` / `resign`) likewise returns the actor's own refilled rack and bag size, so the mover
|
||||
renders the next turn without a self-refetch. Beyond that event-driven warming, the lobby
|
||||
**preloads** the player's ongoing games — each one's `game.state`, `game.history` and saved
|
||||
@@ -720,8 +732,9 @@ localized message with a Mini App deep-link button — only when the recipient h
|
||||
identity and has not confined notifications to the app, so the two channels never duplicate. The
|
||||
connector routes by that language to the matching bot and renders the message in it. The out-of-app set is
|
||||
your-turn, game-over, nudge and the **invitation** (a new invitation) / friend-request notify sub-kinds;
|
||||
the connector renders the message and skips the rest — so the in-app-only **invitation-update** (a
|
||||
response/withdrawal lobby sync) never becomes a platform push. Operator broadcasts
|
||||
the connector renders the message and skips the rest — so in-app-only sub-kinds like
|
||||
**invitation-update** (a response/withdrawal lobby sync) and **user-blocked/-unblocked** (a
|
||||
block-state sync to the blocker) never become a platform push. Operator broadcasts
|
||||
(`SendToUser` / `SendToGameChannel`, §10 admin) instead pick the bot by an
|
||||
**operator-chosen** language in the console, unrelated to the recipient's login. Session-revocation events and
|
||||
cursor-based stream resume stay deferred (single-instance MVP).
|
||||
|
||||
Reference in New Issue
Block a user