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:
@@ -0,0 +1,39 @@
|
||||
import type { SpaceEmoji } from '../api/client';
|
||||
|
||||
/**
|
||||
* Troca `:nome:` pela sintaxe de imagem do markdown, para o renderizador
|
||||
* existente desenhar o emoji próprio sem precisar de plugin.
|
||||
*
|
||||
* Trechos de código são preservados: `:nome:` dentro de crase é texto que a
|
||||
* pessoa quis mostrar literalmente, e virar imagem ali seria destruir o que ela
|
||||
* escreveu.
|
||||
*/
|
||||
export function renderCustomEmojis(
|
||||
content: string,
|
||||
lookup: (name: string) => SpaceEmoji | undefined,
|
||||
urlOf: (filename: string) => string,
|
||||
): string {
|
||||
if (!content.includes(':')) return content;
|
||||
|
||||
// Divide preservando blocos cercados (```) e código em linha (`) —
|
||||
// os delimitadores ficam nos pedaços ímpares e passam intactos.
|
||||
const parts = content.split(/(```[\s\S]*?```|`[^`]*`)/g);
|
||||
|
||||
return parts
|
||||
.map((part, i) => {
|
||||
if (i % 2 === 1) return part;
|
||||
return part.replace(/:([a-z0-9_]{2,32}):/gi, (whole, name: string) => {
|
||||
const emoji = lookup(name.toLowerCase());
|
||||
if (!emoji) return whole;
|
||||
// O alt conserva `:nome:` para que copiar a mensagem devolva o texto
|
||||
// original, e é por ele que o renderizador reconhece o emoji.
|
||||
return `})`;
|
||||
});
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
/** O renderizador usa isto para dar tamanho de emoji em vez de imagem normal. */
|
||||
export function isCustomEmojiAlt(alt: string | undefined): boolean {
|
||||
return !!alt && /^:[a-z0-9_]{2,32}:$/i.test(alt);
|
||||
}
|
||||
Reference in New Issue
Block a user