feat(expressions): custom emojis and stickers per space

Emojis and stickers get separate tables on purpose: an emoji sits inside a
sentence and a sticker is the whole message, so their sizes, limits and render
paths differ.

Emoji names are unique per space and restricted to letters, digits and
underscore — :name: has to resolve to one image, and a space or colon inside
the name would make the reference impossible to delimit. Sticker names are
labels picked from a grid, so they accept anything.

Rendering reuses the existing markdown pipeline by rewriting :name: into image
syntax, which needs no plugin. Code spans and fences are left alone: text
someone wrapped in backticks was meant to be shown literally, and turning it
into an image would destroy what they wrote.

Messages carry stickerId with ON DELETE set null — removing a sticker from the
space must not delete the history of everyone who used it. The server refuses a
sticker from another space, which would otherwise leak images between unrelated
servers.

Expressions load once per space and stay in memory: the message renderer
consults the map for every :name: it finds, and a request per lookup would turn
each message into a cascade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 17:57:32 -03:00
co-authored by Claude Opus 5
parent 50a8f12c77
commit 7d003021d1
15 changed files with 5006 additions and 8 deletions
+4
View File
@@ -215,6 +215,8 @@ export interface Message {
/** Epoch millis when pinned; null when not pinned. */
pinnedAt?: number | null;
pinnedBy?: string | null;
/** Figurinha enviada como mensagem; o conteúdo fica vazio nesse caso. */
stickerId?: string | null;
}
export interface MessageWithUser extends Message {
@@ -599,6 +601,8 @@ export interface CreateMessageRequest {
content: string;
attachments?: string[];
replyToId?: string;
/** Envio de figurinha; nesse caso `content` fica vazio. */
stickerId?: string;
}
export interface UpdateMessageRequest {