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:
@@ -25,6 +25,7 @@ type stubMatcher struct {
|
||||
openErr error
|
||||
openCalls int
|
||||
lastDeadline time.Time
|
||||
lastExclude []uuid.UUID
|
||||
|
||||
expired []game.OpenGame
|
||||
|
||||
@@ -39,9 +40,10 @@ type stubMatcher struct {
|
||||
createParams game.CreateParams
|
||||
}
|
||||
|
||||
func (s *stubMatcher) OpenOrJoin(_ context.Context, _ uuid.UUID, _ game.CreateParams, deadline time.Time) (game.Game, bool, error) {
|
||||
func (s *stubMatcher) OpenOrJoin(_ context.Context, _ uuid.UUID, _ game.CreateParams, deadline time.Time, exclude []uuid.UUID) (game.Game, bool, error) {
|
||||
s.openCalls++
|
||||
s.lastDeadline = deadline
|
||||
s.lastExclude = exclude
|
||||
return s.openGame, s.openJoined, s.openErr
|
||||
}
|
||||
|
||||
@@ -95,6 +97,33 @@ type capturePub struct{ intents []notify.Intent }
|
||||
|
||||
func (c *capturePub) Publish(intents ...notify.Intent) { c.intents = append(c.intents, intents...) }
|
||||
|
||||
// fakeBlocker is a Blocker returning a canned both-direction block set.
|
||||
type fakeBlocker struct {
|
||||
with []uuid.UUID
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeBlocker) Blocks(context.Context, uuid.UUID, uuid.UUID) (bool, error) { return false, nil }
|
||||
func (f *fakeBlocker) BlockedWith(context.Context, uuid.UUID) ([]uuid.UUID, error) {
|
||||
return f.with, f.err
|
||||
}
|
||||
|
||||
// TestEnqueueExcludesBlockedStarters checks the matchmaker passes the caller's block set
|
||||
// (both directions) to OpenOrJoin as the exclusion list, so auto-match never joins a game
|
||||
// whose waiting player has a block with the caller.
|
||||
func TestEnqueueExcludesBlockedStarters(t *testing.T) {
|
||||
caller, blocked := uuid.New(), uuid.New()
|
||||
m := &stubMatcher{openGame: twoSeatGame(caller, uuid.Nil)}
|
||||
mm := NewMatchmaker(m, &fakeRobots{id: uuid.New()}, time.Minute, time.Minute, zap.NewNop())
|
||||
mm.SetBlocker(&fakeBlocker{with: []uuid.UUID{blocked}})
|
||||
if _, err := mm.Enqueue(context.Background(), caller, engine.VariantEnglish, true); err != nil {
|
||||
t.Fatalf("enqueue: %v", err)
|
||||
}
|
||||
if !slices.Contains(m.lastExclude, blocked) {
|
||||
t.Fatalf("OpenOrJoin exclude = %v, want it to contain the blocked id %s", m.lastExclude, blocked)
|
||||
}
|
||||
}
|
||||
|
||||
// twoSeatGame is a two-player game seating starter at seat 0 and opponent at seat 1
|
||||
// (uuid.Nil for a still-empty opponent seat).
|
||||
func twoSeatGame(starter, opponent uuid.UUID) game.Game {
|
||||
|
||||
Reference in New Issue
Block a user