Files
ds-chat/frontend/src/api/customEmoji.ts
T
ksmithandClaude Sonnet 5 2e84ca42b7 Add custom emoji support (#18)
Site-wide, any user can upload -- usable both as reactions and inline
in message text via :shortcode:, alongside the existing built-in
Unicode picker. A :shortcode: reference is stored/sent as literal
text (same as the built-in shortcode convention) and resolved to an
image at render time, so it degrades to plain text if the emoji is
later deleted.

Backend: new custom_emoji table (shortcode unique, sized to fit
MessageReaction.emoji's existing column alongside its colons), upload/
list/delete endpoints (delete restricted to uploader or site admin).

Frontend: a CustomEmojiProvider context feeds a new "Custom" category
in the emoji picker (inline upload + hover-to-remove), extends the
composer's shortcode autocomplete, and a shared EmojiGlyph resolver
renders custom emoji wherever a value can appear -- message text,
reaction pills, and the picker itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 20:47:49 -06:00

48 lines
1.4 KiB
TypeScript

import { apiFetch, ApiError, NetworkError } from './client'
import type { CustomEmoji } from '../types'
export function listCustomEmoji(): Promise<CustomEmoji[]> {
return apiFetch<CustomEmoji[]>('/api/custom-emoji')
}
export function getCustomEmojiUrl(shortcode: string): string {
return `/api/custom-emoji/${encodeURIComponent(shortcode)}/image`
}
export function deleteCustomEmoji(id: string): Promise<void> {
return apiFetch<void>(`/api/custom-emoji/${id}`, { method: 'DELETE' })
}
// Raw fetch, not apiFetch -- same multipart-boundary reason as
// uploadAvatar/uploadRoomImage (a manually-set Content-Type header would
// omit the boundary the browser generates for FormData).
export async function uploadCustomEmoji(shortcode: string, file: File): Promise<CustomEmoji> {
const formData = new FormData()
formData.append('shortcode', shortcode)
formData.append('file', file)
let response: Response
try {
response = await fetch('/api/custom-emoji', {
method: 'POST',
credentials: 'include',
body: formData,
})
} catch {
throw new NetworkError()
}
if (!response.ok) {
let detail = response.statusText
try {
const body = await response.json()
detail = body.detail ?? detail
} catch {
// response had no JSON body
}
throw new ApiError(response.status, detail)
}
return (await response.json()) as CustomEmoji
}