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>
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
import { create } from 'zustand';
|
|
import { api, type SpaceEmoji, type SpaceSticker } from '../api/client';
|
|
|
|
/**
|
|
* Emojis e figurinhas por espaço.
|
|
*
|
|
* Carregados uma vez por espaço e mantidos em memória: o render de mensagem
|
|
* consulta o mapa a cada `:nome:` encontrado, e uma busca por requisição
|
|
* tornaria cada mensagem uma cascata de chamadas.
|
|
*/
|
|
interface ExpressionState {
|
|
emojisBySpace: Map<string, SpaceEmoji[]>;
|
|
stickersBySpace: Map<string, SpaceSticker[]>;
|
|
loaded: Set<string>;
|
|
load: (spaceId: string) => Promise<void>;
|
|
emojiByName: (spaceId: string, name: string) => SpaceEmoji | undefined;
|
|
setEmojis: (spaceId: string, emojis: SpaceEmoji[]) => void;
|
|
setStickers: (spaceId: string, stickers: SpaceSticker[]) => void;
|
|
}
|
|
|
|
export const useExpressionStore = create<ExpressionState>((set, get) => ({
|
|
emojisBySpace: new Map(),
|
|
stickersBySpace: new Map(),
|
|
loaded: new Set(),
|
|
|
|
load: async (spaceId) => {
|
|
if (!spaceId || get().loaded.has(spaceId)) return;
|
|
// Marcado antes da resposta: entrar num espaço dispara vários renders, e
|
|
// sem isso a mesma busca sairia várias vezes em paralelo.
|
|
set((s) => ({ loaded: new Set(s.loaded).add(spaceId) }));
|
|
try {
|
|
const [{ emojis }, { stickers }] = await Promise.all([
|
|
api.expressions.emojis(spaceId),
|
|
api.expressions.stickers(spaceId),
|
|
]);
|
|
set((s) => ({
|
|
emojisBySpace: new Map(s.emojisBySpace).set(spaceId, emojis),
|
|
stickersBySpace: new Map(s.stickersBySpace).set(spaceId, stickers),
|
|
}));
|
|
} catch {
|
|
// Sem emojis próprios a conversa segue: `:nome:` fica como texto.
|
|
set((s) => {
|
|
const loaded = new Set(s.loaded); loaded.delete(spaceId);
|
|
return { loaded };
|
|
});
|
|
}
|
|
},
|
|
|
|
emojiByName: (spaceId, name) =>
|
|
get().emojisBySpace.get(spaceId)?.find((e) => e.name === name),
|
|
|
|
setEmojis: (spaceId, emojis) =>
|
|
set((s) => ({ emojisBySpace: new Map(s.emojisBySpace).set(spaceId, emojis) })),
|
|
|
|
setStickers: (spaceId, stickers) =>
|
|
set((s) => ({ stickersBySpace: new Map(s.stickersBySpace).set(spaceId, stickers) })),
|
|
}));
|