feat(emoji): re-add custom emoji upload via emoji selector - #306
Merged
Conversation
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.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
/emojislash-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 channelFileentities and never appear in the channel Files view.Changes
Backend
emoji:createcommand (core/emoji/create.ts): normalizes/validates the shortname to:name:, rejects duplicates (EmojiAlreadyExists→ 409), creates the emoji row, and broadcasts{ type: "emoji" }.POST /api/emojis/:shortname(routes/emojis/create.ts): auth-required, streams the raw image body tocore.storage.upload(barestorageId, no channelFileentity), then dispatchesemoji:create. Shortname format validated via the route's params JSON-schema (clean 400); image-only content-type guard.core.tsand the route into theemojisrouter. AddedEMOJI_ALREADY_EXISTS→ 409 mapping.Frontend
app.emojis.create()MobX action +client.api.createEmoji().includeScore), so an exact shortname match surfaces at the top instead of being buried in fixed category order.Testing
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 lintclean on all backend changes.eslintclean on changed files (only pre-existing warnings).Notes
/api/files/:storageIdendpoint, which the files-design doc deliberately keeps for avatars/emojis.PUT /api/profile/avatar).