Skip to content

Commit 21a78f3

Browse files
authored
IndexedDB for storing messages locally (#6)
1 parent de2c574 commit 21a78f3

3 files changed

Lines changed: 85 additions & 15 deletions

File tree

app/main.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Brief: Main entry point for the app.
44

55
import * as utils from './utils.js';
66

7+
const idb = new utils.idb();
78
const checkImgURL = 'https://img.icons8.com/color/30/approval--v1.png';
89
const crossImgURL = 'https://img.icons8.com/emoji/30/cross-mark-emoji.png';
910
let myWorker = null;
@@ -45,26 +46,31 @@ function updateViewCount (type, increment=1) {
4546
cache.setItem(id, viewCount);
4647
}
4748

48-
async function inbox (dataArray) {
49+
async function inbox (dataArray, fresh=true) {
4950
const container = document.querySelector('#inbox section');
5051
const inboxUnread = document.getElementById('unread');
5152

5253
// Loop over all messages
5354
for (const el of dataArray) {
55+
if (!el) continue
5456
const dataID = el.id;
5557
if (messagesReceived.includes(dataID)) continue;
58+
5659
messagesReceived.push(dataID);
60+
idb.set(dataID, structuredClone(el)); // Do this before mutating el or el.data
61+
5762
const data = el.data;
5863

59-
const origin = data.FormID ?? 'NA';
64+
const origin = data.FormID ?? 'Undefined';
65+
delete data.FormID;
6066

6167
if (origin.startsWith('_view_')) {
6268
updateViewCount(origin.substring('_view_'.length));
6369
continue;
6470
}
6571

6672
const chatID = data.ChatID;
67-
if (chatID) delete data.ChatID;
73+
delete data.ChatID;
6874

6975
if ('geolocation' in el && ! ('Location' in data)) data.Location = el.geolocation;
7076

@@ -128,13 +134,13 @@ async function inbox (dataArray) {
128134
categoryUnread.toggleAttribute('hidden', true);
129135
} else {
130136
/* the element was toggled closed */
131-
const rowList = tableBody.getElementsByTagName('tr');
132137
// Unaccentuate old messages
133-
for (let i = 1; i <= rowList.length; i++) {
134-
// Looping from bottom [rowList.length - i] to avoid unaccentuating new incoming messages
135-
// rowList.length is live, hence not assigned to const
136-
rowList[rowList.length - i].classList.remove('table-primary');
137-
}
138+
// querySelectorAll returns a live nodelist which may change whenever another event handler updates document
139+
// Hence using a shallow copy (Array.from) to isolate
140+
Array.from(tableBody.querySelectorAll('tr.table-primary'))
141+
.forEach((el) => {
142+
el.classList.remove('table-primary');
143+
});
138144
}
139145
});
140146
}
@@ -165,16 +171,17 @@ async function inbox (dataArray) {
165171
document.getElementById(category).prepend(row);
166172

167173
// Accentuate row as new
168-
row.className = 'table-primary';
174+
if (fresh) row.className = 'table-primary';
169175

170176
// Update unread message count
171-
if (!row.checkVisibility()) {
177+
if (fresh && !row.checkVisibility()) {
172178
inboxUnread.innerText = parseInt(inboxUnread.innerText) + 1;
173179
const categoryUnread = document.getElementById(`${category}Unread`);
174180
categoryUnread.innerText = parseInt(categoryUnread.innerText) + 1;
175181
categoryUnread.toggleAttribute('hidden', false);
176182
}
177183
}
184+
idb.flush(); // Store messages to indexedDB, against id
178185
}
179186

180187
window.reply = async function reply (chatID) {
@@ -227,6 +234,15 @@ window.sync = function sync () {
227234
if (myWorker) myWorker.postMessage({ cmd: 'syncNow' });
228235
};
229236

237+
window.clearInbox = function clearInbox (force = false) {
238+
if ( !(force || confirm('Are you sure you want to delete all inboxed messages?')) ) return;
239+
idb.clear();
240+
document.querySelectorAll('#inbox section details')
241+
.forEach((el) => {
242+
el.remove();
243+
});
244+
}
245+
230246
function updateSyncStatusBadge () {
231247
const badge = document.getElementById('serverStatus');
232248
if (myWorker) {
@@ -321,8 +337,6 @@ window.startWorker = function startWorker () {
321337

322338
logThis('Started sync');
323339
updateSyncStatusBadge();
324-
325-
renderForms();
326340
};
327341

328342
window.stopWorker = function stopWorker () {
@@ -347,17 +361,20 @@ window.toggleWorker = function toggleWorker () {
347361
};
348362

349363
window.signout = function signout () {
364+
if (!confirm('Are you sure you want to log out? This will delete all your data from this device.')) return;
350365
stopWorker();
351366
OneSignalDeferred.push(async function(OneSignal) {
352367
await OneSignal.logout();
353368
});
354369
localStorage.clear();
355370
sessionStorage.clear();
356371
cache = null;
372+
clearInbox(true);
357373
main();
358374
};
359375

360376
window.signIn = async function signIn (callerForm) {
377+
idb.clear(); // Just in case previous logout didnt clear IndexedDB completely
361378
const data = new FormData(callerForm);
362379
const appKey = data.get('appKey');
363380
try {
@@ -481,6 +498,9 @@ function main() {
481498
OneSignalLogin();
482499
if (!pageIsRefreshed) spaGoTo('inbox'); // go to inbox on fresh load
483500
startWorker();
501+
renderForms();
502+
// Load earlier messages from indexedDB, sorted chronologically
503+
idb.vals((el1, el2) => el1.time - el2.time).then((dataArray) => inbox(dataArray, false));
484504
} else {
485505
spaShow('login');
486506
}

app/utils.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,55 @@ Brief: Helper utilities.
33
*/
44

55
import securelayEndpoint, * as securelay from 'https://cdn.jsdelivr.net/gh/securelay/api@v0.0.4/script.js';
6+
import {
7+
set as idbSet,
8+
setMany as idbSetMany,
9+
values as idbValues,
10+
del as idbDel,
11+
delMany as idbDelMany,
12+
clear as idbClear
13+
} from 'https://cdn.jsdelivr.net/npm/idb-keyval@6/+esm';
14+
15+
export class idb {
16+
#staged = {};
17+
18+
constructor () {
19+
this.#clearStage();
20+
}
21+
22+
#clearStage () {
23+
this.#staged = { set: [], del: [] };
24+
}
25+
26+
async set (key, val, flush = false) {
27+
if (flush) return idbSet(key, val);
28+
this.#staged.set.push([key, val]);
29+
}
30+
31+
async flush () {
32+
const ret = Promise.all([
33+
idbSetMany(this.#staged.set),
34+
idbDelMany(this.#staged.del)
35+
])
36+
this.#clearStage();
37+
return ret;
38+
}
39+
40+
async vals (sortFunc) {
41+
const array = await idbValues();
42+
if (sortFunc) return array.sort(sortFunc);
43+
return array;
44+
}
45+
46+
async del (key, flush = false) {
47+
if (flush) return idbDel(key);
48+
this.#staged.del.push(key);
49+
}
50+
51+
async clear () {
52+
return idbClear();
53+
}
54+
}
655

756
/*
857
Brief: Returns the first block of hex chars from a v4 UUID as a unique string

index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,9 @@ <h3>Admin</h3>
296296
<h3>Inbox</h3>
297297
<div class="alert alert-success">
298298
<div class="d-inline-flex w-100 justify-content-between">
299-
<p>This is your storageless Inbox. All data is lost once you close or even reload this window.</p>
300-
<button onClick="sync();">Sync Now</button>
299+
<p class="me-auto"><strong>Note:</strong> All data is lost once you logout.</p>
300+
<button type="button" class="btn btn-danger me-2" onClick="clearInbox();">Clear inbox</button>
301+
<button type="button" class="btn btn-primary" onClick="sync();">Sync Now</button>
301302
</div>
302303
</div>
303304
<p class="alert alert-info"><strong>Protip:</strong> To export to <a href="https://www.microsoft.com/en/microsoft-365/excel">MS Excel</a>, copy the table below, followed by <code>Paste Special &gt; Text</code> in Excel. To export to <a href="https://docs.google.com/spreadsheets/create">Google Sheets</a>, use <code>Paste Special &gt; Values only (Ctrl+Shift+V)</code> instead.</p>

0 commit comments

Comments
 (0)