feat(emoji): user-level (personal) custom emoji (LC-482) #486

Merged
longjacksonle merged 5 commits from feat/LC-482-user-custom-emoji into main 2026-06-28 20:56:08 +02:00

LC-482 - User-level (personal) custom emoji

Adds personal custom emoji alongside enclave emoji, reusing the existing upload/serve pipeline and :shortcode: resolver.

Model + schema

Migration 0075_user_custom_emojis.sql rebuilds custom_emojis: enclave_id becomes nullable, a user_id column is added, and a CHECK ((enclave_id IS NOT NULL) <> (user_id IS NOT NULL)) enforces exactly one scope per row. Existing rows copy in as enclave-scoped. Shortcode uniqueness is now per-scope (two partial unique indexes). Keeping one id space means /api/emojis/{id} and the :shortcode: -> <img> render path are unchanged.

Resolution + precedence

refs_for_room_and_user(room_id, user_id) layers the viewer's personal emoji additively under the room set (own enclave > globally-shared). The room/enclave set wins shortcode collisions, so a shared :code: renders identically for everyone; personal emoji only fill in shortcodes the room does not define. Personal emoji are otherwise per-viewer: they resolve only in the owner's own renders, and the serve route authorizes a personal row to its owner (+ site admin) only. This call replaces refs_for_room at the render/pick sites that have a viewer (message list + single-message renders, WS per-recipient renders, reaction bar + picker, emoji autocomplete, DM page, composer preview). Room wiki/description rendering stays enclave-only (shared docs; documented).

Management

A new "Custom emoji" tab in user Settings uploads/lists/deletes personal emoji (POST /settings/emojis, POST /settings/emojis/{id}/delete), mirroring the enclave emoji card. The multipart ingest (stream, size cap, magic-byte sniff, content-address) is extracted into a shared helper used by both the enclave and personal upload handlers. Account deletion already removes a user's emoji via the existing uploaded_by cleanup.

Tests

db_custom_emojis: user-scoped insert + per-user uniqueness + enclave/personal shortcode coexistence, refs_for_room_and_user precedence (enclave wins, other viewers don't see your personal emoji), owner-scoped delete. The existing enclave-emoji + cascade tests were updated for the now-Option enclave_id and still pass. just check, just test, just test-saas all green (i18n parity covers the new en/es keys); Tailwind rebuilt.

Not operator-visible (no env/config/contract change; the migration is additive and self-applying).

🤖 Generated with Claude Code

## LC-482 - User-level (personal) custom emoji Adds personal custom emoji alongside enclave emoji, reusing the existing upload/serve pipeline and `:shortcode:` resolver. ### Model + schema Migration `0075_user_custom_emojis.sql` rebuilds `custom_emojis`: `enclave_id` becomes nullable, a `user_id` column is added, and a `CHECK ((enclave_id IS NOT NULL) <> (user_id IS NOT NULL))` enforces exactly one scope per row. Existing rows copy in as enclave-scoped. Shortcode uniqueness is now per-scope (two partial unique indexes). Keeping one id space means `/api/emojis/{id}` and the `:shortcode:` -> `<img>` render path are unchanged. ### Resolution + precedence `refs_for_room_and_user(room_id, user_id)` layers the viewer's personal emoji additively under the room set (own enclave > globally-shared). The room/enclave set wins shortcode collisions, so a shared `:code:` renders identically for everyone; personal emoji only fill in shortcodes the room does not define. Personal emoji are otherwise per-viewer: they resolve only in the owner's own renders, and the serve route authorizes a personal row to its owner (+ site admin) only. This call replaces `refs_for_room` at the render/pick sites that have a viewer (message list + single-message renders, WS per-recipient renders, reaction bar + picker, emoji autocomplete, DM page, composer preview). Room wiki/description rendering stays enclave-only (shared docs; documented). ### Management A new "Custom emoji" tab in user Settings uploads/lists/deletes personal emoji (`POST /settings/emojis`, `POST /settings/emojis/{id}/delete`), mirroring the enclave emoji card. The multipart ingest (stream, size cap, magic-byte sniff, content-address) is extracted into a shared helper used by both the enclave and personal upload handlers. Account deletion already removes a user's emoji via the existing `uploaded_by` cleanup. ### Tests `db_custom_emojis`: user-scoped insert + per-user uniqueness + enclave/personal shortcode coexistence, `refs_for_room_and_user` precedence (enclave wins, other viewers don't see your personal emoji), owner-scoped delete. The existing enclave-emoji + cascade tests were updated for the now-`Option` `enclave_id` and still pass. `just check`, `just test`, `just test-saas` all green (i18n parity covers the new en/es keys); Tailwind rebuilt. Not operator-visible (no env/config/contract change; the migration is additive and self-applying). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Migration 0075 rebuilds custom_emojis: enclave_id nullable, add user_id, CHECK exactly one scope, per-scope partial unique indexes (single id space, so /api/emojis/{id} is unchanged). db gains insert_for_user / list_for_user / delete_for_user / refs_for_user and refs_for_room_and_user, which layers a viewer's personal emoji additively under the room set (room/enclave wins collisions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the multipart ingest into a shared helper; reuse it for the enclave upload and a new POST /settings/emojis (personal) + /settings/emojis/{id}/delete (owner-scoped). get_emoji authorizes by scope: enclave rows keep the share/membership rule, personal rows are private to their owner (+ site admin). Fix the data-export query for nullable enclave_id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thread the viewer through the emoji-resolution call sites (message list/single renders, WS per-recipient renders, reaction bar + picker, autocomplete, DM page, composer preview) via refs_for_room_and_user, so a user's personal :shortcode: resolves in their own views and is offered in their composer/reaction picker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New settings tab to upload/list/delete personal emoji, mirroring the enclave emoji card. New i18n keys (en+es).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(emoji): user-scoped emoji insert/scope/precedence/delete (LC-482)
All checks were successful
check-secrets / TruffleHog (push) Successful in 3s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Successful in 2m59s
Create release / Create release from merged PR (pull_request) Has been skipped
f8547617cf
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/LC-482-user-custom-emoji 2026-06-28 20:56:08 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!486
No description provided.