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')}
|
||||
|
||||
@@ -59,6 +59,7 @@ export { PlayTile } from './scrabblefb/play-tile.js';
|
||||
export { Profile } from './scrabblefb/profile.js';
|
||||
export { RedeemCodeRequest } from './scrabblefb/redeem-code-request.js';
|
||||
export { RedeemResult } from './scrabblefb/redeem-result.js';
|
||||
export { RobotBlockRef } from './scrabblefb/robot-block-ref.js';
|
||||
export { SeatView } from './scrabblefb/seat-view.js';
|
||||
export { Session } from './scrabblefb/session.js';
|
||||
export { StateRequest } from './scrabblefb/state-request.js';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { AccountRef } from '../scrabblefb/account-ref.js';
|
||||
import { RobotBlockRef } from '../scrabblefb/robot-block-ref.js';
|
||||
|
||||
|
||||
export class BlockList {
|
||||
@@ -33,8 +34,18 @@ blockedLength():number {
|
||||
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
|
||||
}
|
||||
|
||||
robots(index: number, obj?:RobotBlockRef):RobotBlockRef|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? (obj || new RobotBlockRef()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
|
||||
}
|
||||
|
||||
robotsLength():number {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
|
||||
}
|
||||
|
||||
static startBlockList(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
builder.startObject(2);
|
||||
}
|
||||
|
||||
static addBlocked(builder:flatbuffers.Builder, blockedOffset:flatbuffers.Offset) {
|
||||
@@ -53,14 +64,31 @@ static startBlockedVector(builder:flatbuffers.Builder, numElems:number) {
|
||||
builder.startVector(4, numElems, 4);
|
||||
}
|
||||
|
||||
static addRobots(builder:flatbuffers.Builder, robotsOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, robotsOffset, 0);
|
||||
}
|
||||
|
||||
static createRobotsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
|
||||
builder.startVector(4, data.length, 4);
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
builder.addOffset(data[i]!);
|
||||
}
|
||||
return builder.endVector();
|
||||
}
|
||||
|
||||
static startRobotsVector(builder:flatbuffers.Builder, numElems:number) {
|
||||
builder.startVector(4, numElems, 4);
|
||||
}
|
||||
|
||||
static endBlockList(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createBlockList(builder:flatbuffers.Builder, blockedOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
static createBlockList(builder:flatbuffers.Builder, blockedOffset:flatbuffers.Offset, robotsOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
BlockList.startBlockList(builder);
|
||||
BlockList.addBlocked(builder, blockedOffset);
|
||||
BlockList.addRobots(builder, robotsOffset);
|
||||
return BlockList.endBlockList(builder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
export class RobotBlockRef {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):RobotBlockRef {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsRobotBlockRef(bb:flatbuffers.ByteBuffer, obj?:RobotBlockRef):RobotBlockRef {
|
||||
return (obj || new RobotBlockRef()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsRobotBlockRef(bb:flatbuffers.ByteBuffer, obj?:RobotBlockRef):RobotBlockRef {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new RobotBlockRef()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
id():string|null
|
||||
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
id(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
displayName():string|null
|
||||
displayName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
displayName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
gameId():string|null
|
||||
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
gameId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
seat():number {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 10);
|
||||
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
|
||||
}
|
||||
|
||||
static startRobotBlockRef(builder:flatbuffers.Builder) {
|
||||
builder.startObject(4);
|
||||
}
|
||||
|
||||
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, idOffset, 0);
|
||||
}
|
||||
|
||||
static addDisplayName(builder:flatbuffers.Builder, displayNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, displayNameOffset, 0);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(2, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addSeat(builder:flatbuffers.Builder, seat:number) {
|
||||
builder.addFieldInt32(3, seat, 0);
|
||||
}
|
||||
|
||||
static endRobotBlockRef(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createRobotBlockRef(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, displayNameOffset:flatbuffers.Offset, gameIdOffset:flatbuffers.Offset, seat:number):flatbuffers.Offset {
|
||||
RobotBlockRef.startRobotBlockRef(builder);
|
||||
RobotBlockRef.addId(builder, idOffset);
|
||||
RobotBlockRef.addDisplayName(builder, displayNameOffset);
|
||||
RobotBlockRef.addGameId(builder, gameIdOffset);
|
||||
RobotBlockRef.addSeat(builder, seat);
|
||||
return RobotBlockRef.endRobotBlockRef(builder);
|
||||
}
|
||||
}
|
||||
@@ -27,22 +27,34 @@ accountId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
gameId():string|null
|
||||
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
gameId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startTargetRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
builder.startObject(2);
|
||||
}
|
||||
|
||||
static addAccountId(builder:flatbuffers.Builder, accountIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, accountIdOffset, 0);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static endTargetRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createTargetRequest(builder:flatbuffers.Builder, accountIdOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
static createTargetRequest(builder:flatbuffers.Builder, accountIdOffset:flatbuffers.Offset, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
TargetRequest.startTargetRequest(builder);
|
||||
TargetRequest.addAccountId(builder, accountIdOffset);
|
||||
TargetRequest.addGameId(builder, gameIdOffset);
|
||||
return TargetRequest.endTargetRequest(builder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import type {
|
||||
AccountRef,
|
||||
BlockList,
|
||||
BlockStatus,
|
||||
ChatMessage,
|
||||
EvalResult,
|
||||
@@ -127,8 +128,8 @@ export interface GatewayClient {
|
||||
friendCodeRedeem(code: string): Promise<AccountRef>;
|
||||
|
||||
// --- blocks ---
|
||||
blocksList(): Promise<AccountRef[]>;
|
||||
block(accountId: string): Promise<void>;
|
||||
blocksList(): Promise<BlockList>;
|
||||
block(accountId: string, gameId?: string): Promise<void>;
|
||||
unblock(accountId: string): Promise<void>;
|
||||
|
||||
// --- invitations ---
|
||||
|
||||
+22
-5
@@ -9,6 +9,8 @@ import { indexForLetter, letterForIndex, setAlphabet, type AlphabetEntryWire } f
|
||||
import type { PlacedTile } from './client';
|
||||
import type {
|
||||
AccountRef,
|
||||
BlockList,
|
||||
RobotBlockEntry,
|
||||
Banner,
|
||||
BannerCampaign,
|
||||
BestMove,
|
||||
@@ -576,11 +578,15 @@ export function decodeEvent(kind: string, payload: Uint8Array): PushEvent | null
|
||||
|
||||
// --- social encoders ---
|
||||
|
||||
export function encodeTarget(accountId: string): Uint8Array {
|
||||
export function encodeTarget(accountId: string, gameId?: string): Uint8Array {
|
||||
const b = new Builder(64);
|
||||
const id = b.createString(accountId);
|
||||
// game_id is set only by an in-game block, so a disguised-robot opponent is recorded
|
||||
// per-game; every other target path omits it.
|
||||
const gid = gameId ? b.createString(gameId) : 0;
|
||||
fb.TargetRequest.startTargetRequest(b);
|
||||
fb.TargetRequest.addAccountId(b, id);
|
||||
if (gid) fb.TargetRequest.addGameId(b, gid);
|
||||
return finish(b, fb.TargetRequest.endTargetRequest(b));
|
||||
}
|
||||
|
||||
@@ -722,14 +728,25 @@ export function decodeOutgoingList(buf: Uint8Array): AccountRef[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
export function decodeBlockList(buf: Uint8Array): AccountRef[] {
|
||||
export function decodeBlockList(buf: Uint8Array): BlockList {
|
||||
const l = fb.BlockList.getRootAsBlockList(new ByteBuffer(buf));
|
||||
const out: AccountRef[] = [];
|
||||
const blocked: AccountRef[] = [];
|
||||
for (let i = 0; i < l.blockedLength(); i++) {
|
||||
const r = l.blocked(i);
|
||||
if (r) out.push(decodeAccountRef(r));
|
||||
if (r) blocked.push(decodeAccountRef(r));
|
||||
}
|
||||
return out;
|
||||
const robots: RobotBlockEntry[] = [];
|
||||
for (let i = 0; i < l.robotsLength(); i++) {
|
||||
const r = l.robots(i);
|
||||
if (r)
|
||||
robots.push({
|
||||
id: r.id() ?? '',
|
||||
displayName: r.displayName() ?? '',
|
||||
gameId: r.gameId() ?? '',
|
||||
seat: r.seat(),
|
||||
});
|
||||
}
|
||||
return { blocked, robots };
|
||||
}
|
||||
|
||||
export function decodeFriendCode(buf: Uint8Array): FriendCode {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
import { GatewayError } from '../client';
|
||||
import type {
|
||||
AccountRef,
|
||||
BlockList,
|
||||
BlockStatus,
|
||||
ChatMessage,
|
||||
EvalResult,
|
||||
@@ -555,10 +556,13 @@ export class MockGateway implements GatewayClient {
|
||||
}
|
||||
|
||||
// --- blocks ---
|
||||
async blocksList(): Promise<AccountRef[]> {
|
||||
return this.blocks.map((b) => ({ ...b }));
|
||||
async blocksList(): Promise<BlockList> {
|
||||
// The mock models human blocks only (no disguised robots); robots stays empty.
|
||||
return { blocked: this.blocks.map((b) => ({ ...b })), robots: [] };
|
||||
}
|
||||
async block(accountId: string): Promise<void> {
|
||||
async block(accountId: string, _gameId?: string): Promise<void> {
|
||||
// A block hides the blocked person from the blocker's own friends list (the real
|
||||
// ListFriends filters them out), without deleting the underlying friendship.
|
||||
this.friends = this.friends.filter((f) => f.accountId !== accountId);
|
||||
if (!this.blocks.some((b) => b.accountId === accountId)) {
|
||||
this.blocks.push({ accountId, displayName: this.nameFor(accountId) });
|
||||
|
||||
@@ -205,6 +205,22 @@ export interface AccountRef {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
// RobotBlockEntry is one blocked disguised-robot opponent: a per-game record (not a real
|
||||
// account) carrying the game name the player saw and the game/seat it was blocked in. Its id
|
||||
// is used to unblock it.
|
||||
export interface RobotBlockEntry {
|
||||
id: string;
|
||||
displayName: string;
|
||||
gameId: string;
|
||||
seat: number;
|
||||
}
|
||||
|
||||
// BlockList is the caller's blocked humans plus the per-game disguised-robot blocks.
|
||||
export interface BlockList {
|
||||
blocked: AccountRef[];
|
||||
robots: RobotBlockEntry[];
|
||||
}
|
||||
|
||||
/** A freshly issued one-time friend code (the plaintext is returned once). */
|
||||
export interface FriendCode {
|
||||
code: string;
|
||||
|
||||
@@ -187,8 +187,8 @@ export function createTransport(baseUrl: string): GatewayClient {
|
||||
async blocksList() {
|
||||
return codec.decodeBlockList(await exec('blocks.list', codec.empty()));
|
||||
},
|
||||
async block(accountId) {
|
||||
await exec('blocks.add', codec.encodeTarget(accountId));
|
||||
async block(accountId, gameId) {
|
||||
await exec('blocks.add', codec.encodeTarget(accountId, gameId));
|
||||
},
|
||||
async unblock(accountId) {
|
||||
await exec('blocks.remove', codec.encodeTarget(accountId));
|
||||
|
||||
@@ -8,21 +8,27 @@
|
||||
import { translate } from '../lib/i18n/catalog';
|
||||
import { friendCodeParam, shareLink } from '../lib/deeplink';
|
||||
import { shareTelegramLink } from '../lib/telegram';
|
||||
import type { AccountRef, FriendCode } from '../lib/model';
|
||||
import type { AccountRef, FriendCode, RobotBlockEntry } from '../lib/model';
|
||||
|
||||
let friends = $state<AccountRef[]>([]);
|
||||
let incoming = $state<AccountRef[]>([]);
|
||||
let blocked = $state<AccountRef[]>([]);
|
||||
// Per-game disguised-robot blocks, shown as distinct entries alongside blocked humans.
|
||||
let robotBlocks = $state<RobotBlockEntry[]>([]);
|
||||
let code = $state<FriendCode | null>(null);
|
||||
let redeemInput = $state('');
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
[friends, incoming, blocked] = await Promise.all([
|
||||
const [fl, inc, bl] = await Promise.all([
|
||||
gateway.friendsList(),
|
||||
gateway.friendsIncoming(),
|
||||
gateway.blocksList(),
|
||||
]);
|
||||
friends = fl;
|
||||
incoming = inc;
|
||||
blocked = bl.blocked;
|
||||
robotBlocks = bl.robots;
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
@@ -179,7 +185,7 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if blocked.length}
|
||||
{#if blocked.length || robotBlocks.length}
|
||||
<section>
|
||||
<h3>{t('friends.blockedList')}</h3>
|
||||
{#each blocked as b (b.accountId)}
|
||||
@@ -188,6 +194,12 @@
|
||||
<button class="ghost" onclick={() => unblock(b.accountId)} disabled={!connection.online}>{t('friends.unblock')}</button>
|
||||
</div>
|
||||
{/each}
|
||||
{#each robotBlocks as r (r.id)}
|
||||
<div class="item">
|
||||
<span class="who">{r.displayName}</span>
|
||||
<button class="ghost" onclick={() => unblock(r.id)} disabled={!connection.online}>{t('friends.unblock')}</button>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user