Private
Public Access
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>
This commit is contained in:
1 parent
b26643527d
commit
2e84ca42b7
23 files changed
+1056
-28
No files matched your search
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user