| |
| |
|
|
| |
| function genId() { |
| try { |
| if (typeof crypto !== 'undefined' && (crypto as any).randomUUID) return (crypto as any).randomUUID() |
| } catch (e) { |
| |
| } |
| |
| return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { |
| const r = (Math.random() * 16) | 0 |
| const v = c === 'x' ? r : (r & 0x3) | 0x8 |
| return v.toString(16) |
| }) |
| } |
|
|
| type OfflineMessage = { |
| id: string |
| room_id: string |
| code_id: string |
| user_id?: string | null |
| content: string |
| image_url?: string | null |
| created_at: string |
| pending?: boolean |
| } |
|
|
| const DB_NAME = 'letschat_offline' |
| const DB_VERSION = 1 |
| const STORE_MESSAGES = 'messages' |
| const STORE_PENDING = 'pending_messages' |
|
|
| function isBrowser() { |
| return typeof window !== 'undefined' && typeof indexedDB !== 'undefined' |
| } |
|
|
| function openDB(): Promise<IDBDatabase> { |
| return new Promise((resolve, reject) => { |
| if (!isBrowser()) { |
| reject(new Error('IndexedDB not available')) |
| return |
| } |
| const req = indexedDB.open(DB_NAME, DB_VERSION) |
| req.onupgradeneeded = () => { |
| const db = req.result |
| if (!db.objectStoreNames.contains(STORE_MESSAGES)) { |
| db.createObjectStore(STORE_MESSAGES, { keyPath: 'id' }) |
| } |
| if (!db.objectStoreNames.contains(STORE_PENDING)) { |
| db.createObjectStore(STORE_PENDING, { keyPath: 'id' }) |
| } |
| } |
| req.onsuccess = () => resolve(req.result) |
| req.onerror = () => reject(req.error || new Error('IndexedDB open error')) |
| }) |
| } |
|
|
| async function put(storeName: string, value: any) { |
| const db = await openDB() |
| await new Promise<void>((resolve, reject) => { |
| const tx = db.transaction(storeName, 'readwrite') |
| tx.objectStore(storeName).put(value) |
| tx.oncomplete = () => resolve() |
| tx.onerror = () => reject(tx.error || new Error('IndexedDB put error')) |
| }) |
| } |
|
|
| async function getAll(storeName: string): Promise<any[]> { |
| const db = await openDB() |
| return new Promise((resolve, reject) => { |
| const tx = db.transaction(storeName, 'readonly') |
| const req = tx.objectStore(storeName).getAll() |
| req.onsuccess = () => resolve(req.result || []) |
| req.onerror = () => reject(req.error || new Error('IndexedDB getAll error')) |
| }) |
| } |
|
|
| async function deleteKeys(storeName: string, ids: string[]) { |
| const db = await openDB() |
| await new Promise<void>((resolve, reject) => { |
| const tx = db.transaction(storeName, 'readwrite') |
| const store = tx.objectStore(storeName) |
| ids.forEach((id) => store.delete(id)) |
| tx.oncomplete = () => resolve() |
| tx.onerror = () => reject(tx.error || new Error('IndexedDB delete error')) |
| }) |
| } |
|
|
| export async function saveLocalMessage(message: Omit<OfflineMessage, 'id' | 'created_at'> & Partial<Pick<OfflineMessage, 'created_at'>>) { |
| if (!isBrowser()) return null |
| const msg: OfflineMessage = { |
| id: genId(), |
| created_at: message.created_at || new Date().toISOString(), |
| ...message |
| } |
| await put(STORE_MESSAGES, msg) |
| return msg |
| } |
|
|
| export async function savePendingMessage(message: Omit<OfflineMessage, 'id' | 'created_at'> & Partial<Pick<OfflineMessage, 'created_at'>>) { |
| if (!isBrowser()) return null |
| const msg: OfflineMessage = { |
| id: genId(), |
| created_at: message.created_at || new Date().toISOString(), |
| pending: true, |
| ...message |
| } |
| await put(STORE_PENDING, msg) |
| await put(STORE_MESSAGES, msg) |
| return msg |
| } |
|
|
| export async function getLocalMessages(roomId: string, limit = 50): Promise<OfflineMessage[]> { |
| if (!isBrowser()) return [] |
| const all = await getAll(STORE_MESSAGES) |
| return all |
| .filter((m) => m.room_id === roomId) |
| .sort((a, b) => a.created_at.localeCompare(b.created_at)) |
| .slice(-limit) |
| } |
|
|
| export async function getPendingMessages(): Promise<OfflineMessage[]> { |
| if (!isBrowser()) return [] |
| return getAll(STORE_PENDING) |
| } |
|
|
| export async function clearPendingMessages(ids: string[]) { |
| if (!isBrowser() || ids.length === 0) return |
| await deleteKeys(STORE_PENDING, ids) |
| } |
|
|