File size: 4,165 Bytes
9853b20
 
 
24b7e75
 
 
 
 
 
 
 
 
 
 
 
 
 
9853b20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24b7e75
9853b20
 
 
 
 
 
 
 
 
 
24b7e75
9853b20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Simple IndexedDB-backed offline store for messages and pending queue.
// All functions are no-ops on server/SSR.

// Use browser crypto.randomUUID when available, fallback to simple UUID v4
function genId() {
  try {
    if (typeof crypto !== 'undefined' && (crypto as any).randomUUID) return (crypto as any).randomUUID()
  } catch (e) {
    // ignore
  }
  // fallback UUID v4
  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)
}