From 51ab5aae2af6a658afa3f14c910371c9bc12dca4 Mon Sep 17 00:00:00 2001 From: Thomas Mathews Date: Tue, 27 Dec 2022 13:57:54 -0800 Subject: [PATCH] Notifications! Plus minor bug fixes (like saving not working after first shot). This needs to address the favicon issue still, but notifications are largely working. --- js/contacts.js | 9 ++++---- js/main.js | 14 ++++++------- js/model.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++---- js/ui/state.js | 40 ++++++++++++++++++++--------------- js/ui/util.js | 26 +++++++++-------------- 5 files changed, 98 insertions(+), 48 deletions(-) diff --git a/js/contacts.js b/js/contacts.js index f2857af..0754f7a 100644 --- a/js/contacts.js +++ b/js/contacts.js @@ -59,7 +59,7 @@ async function contacts_load(model) { } else { db.close(); resolve(); - log_debug("contacts loaded successfully"); + log_debug(`contacts loaded successfully ${model.contacts.friends.size}`); } } cursor.onerror = (ev) => { @@ -75,13 +75,15 @@ async function contacts_load(model) { async function dbcall(fn) { return new Promise((resolve, reject) => { - var open = indexedDB.open("damus", 4); + var open = indexedDB.open("damus", 5); open.onupgradeneeded = (ev) => { const db = ev.target.result; if (!db.objectStoreNames.contains("friends")) db.createObjectStore("friends", {keyPath: "pubkey"}); if (!db.objectStoreNames.contains("events")) db.createObjectStore("events", {keyPath: "id"}); + if (!db.objectStoreNames.contains("settings")) + db.createObjectStore("settings", {keyPath: "pubkey"}); }; open.onsuccess = (ev) => { fn(ev, resolve, reject); @@ -94,7 +96,7 @@ async function dbcall(fn) { async function dbclear() { function _dbclear(ev, resolve, reject) { - const stores = ["friends", "events"]; + const stores = ["friends", "events", "settings"]; const db = ev.target.result; const tx = db.transaction(stores, "readwrite"); tx.oncomplete = (ev) => { @@ -109,7 +111,6 @@ async function dbclear() { }; for (const store of stores) { tx.objectStore(store).clear(); - tx.objectStore(store).clear(); } } return dbcall(_dbclear); diff --git a/js/main.js b/js/main.js index 70da67d..f5b731e 100644 --- a/js/main.js +++ b/js/main.js @@ -76,11 +76,9 @@ async function webapp_init() { init_message_textareas(); view_show_spinner(true); redraw_my_pfp(model); - document.addEventListener('visibilitychange', () => { - update_title(model); - }); - // Load our contacts first + // Load data from storage + await model_load_settings(model); let err; err = await contacts_load(model); if (err) { @@ -131,9 +129,11 @@ function on_timer_invalidations() { function on_timer_save() { setTimeout(() => { - model_save_events(DAMUS); - contacts_save(DAMUS.contacts); - on_timer_invalidations(); + const model = DAMUS; + model_save_events(model); + model_save_settings(model); + contacts_save(model.contacts); + on_timer_save(); }, 10 * 1000); } diff --git a/js/model.js b/js/model.js index da0ffe5..a84d9f1 100644 --- a/js/model.js +++ b/js/model.js @@ -283,6 +283,55 @@ function test_model_events_arr() { } } +async function model_save_settings(model) { + function _settings_save(ev, resolve, reject) { + const db = ev.target.result; + const tx = db.transaction("settings", "readwrite"); + const store = tx.objectStore("settings"); + tx.oncomplete = (ev) => { + db.close(); + resolve(); + log_debug("settings saved"); + }; + tx.onerror = (ev) => { + db.close(); + log_error("failed to save events"); + reject(ev); + }; + store.clear().onsuccess = () => { + store.put({ + pubkey: model.pubkey, + notifications_last_viewed: model.notifications.last_viewed, + }); + }; + } + return dbcall(_settings_save); +} + +async function model_load_settings(model) { + function _settings_load(ev, resolve, reject) { + const db = ev.target.result; + const tx = db.transaction("settings", "readonly"); + const store = tx.objectStore("settings"); + const req = store.get(model.pubkey); + req.onsuccess = (ev) => { + const settings = ev.target.result; + if (settings) { + model.notifications.last_viewed = settings.notifications_last_viewed; + } + db.close(); + resolve(); + log_debug("Successfully loaded events"); + } + req.onerror = (ev) => { + db.close(); + reject(ev); + log_error("Could not load settings."); + }; + } + return dbcall(_settings_load); +} + async function model_save_events(model) { function _events_save(ev, resolve, reject) { const db = ev.target.result; @@ -336,11 +385,12 @@ async function model_load_events(model, fn) { function new_model() { return { all_events: {}, // our master list of all events - done_init: {}, - notifications: 0, + notifications: { + last_viewed: 0, // time since last looking at notifications + count: 0, // the number not seen since last looking + }, max_depth: 2, reactions_to: {}, - chatrooms: {}, unknown_ids: {}, unknown_pks: {}, @@ -348,7 +398,6 @@ function new_model() { deletions: {}, deleted: {}, - last_event_of_kind: {}, pow: 0, // pow difficulty target profiles: {}, // pubkey => profile data profile_events: {}, // pubkey => event id - use with all_events diff --git a/js/ui/state.js b/js/ui/state.js index 0bfbd3d..acced0c 100644 --- a/js/ui/state.js +++ b/js/ui/state.js @@ -28,6 +28,11 @@ function view_timeline_apply_mode(model, mode, opts={}) { view_show_spinner(true); fetch_profile(pubkey, model.pool); } + if (mode == VM_NOTIFICATIONS) { + model.notifications.count = 0; + model.notifications.last_viewed = new_creation_time(); + update_notifications(model); + } el.dataset.mode = mode; switch(mode) { @@ -99,8 +104,8 @@ function view_show_spinner(show=true) { find_node("#view .loading-events").classList.toggle("hide", !show); } -/* view_timeline_update iterates through invalidated event ids and either adds - * or removes them from the timeline. +/* view_timeline_update iterates through invalidated event ids and updates the + * state of the timeline and other factors such as notifications, etc. */ function view_timeline_update(model) { const el = view_get_timeline_el(); @@ -111,6 +116,7 @@ function view_timeline_update(model) { }; let count = 0; + let ncount = 0; const latest_ev = el.firstChild ? model.all_events[el.firstChild.id.slice(2)] : undefined; const all = model_events_arr(model); @@ -140,22 +146,34 @@ function view_timeline_update(model) { continue; } + // Increase notification count if needed + if (event_refs_pubkey(ev, model.pubkey) && + ev.created_at > model.notifications.last_viewed) { + ncount++; + } + // If the new element is newer than the latest & is viewable then // we want to increase the count of how many to add to view - if (event_cmp_created(ev, latest_ev) >= 0 && view_mode_contains_event(model, ev, mode, opts)) { + if (event_cmp_created(ev, latest_ev) >= 0 && + view_mode_contains_event(model, ev, mode, opts)) { count++; } } model.invalidated = model.invalidated.concat(left_overs); + // If there are new things to show on our current view lets do it if (count > 0) { - // If we have things to show and we have initted and we don't have - // anything update the current view if (!latest_ev) { view_timeline_show_new(model); } view_set_show_count(count, true, false); } + // Update notification markers and count + if (ncount > 0) { + log_debug(`new notis ${ncount}`); + model.notifications.count += ncount; + update_notifications(model); + } } function view_set_show_count(count, add=false, hide=false) { @@ -309,18 +327,6 @@ function get_thread_max_depth(damus, view, root_id) { return view.depths[root_id] } -/*function expand_thread(id, reply_id) { - const view = get_current_view() - const root_id = get_thread_root_id(DAMUS, id) - if (!root_id) { - log_debug("could not get root_id for", DAMUS.all_events[id]) - return - } - view.expanded.add(reply_id) - view.depths[root_id] = get_thread_max_depth(DAMUS, view, root_id) + 1 - redraw_events(DAMUS, view) -}*/ - function get_thread_root_id(damus, id) { const ev = damus.all_events[id] if (!ev) { diff --git a/js/ui/util.js b/js/ui/util.js index 57167f5..175c8e2 100644 --- a/js/ui/util.js +++ b/js/ui/util.js @@ -47,7 +47,8 @@ function init_message_textareas() { } // update_notification_markers will find all markers and hide or show them -// based on the passed in state of 'active'. +// based on the passed in state of 'active'. This applies to the navigation +// icons. function update_notification_markers(active) { let els = document.querySelectorAll(".new-notifications") for (const el of els) { @@ -214,8 +215,7 @@ function redraw_my_pfp(model) { el.innerHTML = html; } -function update_favicon(path) -{ +function update_favicon(path) { let link = document.querySelector("link[rel~='icon']"); const head = document.getElementsByTagName('head')[0] @@ -228,21 +228,15 @@ function update_favicon(path) link.href = path; } -// update_title updates the document title & visual indicators based on if the +// update_notifications updates the document title & visual indicators based on if the // number of notifications that are unseen by the user. -function update_title(model) { - // TODO rename update_title to update_notification_state or similar - // TODO only clear notifications once they have seen all targeted events - if (document.visibilityState === 'visible') { - model.notifications = 0 - } - - const num = model.notifications - const has_notes = num !== 0 - document.title = has_notes ? `(${num}) Yo Sup` : "Yo Sup"; +function update_notifications(model) { + const { count } = model.notifications; + const suffix = "Yo Sup"; + document.title = count ? `(${count}) ${suffix}` : suffix; // TODO I broke the favicons. I will fix with notications update - update_favicon(has_notes ? "img/damus_notif.svg" : "img/damus.svg"); - update_notification_markers(has_notes) + //update_favicon(has_notes ? "img/damus_notif.svg" : "img/damus.svg"); + update_notification_markers(count); } async function get_pubkey(use_prompt=true) {