b97f7f94e4
- 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>
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* useStickerCache — cache de figurinhas em IndexedDB.
|
|
*
|
|
* Chave de cache: sha256 hex do arquivo (conteúdo permanente — nunca muda).
|
|
* Sem TTL: stickers são imutáveis, então o cache é eterno.
|
|
* O único motivo para uma entrada sumir é o usuário limpar dados do navegador.
|
|
*
|
|
* Armazenamento: dataURL base64 do WebP (compatível com <img src>).
|
|
*/
|
|
|
|
const DB_NAME = 'nw-stickers'
|
|
const STORE_NAME = 'blobs'
|
|
const DB_VERSION = 1
|
|
|
|
interface CacheEntry {
|
|
sha256: string
|
|
dataUrl: string
|
|
}
|
|
|
|
let dbPromise: Promise<IDBDatabase> | null = null
|
|
|
|
function openDB(): Promise<IDBDatabase> {
|
|
if (dbPromise) return dbPromise
|
|
dbPromise = new Promise((resolve, reject) => {
|
|
const req = indexedDB.open(DB_NAME, DB_VERSION)
|
|
req.onupgradeneeded = () => {
|
|
const db = req.result
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
db.createObjectStore(STORE_NAME, { keyPath: 'sha256' })
|
|
}
|
|
}
|
|
req.onsuccess = () => resolve(req.result)
|
|
req.onerror = () => { dbPromise = null; reject(req.error) }
|
|
})
|
|
return dbPromise
|
|
}
|
|
|
|
export async function getStickerFromCache(sha256: string): Promise<string | null> {
|
|
if (typeof window === 'undefined') return null
|
|
try {
|
|
const db = await openDB()
|
|
return new Promise((resolve) => {
|
|
const tx = db.transaction(STORE_NAME, 'readonly')
|
|
const req = tx.objectStore(STORE_NAME).get(sha256)
|
|
req.onsuccess = () => resolve((req.result as CacheEntry | undefined)?.dataUrl ?? null)
|
|
req.onerror = () => resolve(null)
|
|
})
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function setStickerInCache(sha256: string, dataUrl: string): Promise<void> {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
const db = await openDB()
|
|
await new Promise<void>((resolve, reject) => {
|
|
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
const entry: CacheEntry = { sha256, dataUrl }
|
|
const req = tx.objectStore(STORE_NAME).put(entry)
|
|
req.onsuccess = () => resolve()
|
|
req.onerror = () => reject(req.error)
|
|
})
|
|
} catch { /* quota exceeded — ignora */ }
|
|
}
|
|
|
|
export async function hasStickerInCache(sha256: string): Promise<boolean> {
|
|
if (typeof window === 'undefined') return false
|
|
try {
|
|
const db = await openDB()
|
|
return new Promise((resolve) => {
|
|
const tx = db.transaction(STORE_NAME, 'readonly')
|
|
const req = tx.objectStore(STORE_NAME).count(sha256)
|
|
req.onsuccess = () => resolve(req.result > 0)
|
|
req.onerror = () => resolve(false)
|
|
})
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|