import { normalizeId } from "./utils.js"; const DB_NAME = "pandora"; const DB_VERSION = 6; let dbPromise = null; function openDb() { if (dbPromise) return dbPromise; dbPromise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = (e) => { const db = req.result; const tx = req.transaction; if (!db.objectStoreNames.contains("chats")) { const store = db.createObjectStore("chats", { keyPath: "id" }); store.createIndex("timestamp", "timestamp", { unique: false }); } let msgStore; if (!db.objectStoreNames.contains("messages")) { msgStore = db.createObjectStore("messages", { keyPath: "id" }); msgStore.createIndex("from", "from", { unique: false }); msgStore.createIndex("fingerprint", ["from", "timestamp"], { unique: false }); msgStore.createIndex("cidTimestampId", ["chatId", "timestamp", "id"], { unique: false }); } else { msgStore = tx.objectStore("messages"); } if (!msgStore.indexNames.contains("chatId_timestamp")) { msgStore.createIndex("chatId_timestamp", ["chatId", "timestamp"], { unique: false }); } if (db.objectStoreNames.contains("messages")) { msgStore.openCursor().onsuccess = (event) => { const cursor = event.target.result; if (cursor) { const m = cursor.value; const from = normalizeId(m.from); const to = normalizeId(m.to); const chatId = normalizeId(m.chatId) || (m.fromMe ? to : from); if (!m.chatId && chatId) { m.chatId = chatId; cursor.update(m); } cursor.continue(); } }; } if (!db.objectStoreNames.contains("media")) { db.createObjectStore("media", { keyPath: "reqId" }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); return dbPromise; } export async function upsertChats(chats) { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction("chats", "readwrite"); const store = tx.objectStore("chats"); for (const c of chats) { store.put({ id: c.id, name: c.name, lastMessage: c.lastMessage, timestamp: c.timestamp, unreadCount: c.unreadCount ?? 0 }); } tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } export async function loadChatsSorted() { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction("chats", "readonly"); const store = tx.objectStore("chats"); const idx = store.index("timestamp"); const result = []; idx.openCursor(null, "prev").onsuccess = (e) => { const cursor = e.target.result; if (cursor) { result.push(cursor.value); cursor.continue(); } else { resolve(result); } }; tx.onerror = () => reject(tx.error); }); } function mapMessage(m) { const from = normalizeId(m.from); const to = normalizeId(m.to); const chatId = normalizeId(m.chatId) || (m.fromMe ? to : from); return { _data: m._data, id: normalizeId(m.id), timestamp: m.timestamp, body: m.body, from: from, fromMe: m.fromMe, ack: m.ack, hasMedia: m.hasMedia, media: m.media, chatId: chatId, } } export async function upsertMessages(messages) { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction("messages", "readwrite"); const store = tx.objectStore("messages"); for (const m of messages) { store.put(mapMessage(m)); } tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } export async function loadLatestMessages(chatId, limit = 50) { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction("messages", "readonly"); const store = tx.objectStore("messages"); const idx = store.index("chatId_timestamp"); const out = []; const range = IDBKeyRange.bound([chatId, -Infinity], [chatId, Infinity]); idx.openCursor(range, "prev").onsuccess = (e) => { const cursor = e.target.result; if (!cursor) return resolve(out); out.push(cursor.value); if (out.length >= limit) resolve(out); else cursor.continue(); }; tx.onerror = () => reject(tx.error); }); } export async function loadOlderMessages(chatId, oldestTimestamp, oldestId, limit = 50) { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction("messages", "readonly"); const store = tx.objectStore("messages"); const idx = store.index("cidTimestampId"); const out = []; const parsedTimestamp = isNaN(oldestTimestamp) ? oldestTimestamp : Number(oldestTimestamp); const range = IDBKeyRange.bound( [chatId, -Infinity, ""], [chatId, parsedTimestamp, oldestId], false, false ); idx.openCursor(range, "prev").onsuccess = (e) => { const cursor = e.target.result; if (!cursor) return resolve(out); out.push(cursor.value); if (out.length >= limit) resolve(out); else cursor.continue(); }; tx.onerror = () => reject(tx.error); }); } export async function upsertMedia(reqId, blob, filename) { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction("media", "readwrite"); const store = tx.objectStore("media"); store.put({ reqId: reqId, blob: blob, filename: filename }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } export async function loadMedia(reqId) { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction("media", "readonly"); const store = tx.objectStore("media"); const req = store.get(reqId); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); }