feat(chat): @everyone mention
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s

Rides the existing `<@id>` token: `everyone` already matches the parser's
`[a-zA-Z0-9_-]+`, so the markdown pre-processor, the inline renderer and the
badge pick it up with no second syntax. User ids are snowflakes, so the
literal `everyone` cannot collide with one.

Gated on a new MENTION_EVERYONE permission rather than being open to all —
otherwise a role can never take it away. Granted to @everyone by default: on
a small instance the ping is a convenience, not a weapon, and an owner who
disagrees can deny it and grant it to a role instead.

Two things that would each have made the gate a no-op:

  - DEFAULT_EVERYONE_PERMISSIONS only applies at space creation, so spaces
    that predate the bit would have had every @everyone mention silently
    stripped. A boot-time backfill ORs the bit into @everyone roles only
    (`id = space_id`), leaving other roles alone so a later denial is not
    overruled on the next restart. Verified against a copy of the production
    database: three roles moved by exactly 2^15, non-@everyone roles
    untouched, second pass a no-op.
  - Enforcement covers edit as well as create; without that, posting a plain
    message and editing it is the way around the permission.

A message that lacks the permission is not rejected — losing what someone
wrote over one token is worse than posting it. The token is demoted to the
literal `@everyone` text they typed: the message reads the same and rings
nobody.

Code spans and fences never ping. People paste payloads and snippets into
chat, and a paste should not ring everyone's client. The same rule decides
the row highlight and the notification sound, so the three cannot disagree.

Verified: web build, server typecheck, 504 web tests, 804 server tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 22:10:56 -03:00
co-authored by Claude Opus 5
parent 1ec5d88fa2
commit a41a944742
16 changed files with 364 additions and 29 deletions
+7
View File
@@ -669,6 +669,7 @@ export const en = {
'perm.manageMessages': 'Manage Messages',
'perm.attachFiles': 'Attach Files',
'perm.readHistory': 'Read Message History',
'perm.mentionEveryone': 'Mention @everyone',
'perm.addReactions': 'Add Reactions',
'perm.connect': 'Connect',
'perm.speak': 'Speak',
@@ -1019,6 +1020,12 @@ export const en = {
'mobile.voice.shareScreen': 'Share screen',
'mobile.voice.stopSharing': 'Stop sharing screen',
'mobile.voice.disconnect': 'Disconnect from call',
// @everyone mention
'mention.everyone.label': 'everyone',
'mention.everyone.description': 'Notify everyone in this channel',
'mention.everyone.section': 'Special',
'mention.unknownUser': 'Unknown User',
'mention.members': 'Members',
} as const;
export type TranslationKey = keyof typeof en;