Skip to content

feat(messaging): friend-gated direct messages#8

Merged
danjdewhurst merged 1 commit into
mainfrom
direct-messages
Jun 13, 2026
Merged

feat(messaging): friend-gated direct messages#8
danjdewhurst merged 1 commit into
mainfrom
direct-messages

Conversation

@danjdewhurst

Copy link
Copy Markdown
Contributor

Summary

Implements the Direct Friend Messaging follow-up from FOLLOW_UPS.md.

Design choice (the follow-up asked to define it): persisted friend-to-friend direct messages, not ephemeral room whispers — room chat already covers same-room talk, so DMs add private, cross-room, offline-capable messaging. Privacy: only mutual friends can message each other or read history.

Protocol

  • dm.send client message ({ toUserId, text }) and dm.message server message ({ id, fromUserId, toUserId, text, sentAt }). Text is sanitized + length-bounded by the schema.

Server

  • DirectMessageService gates every send and history read on a mutual-friendship check (FriendStore.areFriends), persists to a new direct_messages table (migration 0009, with a no-self CHECK and conversation indexes).
  • The WS handler publishes dm.message to the recipient's and sender's per-user topic (user:<id>) for realtime delivery to every open tab; each socket subscribes to its user topic on open.
  • Sends are rate-limited via a new dm token bucket. GET /friends/:id/messages returns friend-gated, bounded, oldest-first history.

Client

  • A "Message" action on each friend opens a DirectMessagePanel — history loaded over HTTP, live messages appended over the socket, own vs. incoming alignment, styled to match the app.
  • Game routes dm.message to onDirectMessage and exposes sendDirectMessage.

Test plan

  • bun run lint ✅ · bun run typecheck
  • bun test272 pass / 1 skip · bun run coverage:check91.6% (critical dirs ≥81%)
  • RUN_DB_TESTS=1 (real Postgres) ✅ 277 pass incl. a DM-store integration test (round-trip + bidirectional conversation + the no-self CHECK)
  • Two-user browser test (Playwright): A befriends B → A messages B → A's panel shows it → B opens the conversation and loads the persisted message → A sends again → B receives it live, aligned as incoming. A stranger's dm.send is rejected with NOT_FRIENDS.

🤖 Generated with Claude Code

Implements the Direct Friend Messaging follow-up. Design choice: persisted
friend-to-friend direct messages (not ephemeral room whispers), since room chat
already covers same-room talk and DMs add private, cross-room, offline-capable
messaging.

Protocol:
- New dm.send client message ({ toUserId, text }) and dm.message server message
  ({ id, fromUserId, toUserId, text, sentAt }); text is sanitized and bounded.

Server:
- DirectMessageService gates every send and history read on a mutual-friendship
  check, persists to a new direct_messages table (migration 0009, with a no-self
  CHECK and conversation indexes), and the WS handler publishes dm.message to the
  recipient's and sender's per-user topics (user:<id>) for realtime delivery.
- Each socket subscribes to its user topic on open. Sends are rate-limited via a
  new `dm` token bucket. GET /friends/:id/messages returns friend-gated, bounded,
  oldest-first conversation history.

Client:
- A "Message" action on each friend opens a DirectMessagePanel (history loaded via
  HTTP, live messages appended over the socket, own vs. incoming alignment).
- Game routes dm.message to onDirectMessage and exposes sendDirectMessage.

Tests/verification:
- Protocol, DirectMessageService, Drizzle store, friend areFriends, WS handler,
  router history, and client (DMClient + panel) unit tests; a real-Postgres
  integration test; and a two-browser end-to-end test confirming send, realtime
  delivery, persisted history, friend-gating, and message alignment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danjdewhurst danjdewhurst merged commit 924f806 into main Jun 13, 2026
2 checks passed
@danjdewhurst danjdewhurst deleted the direct-messages branch June 13, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant