feat: sistema de figurinhas, emoji PT-BR e preloader inteligente

- Stickers: captura automática por fileSha256 (Baileys) no MessageHandler,
  tabela Prisma 'stickers' com deduplicação por [tenantId, sha256],
  API REST /api/stickers (list + favorite toggle), cache IndexedDB permanente
  (sem TTL — conteúdo imutável), StickerPanel.tsx com abas Recentes/Favoritos
- Emoji picker: migrado de emoji-picker-react para @emoji-mart/react com
  locale="pt" (tradução completa: categorias, busca, mensagem de sem resultado)
- Preloader: aparece apenas em hard reload (F5/URL direta) via
  performance.getEntriesByType('navigation').type + sessionStorage flag,
  navegação client-side Next.js pula o overlay instantaneamente
- Avatar cache: Wasabi como storage persistente + IndexedDB client-side,
  avatar_version via MD5(avatarUrl) sem coluna extra no banco,
  rota /api/storage/view/* para servir arquivos Wasabi ao frontend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-05-11 17:48:36 +02:00
parent 2b9b855fc9
commit b97f7f94e4
33 changed files with 1183 additions and 128 deletions
@@ -292,10 +292,11 @@ class UnifiedStorageProviderPlugin {
}
const safeBase = originalName.replace(/[^a-zA-Z0-9._-]/g, '_');
const fileName = `${Date.now()}-${safeBase}`;
// Path no bucket: whatsapp/{tenantId}/{instanceId}/{file} ou {category}/{file}
const relativePath = (category === 'whatsapp' && payload.usuarioSis && payload.sessionJID)
? `whatsapp/${payload.usuarioSis}/${payload.sessionJID}/${fileName}`
: `${category}/${fileName}`;
// Path no bucket: customPath (quando fornecido) > whatsapp/{t}/{i}/{file} > {cat}/{file}
const relativePath = payload.customPath
?? ((category === 'whatsapp' && payload.usuarioSis && payload.sessionJID)
? `whatsapp/${payload.usuarioSis}/${payload.sessionJID}/${fileName}`
: `${category}/${fileName}`);
if (!this.s3 || !this.bucket) {
return this.saveToLocalFallback(relativePath, buffer, originalName, category);
}
@@ -317,6 +318,26 @@ class UnifiedStorageProviderPlugin {
return this.saveToLocalFallback(relativePath, buffer, originalName, category);
}
}
async getBuffer(relativePath) {
// 1. Fallback local
const localPath = path_1.default.join(this.localFallbackDir, relativePath);
if (fs_1.default.existsSync(localPath)) {
return fs_1.default.readFileSync(localPath);
}
// 2. Wasabi
if (!this.s3 || !this.bucket)
return null;
try {
const result = await this.s3.send(new client_s3_1.GetObjectCommand({ Bucket: this.bucket, Key: relativePath }));
if (!result.Body)
return null;
const bytes = await result.Body.transformToByteArray();
return Buffer.from(bytes);
}
catch {
return null;
}
}
/** Salva buffer no diretório de fallback local, criando subpastas se necessário */
saveToLocalFallback(relativePath, buffer, originalName, _category) {
const dest = path_1.default.join(this.localFallbackDir, relativePath);
File diff suppressed because one or more lines are too long