Skip to content

feat(emoji): re-add custom emoji upload via emoji selector - #306

Merged
raaymax merged 8 commits into
devfrom
feat/emoji-upload
Jun 19, 2026
Merged

feat(emoji): re-add custom emoji upload via emoji selector#306
raaymax merged 8 commits into
devfrom
feat/emoji-upload

Conversation

@raaymax

@raaymax raaymax commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Summary

The /emoji slash-command was removed during the files-root-entity work (#302) because it consumed the old embedded-attachment shape, leaving no way to add custom emojis. This restores emoji creation the right way — as a workspace-global asset decoupled from channel files (per the v4-scope §6 plan): emojis never become channel File entities and never appear in the channel Files view.

Changes

Backend

  • New emoji:create command (core/emoji/create.ts): normalizes/validates the shortname to :name:, rejects duplicates (EmojiAlreadyExists → 409), creates the emoji row, and broadcasts { type: "emoji" }.
  • New route POST /api/emojis/:shortname (routes/emojis/create.ts): auth-required, streams the raw image body to core.storage.upload (bare storageId, no channel File entity), then dispatches emoji:create. Shortname format validated via the route's params JSON-schema (clean 400); image-only content-type guard.
  • Wired the command into core.ts and the route into the emojis router. Added EMOJI_ALREADY_EXISTS → 409 mapping.

Frontend

  • The previously-disabled ADD EMOJI button in the emoji selector now opens an inline upload form: image picker with live preview (object URL), custom shortname input, inline validation, and equal-sized Cancel/Add buttons.
  • New app.emojis.create() MobX action + client.api.createEmoji().
  • Search now orders categories by their best Fuse match score (enabled includeScore), so an exact shortname match surfaces at the top instead of being buried in fixed category order.

Testing

  • Added route tests (routes/emojis/__tests__/emojis.test.ts): unauthorized, upload→create→list round-trip, duplicate → 409, non-image → 400, invalid shortname → 4xx. 8/8 pass.
  • deno fmt / deno lint clean on all backend changes.
  • Frontend eslint clean on changed files (only pre-existing warnings).
  • Manually verified end-to-end in Storybook (Organisms/EmojiSearch): add a custom emoji, live preview, button sizing, and exact-match-on-top search ordering.

Notes

  • Custom emoji images render through the legacy /api/files/:storageId endpoint, which the files-design doc deliberately keeps for avatars/emojis.
  • Follow-up: the avatar upload command was removed in the same feat(files): files as a channel-scoped root entity #302 work and still needs the same treatment (PUT /api/profile/avatar).

raaymax added 8 commits June 19, 2026 12:47
Restores emoji creation removed with the files-root-entity work, decoupled
from channel files: POST /api/emojis/:shortname uploads the image as a
workspace-global asset (bare storageId, no channel File entity) and creates
the emoji via an emoji:create command. The disabled ADD EMOJI button in the
emoji selector now opens an inline upload form with live preview and a custom
shortname. Search also orders categories by best match score so an exact
match surfaces at the top.
Center the ADD EMOJI button's plus icon with a flex row + gap, and constrain
custom emoji images in the search grid via a higher-specificity .emoji img
rule (22px, object-fit: contain) so uploaded images no longer overflow their
hover slot.
Add PUT /api/emojis/:shortname (emoji:replace command) that swaps the image,
deletes the now-orphaned old blob (reference-counted), and broadcasts. POST
still returns 409 so there is no silent overwrite. In the selector, adding a
shortname that matches an existing custom emoji shows a warning panel and
turns the action into Replace; base unicode names fall through to create.
Also hard-cap custom emoji image size in the grid so they fit their slot.
Pull the emoji add/replace form out of EmojiSearch into its own AddEmojiForm
component (EmojiSearch 466 to 249 lines). Extract shared upload parsing and
params schema into routes/emojis/parse.ts so create and replace routes stop
duplicating it. Replace the non-existent theme.Error/theme.Warning lookups
with explicit color constants, fixing a type error.
The image is streamed to storage before the command validates, so a rejected
create (duplicate, 409) or replace (missing, 404) left the blob orphaned in
storage with nothing referencing it. Route uploads now go through a shared
saveEmojiUpload helper that removes the blob if the command throws. Expose
list() on the storage facade and assert no leak in the 409/404 tests.
The leak fix only needs storage.remove; list() was added solely so the test
could count blobs. Revert that storage-facade addition and instead track the
uploaded storageId and assert it no longer exists after a rejected request.
@raaymax
raaymax merged commit 61fd375 into dev Jun 19, 2026
1 check passed
@raaymax
raaymax deleted the feat/emoji-upload branch June 19, 2026 12:28
@raaymax raaymax mentioned this pull request Jul 24, 2026
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