Skip to content

feat(messages): private messages between members - #66

Merged
florianjs merged 8 commits into
mainfrom
feat/private-messages
Oct 8, 2026
Merged

florianjs merged 8 commits into
mainfrom
feat/private-messages

Conversation

@florianjs

Copy link
Copy Markdown
Owner

⚠️ Built on top of #64 (shell) and #65 (dashboard), because it uses the new sidebar. Merge #64, then #65, then this one. It targets main directly; the diff shrinks as the earlier PRs land.

Summary

Members can now talk to each other privately.

  • Conversations:
    • one conversation per pair of members, created on the first message;
    • the Messages page has a conversation list (unread marker, preview, "You:" prefix) and the thread (grouped by day, Enter to send, Shift+Enter for a new line, earlier messages paginated);
    • new messages are polled every 15 s while the page is visible.
  • Starting a conversation: use Send a message on a member's profile, or New message with a username. Usernames are matched case-insensitively.
  • Unread badge: in the sidebar, refreshed every minute and on navigation. Messages is also available in the ⌘K palette.
  • Block a member: they can no longer send you messages, and the thread explains why.
  • Hide a conversation: for yourself only; it comes back if a new message arrives.

Privacy and safety

  • Access: only the two participants can read a conversation. Staff roles grant no access. An unknown or foreign conversation returns 404, without revealing whether it exists.
  • Plain text: messages are interpolated, never rendered as HTML (verified: <b> shows literally).
  • Limits: 1 to 5000 characters, rate limited like other mutations (10 per minute); self-messages are refused.
  • Concurrency: the conversation is created with ON CONFLICT DO NOTHING on a unique pair key, so concurrent first messages share one conversation.

Schema

New tables conversations, conversation_members, private_messages and user_blocks. They are applied by drizzle-kit push at startup, and 0014_private_messages.sql is provided for reference.

Test plan

  • npx vitest run: 165 passing, including the new message rule tests (pair key, body normalization, unread, visibility)
  • End to end on the demo instance:
    • self, unknown member and empty message are refused;
    • send, list, read and the unread count all work;
    • a foreign conversation returns 404;
    • a reply while blocked returns 403;
    • a hidden conversation reappears on a new message
  • In the browser: list, thread, sending with Enter, plain-text rendering
  • Two real accounts messaging each other
  • Mobile layout (list, then thread with a back button)

🤖 Generated with Claude Code

florianjs and others added 3 commits October 8, 2026 16:09
- theme tokens as CSS variables: light, dark, or follow the system, chosen
  per user (cookie, applied during SSR so there is no flash)
- meaningful colors: green for upload/seed, amber for download/leech,
  violet for freeleech; contrast inks keep existing controls legible in
  both themes
- self-hosted IBM Plex Sans and Mono (Google Fonts was blocked by the CSP,
  so the app silently fell back to system fonts)
- app shell: grouped sidebar (browse, community, account, staff), top bar
  with a transfer meter, freeleech pill and account menu (passkey copy,
  points, theme, language)
- command palette (Cmd/Ctrl+K or /): search torrents and jump to pages
- mobile navigation drawer, skip link, visible focus, reduced motion
- dialog overlays stay dark in both themes; fix icons collapsed to zero
  width by an inline class

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- greeting, account strip (ratio with upload/download, seeding and leeching
  counts, bonus points, hit & runs), staff note, new torrents, open bounties
  and latest forum topics, from a single /api/dashboard request
- track per-user seeding/leeching torrents in Redis on announce
- show the admin welcome message, which was editable but never displayed
- transfer meter reads live upload/download (the sealed session only held
  the values from sign-in)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- one conversation per pair of members, unread badge in the sidebar,
  messages page with conversation list and thread, start a conversation
  from a profile or by username
- block a member (they can no longer message you), hide a conversation for
  yourself (it comes back on a new message)
- only participants can read a conversation, staff included; plain text
  rendered as text, 1 to 5000 characters, rate limited
- conversation marked read up to the newest message shown

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 11 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d53f5724-cfcc-489b-8274-26f5cbc5d55c
📥 Commits

Reviewing files that changed from the base of the PR and between 22bd096 and b5e3716.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (51)
  • app/app.vue
  • app/assets/css/main.css
  • app/components/EditTorrentModal.vue
  • app/components/ThemeSwitcher.vue
  • app/components/UploadTorrentModal.vue
  • app/components/UserAvatar.vue
  • app/components/admin/Announcements.vue
  • app/components/admin/Branding.vue
  • app/components/admin/HnR.vue
  • app/components/admin/HomepageContent.vue
  • app/components/admin/Panic.vue
  • app/components/admin/Registration.vue
  • app/components/admin/Reports.vue
  • app/components/admin/TorznabRateLimiting.vue
  • app/components/admin/TorznabUsers.vue
  • app/components/shell/AppSidebar.vue
  • app/components/shell/CommandPalette.vue
  • app/components/shell/TransferMeter.vue
  • app/components/shell/UserMenu.vue
  • app/composables/useTheme.ts
  • app/composables/useUnreadMessages.ts
  • app/layouts/default.vue
  • app/pages/auth/register.vue
  • app/pages/forum/index.vue
  • app/pages/index.vue
  • app/pages/messages.vue
  • app/pages/torrents/[hash].vue
  • app/pages/users/[id].vue
  • i18n/locales/en/app.json
  • i18n/locales/en/messages.json
  • i18n/locales/fr/app.json
  • i18n/locales/fr/messages.json
  • nuxt.config.ts
  • package.json
  • server/api/dashboard.get.ts
  • server/api/messages/[id].delete.ts
  • server/api/messages/[id].get.ts
  • server/api/messages/[id].post.ts
  • server/api/messages/block.post.ts
  • server/api/messages/index.get.ts
  • server/api/messages/index.post.ts
  • server/api/messages/unread.get.ts
  • server/db/migrations/0014_private_messages.sql
  • server/db/schema.ts
  • server/redis/userActivity.ts
  • server/tracker/handlers.ts
  • server/utils/messageRules.ts
  • server/utils/messages.ts
  • tailwind.config.ts
  • tests/message-rules.test.ts
  • tests/theme.test.ts
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

florianjs and others added 5 commits October 8, 2026 17:13
- closed mobile drawer is invisible so its links leave the tab order;
  the sidebar gets a landmark label instead of an action label
- command palette drops late search answers and resets on close, and
  returns focus to the element that opened it
- passkey copy only shows Copied when the clipboard write succeeds
- account switch clears the previous balance and avatar
- theme cookie is validated, unknown values fall back to system
- fixed colors: paper (white) for text on colored fills and uploaded
  logos, scrim for shadows, so they do not flip with the theme

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- user activity tracking errors are logged instead of failing the
  announce (and skipping H&R and completion tracking after it)
- the dashboard falls back to empty swarm counts when stats are
  unavailable instead of returning a 500

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- banned or missing recipients are refused (404) for new conversations
  and replies alike
- blocking an unknown user id returns 404 instead of a 500
- malformed ?before / ?limit return 400 instead of a 500
- hidden conversations are filtered in SQL so they do not use up the
  100-row limit and push older visible ones out of the list
- block and hide failures show an error instead of an unhandled
  rejection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@florianjs

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@florianjs
florianjs merged commit acf0b01 into main Oct 8, 2026
1 check passed
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