diff --git a/web/css/styles.css b/web/css/styles.css
index 31123b6..dc3edc6 100644
--- a/web/css/styles.css
+++ b/web/css/styles.css
@@ -138,6 +138,7 @@ button.nav > img.icon {
top: 0;
z-index: var(--zHeader);
backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
}
#view header > label {
padding: 15px;
@@ -185,6 +186,18 @@ button.nav > img.icon {
transform: rotate(360deg);
}
}
+.show-new {
+ text-align: center;
+}
+.show-new > button {
+ color: var(--clrText);
+ border: none;
+ padding: 15px;
+ background: transparent;
+ width: 100%;
+ font-size: var(--fsNormal);
+ font-weight: bold;
+}
.userpic { /* TODO remove .userpic and use helper class */
flex-shrink: 1;
}
diff --git a/web/index.html b/web/index.html
index 4dc9900..3e92613 100644
--- a/web/index.html
+++ b/web/index.html
@@ -132,6 +132,10 @@
+
+
+
diff --git a/web/js/event.js b/web/js/event.js
index f03dfa7..d86b6a1 100644
--- a/web/js/event.js
+++ b/web/js/event.js
@@ -81,7 +81,7 @@ function event_is_spam(ev, contacts, pow) {
return ev.pow >= pow
}
-function event_cmp_created(a, b) {
+function event_cmp_created(a={}, b={}) {
if (a.created_at > b.created_at)
return 1;
if (a.created_at < b.created_at)
diff --git a/web/js/model.js b/web/js/model.js
index 8bdf7c1..9c92a28 100644
--- a/web/js/model.js
+++ b/web/js/model.js
@@ -41,7 +41,7 @@ function model_process_event(model, ev) {
if (model_event_has_unknown_ids(model, ev))
schedule_unknown_refetch(model);
- // Refresh timeline
+ // Queue event for rendering
model.invalidated.push(ev.id);
}
@@ -250,5 +250,6 @@ function new_model() {
friend_of_friends: new Set(),
},
invalidated: [], // event ids which should be added/removed
+ elements: {},
};
}
diff --git a/web/js/ui/state.js b/web/js/ui/state.js
index 3648f6e..b26be60 100644
--- a/web/js/ui/state.js
+++ b/web/js/ui/state.js
@@ -42,13 +42,28 @@ function view_timeline_apply_mode(model, mode, opts={}) {
find_node("#newpost").classList.toggle("hide", mode != VM_FRIENDS);
find_node("#timeline").classList.toggle("reverse", mode == VM_THREAD);
- // Show or hide all applicable events related to the mode.
- xs = el.querySelectorAll(".event");
- for (const x of xs) {
- let evid = x.id.substr(2);
- let ev = model.all_events[evid];
- x.classList.toggle("hide",
- !view_mode_contains_event(model, ev, mode, opts));
+ // Remove all
+ // This is faster than removing one by one
+ el.innerHTML = "";
+ // Build DOM fragment and render it
+ let count = 0;
+ const evs = model_events_arr(model)
+ const fragment = new DocumentFragment();
+ for (let i = evs.length - 1; i >= 0 && count < 50000; i--) {
+ const ev = evs[i];
+ if (!view_mode_contains_event(model, ev, mode, opts))
+ continue;
+ let ev_el = model.elements[ev.id];
+ if (!ev_el)
+ continue;
+ fragment.appendChild(ev_el);
+ count++;
+ }
+ if (count > 0) {
+ find_node("#view .loading-events").classList.add("hide");
+ el.append(fragment);
+ view_set_show_count(0);
+ view_timeline_update_timestamps();
}
return mode;
@@ -65,19 +80,15 @@ function view_timeline_update(model) {
pubkey: el.dataset.pubkey,
};
- // for each event not rendered, go through the list and render it marking
- // it as rendered and adding it to the appropriate fragment. fragments are
- // created based on slices in the existing timeline. fragments are started
- // at the previous event
- // const fragments = {};
- // const cache = {};
-
- // Dumb function to insert needed events
- let visible_count = 0;
+ let count = 0;
+ const latest_ev = el.firstChild ?
+ model.all_events[el.firstChild.id.slice(2)] : undefined;
const all = model_events_arr(model);
const left_overs = [];
while (model.invalidated.length > 0) {
var evid = model.invalidated.pop();
+ if (model.elements[evid])
+ continue;
var ev = model.all_events[evid];
if (!event_is_renderable(ev) || model_is_event_deleted(model, evid)) {
let x = find_node("#ev"+evid, el);
@@ -85,46 +96,71 @@ function view_timeline_update(model) {
continue;
}
- // If event is in el already, do nothing or update?
- let ev_el = find_node("#ev"+evid, el);
- if (ev_el) {
- continue;
- } else {
- const html = render_event(model, ev, {});
- // Put it back on the stack to re-render if it's not ready.
- if (html == "") {
- left_overs.push(evid);
- continue;
- }
- const div = document.createElement("div");
- div.innerHTML = html;
- ev_el = div.firstChild;
- if (!view_mode_contains_event(model, ev, mode, opts)) {
- ev_el.classList.add("hide");
- } else {
- visible_count++;
- }
- }
-
- // find prior event element and insert it before that
- let prior_el;
- let prior_idx = arr_bsearch_insert(all, ev, event_cmp_created);
- while (prior_idx >= 0 && !prior_el) {
- prior_el = find_node("#ev"+all[prior_idx].id, el);
- prior_idx--;
- }
- if (prior_el) {
- el.insertBefore(ev_el, prior_el);
- } else if (el.childElementCount == 0) {
- el.appendChild(ev_el);
- } else {
+ const html = render_event(model, ev, {});
+ // Put it back on the stack to re-render if it's not ready.
+ if (html == "") {
left_overs.push(evid);
+ continue;
+ }
+ const div = document.createElement("div");
+ div.innerHTML = html;
+ ev_el = div.firstChild;
+ model.elements[evid] = ev_el;
+
+ // 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)) {
+ count++;
}
}
model.invalidated = model.invalidated.concat(left_overs);
-
- if (visible_count > 0)
+
+ if (count > 0) {
+ view_set_show_count(count, true);
+ }
+}
+
+function view_set_show_count(count, add) {
+ const show_el = find_node("#show-new")
+ const num_el = find_node("#show-new span", show_el);
+ if (add) {
+ count += parseInt(num_el.innerText || 0)
+ }
+ num_el.innerText = count;
+ show_el.classList.toggle("hide", count <= 0);
+}
+
+function view_timeline_show_new(model) {
+ const el = view_get_timeline_el();
+ const mode = el.dataset.mode;
+ const opts = {
+ thread_id: el.dataset.threadId,
+ pubkey: el.dataset.pubkey,
+ };
+ const latest_evid = el.firstChild ? el.firstChild.id.slice(2) : undefined;
+
+ let count = 0;
+ const evs = model_events_arr(model)
+ const fragment = new DocumentFragment();
+ for (let i = evs.length - 1; i >= 0 && count < 500; i--) {
+ const ev = evs[i];
+ if (latest_evid && ev.id == latest_evid) {
+ break;
+ }
+ if (!view_mode_contains_event(model, ev, mode, opts))
+ continue;
+ let ev_el = model.elements[ev.id];
+ if (!ev_el)
+ continue;
+ fragment.appendChild(ev_el);
+ count++;
+ }
+ if (count > 0) {
find_node("#view .loading-events").classList.add("hide");
+ el.prepend(fragment);
+ }
+ view_set_show_count(-count, true);
+ view_timeline_update_timestamps();
}
function view_timeline_update_profiles(model, ev) {
@@ -138,22 +174,19 @@ function view_timeline_update_profiles(model, ev) {
redraw_my_pfp(model);
}
- // Update displayed names
- xs = el.querySelectorAll(`.username[data-pubkey='${pk}']`)
- html = fmt_profile_name(p, fmt_pubkey(pk));
- for (const x of xs) {
- x.innerText = html;
- }
-
- // Update profile pictures
- xs = el.querySelectorAll(`img.pfp[data-pubkey='${pk}']`);
- html = get_picture(pk, p)
- for (const x of xs) {
- x.src = html;
+ const name = fmt_profile_name(p, fmt_pubkey(pk));
+ const pic = get_picture(pk, p)
+ for (const evid in model.elements) {
+ if (model.all_events[evid].pubkey != pk)
+ continue;
+ const el = model.elements[evid];
+ find_node(`.username[data-pubkey='${pk}']`, el).innerText = name;
+ find_node(`img.pfp[data-pubkey='${pk}']`, el).src = pic;
}
}
-function view_timeline_update_timestamps(model) {
+function view_timeline_update_timestamps() {
+ // TODO only update elements that are fresh and are in DOM
const el = view_get_timeline_el();
let xs = el.querySelectorAll(".timestamp");
let now = new Date().getTime();
@@ -166,16 +199,12 @@ function view_timeline_update_timestamps(model) {
function view_timeline_update_reaction(model, ev) {
let el;
const o = event_parse_reaction(ev);
- if (!o) {
+ if (!o)
return;
- }
const ev_id = o.e;
- const root = find_node(`#ev${ev_id}`);
- if (!root) {
- // It's possible the event didn't get rendered yet from the
- // invalidation stack. In which case emojis will get rendered then.
+ const root = model.elements[ev_id];
+ if (!root)
return;
- }
// Update reaction groups
el = find_node(`.reactions`, root);
diff --git a/web/js/ui/util.js b/web/js/ui/util.js
index 1d1e194..dbdf83f 100644
--- a/web/js/ui/util.js
+++ b/web/js/ui/util.js
@@ -115,6 +115,10 @@ function click_toggle_follow_user(el) {
contacts_save(contacts);
}
+function show_new() {
+ view_timeline_show_new(DAMUS);
+}
+
/* click_event opens the thread view from the element's specified element id
* "dataset.eid".
*/