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,44 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderCustomEmojis, isCustomEmojiAlt } from './customEmoji';
|
||||
import type { SpaceEmoji } from '../api/client';
|
||||
|
||||
const emoji = (name: string): SpaceEmoji => ({
|
||||
id: '1', spaceId: 's', name, filename: `${name}.png`, uploaderId: null, createdAt: 0,
|
||||
});
|
||||
const lookup = (n: string) => (n === 'trollface' ? emoji('trollface') : undefined);
|
||||
const urlOf = (f: string) => `/api/uploads/${f}`;
|
||||
|
||||
describe('renderCustomEmojis', () => {
|
||||
it('troca um emoji conhecido por imagem', () => {
|
||||
expect(renderCustomEmojis('olha :trollface: isso', lookup, urlOf))
|
||||
.toBe('olha  isso');
|
||||
});
|
||||
|
||||
it('deixa nome desconhecido como texto', () => {
|
||||
expect(renderCustomEmojis('nada :inexistente: aqui', lookup, urlOf))
|
||||
.toBe('nada :inexistente: aqui');
|
||||
});
|
||||
|
||||
it('não mexe em código em linha', () => {
|
||||
// Quem escreveu entre crases quis mostrar o texto literal.
|
||||
expect(renderCustomEmojis('use `:trollface:` assim', lookup, urlOf))
|
||||
.toBe('use `:trollface:` assim');
|
||||
});
|
||||
|
||||
it('não mexe em bloco de código', () => {
|
||||
const src = '```\n:trollface:\n```';
|
||||
expect(renderCustomEmojis(src, lookup, urlOf)).toBe(src);
|
||||
});
|
||||
|
||||
it('devolve o texto intacto quando não há dois-pontos', () => {
|
||||
expect(renderCustomEmojis('sem nada', lookup, urlOf)).toBe('sem nada');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomEmojiAlt', () => {
|
||||
it('reconhece o alt de emoji e ignora imagem comum', () => {
|
||||
expect(isCustomEmojiAlt(':trollface:')).toBe(true);
|
||||
expect(isCustomEmojiAlt('foto de praia')).toBe(false);
|
||||
expect(isCustomEmojiAlt(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user