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).
|
||||
|
||||
+20
-8
@@ -179,14 +179,26 @@ digits, valid for twelve hours), or send a **request to someone you have played
|
||||
with** — they accept, ignore it (a request lapses after thirty days and can then be
|
||||
re-sent), or decline (a decline blocks further requests from you until they hand you
|
||||
a code). Cancelling your own pending request withdraws it; unfriending removes the
|
||||
friendship. In a game, each opponent's score card carries an **add-to-friends 🤝** control (while the
|
||||
move history is open) that mirrors the live relationship: it confirms with a tap on a fading
|
||||
✅ (the card reads *Add friend?* while confirming), goes **disabled** while a request is
|
||||
pending or was declined, and **disappears** once you are friends — updating in place the
|
||||
moment the opponent answers, and staying correct across reloads. Block globally — switch off incoming chat
|
||||
and/or friend requests — and block individual players (a per-user block hides that
|
||||
person's chat and stops requests and game invitations both ways; it also ends any
|
||||
existing friendship). Per-game chat is for quick reactions: messages are short
|
||||
friendship. In a game, each opponent's score card (while the move history is open) carries two controls:
|
||||
an **add-to-friends 🤝** on the right and a **block ✖️** on the left. Each confirms with a tap
|
||||
on a fading ✅ — the card reads *Add friend?* (or *Block?*, in red) in place of the score while
|
||||
confirming — and while one is confirming the other is hidden so they never overlap. The 🤝
|
||||
goes **disabled** while a request is pending or was declined and **disappears** once you are
|
||||
friends; both controls **disappear and the opponent's name is struck through** once you have
|
||||
blocked them. They update in place the moment the relationship changes and stay correct across
|
||||
reloads. Applying a block (or friend request) takes effect immediately and is confirmed by a
|
||||
live event; a transport failure rolls the control back to its prior state.
|
||||
|
||||
Block globally — switch off incoming chat and/or friend requests — or block an **individual
|
||||
player**. A per-user block is **one-directional and silent**: you stop receiving everything
|
||||
**from** that person — chat, nudges, friend requests and game invitations — and the matchmaker
|
||||
never pairs you with them, while they **notice nothing**. Their messages and nudges still send
|
||||
by the normal rules but never reach you (anything that would show as unread for you is marked
|
||||
read at once), and a friend request or invitation they send you is kept but never surfaces. A
|
||||
block **overrides but does not delete** an existing friendship (so you may block a friend, and
|
||||
they keep seeing you as one); active games are never interrupted — you can finish them, with
|
||||
the blocked opponent's chat composer hidden (only the log remains). Blocking from a game card
|
||||
mirrors the block in **Settings → Friends**; **unblock** and **unfriend** live there only. Per-game chat is for quick reactions: messages are short
|
||||
(up to 60 characters) and may not contain links, email addresses or phone numbers,
|
||||
even disguised. You may send **one message per turn, on your own turn**; once it is sent
|
||||
the field gives way to a short caption until your next turn. Nudge the player whose turn
|
||||
|
||||
+21
-8
@@ -183,14 +183,27 @@ nudge) приходят от бота **этой партии** — по язы
|
||||
тому, с кем вы играли** — он принимает, игнорирует (заявка истекает через тридцать
|
||||
дней, после чего её можно отправить снова) или отклоняет (отказ блокирует ваши
|
||||
повторные заявки, пока он сам не передаст вам код). Отмена своей висящей заявки
|
||||
снимает её; удаление расторгает дружбу. В партии карточка счёта каждого соперника несёт контрол
|
||||
**в друзья 🤝** (при открытой истории ходов), отражающий живое отношение: он подтверждается
|
||||
тапом по затухающей ✅ (карточка показывает *В друзья?* во время подтверждения), становится
|
||||
**неактивным**, пока заявка висит или была отклонена, и **исчезает** после принятия —
|
||||
обновляясь на месте в момент ответа соперника и оставаясь верным после перезагрузки. Глобальная блокировка — отключить входящие
|
||||
чат и/или заявки —
|
||||
и блокировка конкретного игрока (пер-юзер блок скрывает его чат и запрещает заявки
|
||||
и приглашения в игру в обе стороны, а также расторгает уже имеющуюся дружбу). Чат
|
||||
снимает её; удаление расторгает дружбу. В партии карточка счёта каждого соперника (при открытой
|
||||
истории ходов) несёт два контрола: **в друзья 🤝** справа и **блокировка ✖️** слева. Каждый
|
||||
подтверждается тапом по затухающей ✅ — на месте счёта карточка показывает *В друзья?* (или
|
||||
*Блокируем?* красным) во время подтверждения, — и пока подтверждается один, второй скрыт, чтобы
|
||||
они не пересекались. 🤝 становится **неактивным**, пока заявка висит или была отклонена, и
|
||||
**исчезает** после принятия; **оба контрола исчезают, а имя соперника становится зачёркнутым**
|
||||
после блокировки. Они обновляются на месте в момент изменения отношения и остаются верны после
|
||||
перезагрузки. Блокировка (как и заявка в друзья) применяется немедленно и подтверждается живым
|
||||
событием; при сбое транспорта контрол возвращается в прежнее состояние.
|
||||
|
||||
Глобальная блокировка — отключить входящие чат и/или заявки — либо блокировка **конкретного
|
||||
игрока**. Пер-юзер блок **односторонний и незаметный**: вы перестаёте получать от этого человека
|
||||
всё — чат, nudge, заявки в друзья и приглашения в игру, — и матчмейкер никогда не сводит вас с
|
||||
ним, а он **ничего не замечает**. Его сообщения и nudge по-прежнему отправляются по обычным
|
||||
правилам, но не доходят до вас (всё, что было бы для вас непрочитанным, сразу помечается
|
||||
прочитанным), а присланные им заявка или приглашение сохраняются, но никогда не всплывают. Блок
|
||||
**перекрывает, но не удаляет** имеющуюся дружбу (поэтому можно заблокировать друга, а он
|
||||
продолжает видеть вас в друзьях); активные игры не прерываются — их можно доигрывать, при этом у
|
||||
заблокированного соперника «подвал» чата скрыт (остаётся только лог). Блокировка с карточки в
|
||||
партии повторяет блокировку в **Настройках → Друзья**; **разблокировка** и **удаление из друзей**
|
||||
есть только там. Чат
|
||||
партии — для быстрых реакций: сообщения короткие (до 60 символов) и не должны
|
||||
содержать ссылок, email и телефонов, даже завуалированных. В свой ход можно отправить
|
||||
**одно сообщение за ход**; после отправки поле сменяется короткой подписью до следующего
|
||||
|
||||
+10
-6
@@ -131,10 +131,14 @@ except Login uses `Screen`.
|
||||
(badged with unread chat) at the right, icon-only. A **single-word-rule** game (a Russian
|
||||
game with "multiple words per turn" off) centres a **"One word per turn"** label between
|
||||
those two icons, and the status bar shows a small **1️⃣** in the score-preview slot that
|
||||
yields to the live word/score preview while tiles are pending; standard games show neither. Each **opponent**'s card also gains a
|
||||
🤝 **add-friend** control (non-guests; hidden once a friend, disabled once requested) that
|
||||
confirms via the fading-✅ tap, swapping the card's score for "Add friend?" while armed
|
||||
(see Controls); the name and score stay centred — the 🤝 is pinned to the card's edge.
|
||||
yields to the live word/score preview while tiles are pending; standard games show neither. Each **opponent**'s card also gains two
|
||||
edge-pinned controls (non-guests): a 🤝 **add-friend** on the right (hidden once a friend,
|
||||
disabled once requested) and a ✖️ **block** on the left. Each confirms via the fading-✅ tap,
|
||||
swapping the card's score for "Add friend?" — or **"Block?" in the danger colour** — while
|
||||
armed (see Controls); while one is armed the other hides so they never overlap. Once the
|
||||
opponent is **blocked** both controls go and the **name is struck through** (and the comms-hub
|
||||
chat composer is hidden — only the log remains). The name and score stay centred — the controls
|
||||
are pinned to the card's edges.
|
||||
Unread chat is also badged on the score bar itself, so it shows with the history closed.
|
||||
- **Vertical fit & keyboard**: when the game does not fit the viewport, only the
|
||||
board area scrolls vertically (`Screen` `column` mode; the score bar, status, rack and tab
|
||||
@@ -180,8 +184,8 @@ except Login uses `Screen`.
|
||||
tap-to-confirm control. A first tap arms a ~2 s window showing a **fading ✅** (no fade
|
||||
under reduce-motion, but the window still holds); a tap on the ✅ within it confirms,
|
||||
otherwise it reverts. Used by the **Hint** tab (the icon morphs to ✅, no label — replacing
|
||||
the old press-and-hold popover) and the in-game **add-friend 🤝**. The
|
||||
**Drop game** action keeps its `Modal` confirmation (a destructive, less-frequent action).
|
||||
the old press-and-hold popover) and the in-game **add-friend 🤝** and **block ✖️** card
|
||||
controls. The **Drop game** action keeps its `Modal` confirmation (a destructive, less-frequent action).
|
||||
- **MakeMove / Reset**: when ≥1 tile is pending the rack collapses its used slots
|
||||
and shifts left, a **borderless ✅ icon button** (styled like a tab, not a filled accent
|
||||
button) beside the rack commits the move — no popover, and disabled while the pending word
|
||||
|
||||
Reference in New Issue
Block a user