From f00f327a3daee731a6cfb105141d3c51dcea3f3b Mon Sep 17 00:00:00 2001 From: Thomas Mathews Date: Thu, 29 Dec 2022 18:01:05 -0800 Subject: [PATCH] Updates Improved recognization of replying to thread and being able to open it. Rewrote profile storing on the model. Additionally fixed issues where the profile was not getting loaded for referenced pubkeys on an event. --- css/styles.css | 31 +--------------------- icon/open-thread-here.svg | 1 + icon/open-thread.svg | 2 +- js/core.js | 41 +++++++++++++++-------------- js/event.js | 19 ++++++++++++++ js/main.js | 2 +- js/model.js | 52 +++++++++++++++++++++++------------- js/ui/render.js | 55 +++++++++++++++++++++++---------------- js/ui/state.js | 29 ++++++++++++--------- js/ui/util.js | 29 +++++++++++---------- js/util.js | 6 ++--- 11 files changed, 145 insertions(+), 122 deletions(-) create mode 100644 icon/open-thread-here.svg diff --git a/css/styles.css b/css/styles.css index 56d0aa0..25172ee 100644 --- a/css/styles.css +++ b/css/styles.css @@ -239,17 +239,6 @@ button.nav > img.icon { z-index: var(--zPFP); object-fit: cover; } -.pfp.deleted { - font-size: 32px; - color: var(--clrBg); - background: var(--clrText); -} -.pfp.deleted > i { - top: 40%; - left: 50%; - position: relative; - transform: translateX(-50%) translateY(-50%); -} .event-content { flex: 1; @@ -260,13 +249,8 @@ button.nav > img.icon { } .event-content > .info button[role="view-event"] { margin-left: 10px; - opacity: 0; - transition: opacity 0.2s linear; } -.event:hover .event-content > .info button[role="view-event"] { - opacity: 0.6; -} -.username { +.username, .thread-id { font-weight: 800; font-size: var(--fsReduced); } @@ -363,19 +347,6 @@ details.cw summary { margin-bottom: 10px; } -/* Thread Expansion */ -.thread-collapsed { - padding: 7px; -} -.thread-summary { - text-align: center; -} -.thread-summary img.icon { - opacity: 0.6; - position: relative; - top: 1px; -} - /* Modal */ .modal { position: fixed; diff --git a/icon/open-thread-here.svg b/icon/open-thread-here.svg new file mode 100644 index 0000000..6ffa7d1 --- /dev/null +++ b/icon/open-thread-here.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon/open-thread.svg b/icon/open-thread.svg index 55dfa1c..c40bb2f 100644 --- a/icon/open-thread.svg +++ b/icon/open-thread.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/js/core.js b/js/core.js index 4d71ae6..a9bd0cd 100644 --- a/js/core.js +++ b/js/core.js @@ -38,23 +38,22 @@ async function sign_id(privkey, id) { } async function broadcast_related_events(ev) { - ev.tags - .reduce((evs, tag) => { - // cap it at something sane - if (evs.length >= 5) - return evs - const ev = get_tag_event(tag) - if (!ev) - return evs + ev.tags.reduce((evs, tag) => { + // cap it at something sane + if (evs.length >= 5) return evs - }, []) - .forEach((ev, i) => { - // so we don't get rate limited - setTimeout(() => { - log_debug("broadcasting related event", ev) - broadcast_event(ev) - }, (i+1)*1200) - }) + const ev = get_tag_event(tag) + if (!ev) + return evs + return evs + }, []) + .forEach((ev, i) => { + // so we don't get rate limited + setTimeout(() => { + log_debug("broadcasting related event", ev) + broadcast_event(ev) + }, (i+1)*1200) + }); } function broadcast_event(ev) { @@ -278,12 +277,16 @@ function gather_reply_tags(pubkey, from) { } function get_tag_event(tag) { + const model = DAMUS; if (tag.length < 2) return null if (tag[0] === "e") - return DAMUS.all_events[tag[1]] - if (tag[0] === "p") - return DAMUS.all_events[DAMUS.profile_events[tag[1]]] + return model.all_events[tag[1]] + if (tag[0] === "p") { + let profile = model_get_profile(model, tag[1]); + if (profile.evid) + return model.all_events[profile.evid]; + } return null } diff --git a/js/event.js b/js/event.js index 864d447..484c963 100644 --- a/js/event.js +++ b/js/event.js @@ -15,6 +15,25 @@ function event_refs_pubkey(ev, pubkey) { return false } +function event_contains_pubkey(ev, pubkey) { + if (ev.pubkey == pubkey) + return true; + for (const tag of ev.tags) { + if (tag.length >= 2 && tag[0] == "p" && tag[1] == pubkey) + return true; + } + return false; +} + +function event_get_pubkeys(ev) { + const keys = [ev.pubkey]; + for (const tag of ev.tags) { + if (tag.length >= 2 && tag[0] == "p") + keys.push(tag[1]); + } + return keys; +} + function event_calculate_pow(ev) { const id_bits = leading_zero_bits(ev.id) for (const tag of ev.tags) { diff --git a/js/main.js b/js/main.js index 2f070be..f0383e1 100644 --- a/js/main.js +++ b/js/main.js @@ -242,7 +242,7 @@ function fetch_profiles(pool, relay, pubkeys) { function fetch_profile_info(pubkey, pool, relay) { const sid = `${SID_META}:${pubkey}`; pool.subscribe(sid, [{ - kinds: [KIND_METADATA, KIND_CONTACT], + kinds: [KIND_METADATA, KIND_CONTACT, KIND_RELAY], authors: [pubkey], limit: 1, }], relay); diff --git a/js/model.js b/js/model.js index 759198d..b0626d8 100644 --- a/js/model.js +++ b/js/model.js @@ -39,9 +39,12 @@ function model_process_event(model, relay, ev) { if (!relay) return; - // Request the profile if we have never seen it - if (!model.profile_events[ev.pubkey]) - model_que_profile(model, relay, ev.pubkey); + // Request new profiles for unseen pubkeys of the event + event_get_pubkeys(ev).forEach((pubkey) => { + if (!model_has_profile(model, pubkey)) { + model_que_profile(model, relay, pubkey); + } + }); // TODO fetch unknown referenced events & pubkeys from this event // TODO notify user of new events aimed at them! @@ -108,22 +111,40 @@ function model_fetch_next_profile(model, relay) { * in the event. */ function model_process_event_metadata(model, ev, update_view) { - const prev_ev = model.all_events[model.profile_events[ev.pubkey]] - if (prev_ev && prev_ev.created_at > ev.created_at) - return - model.profile_events[ev.pubkey] = ev.id - model.profiles[ev.pubkey] = safe_parse_json(ev.content, "profile contents") - if (update_view) - view_timeline_update_profiles(model, ev); - + const profile = model_get_profile(model, ev.pubkey); + const evs = model.all_events; + if (profile.evid && + evs[ev.id].created_at < evs[profile.evid].created_at) + return; + profile.evid = ev.id; + profile.data = safe_parse_json(ev.content, "profile contents"); + if (update_view) + view_timeline_update_profiles(model, profile.pubkey); // If it's my pubkey let's redraw my pfp that is not located in the view // This has to happen regardless of update_view because of the it's not // related to events - if (ev.pubkey == model.pubkey) { + if (profile.pubkey == model.pubkey) { redraw_my_pfp(model); } } +function model_has_profile(model, pk) { + return !!model_get_profile(model, pk).evid; +} + +function model_get_profile(model, pubkey) { + if (model.profiles.has(pubkey)) { + return model.profiles.get(pubkey); + } + model.profiles.set(pubkey, { + pubkey: pubkey, + evid: "", + relays: [], + data: {}, + }); + return model.profiles.get(pubkey); +} + function model_process_event_following(model, ev, update_view) { contacts_process_event(model.contacts, model.pubkey, ev) // TODO support loading relays that are stored on the initial relay @@ -241,10 +262,6 @@ function model_is_event_deleted(model, evid) { return false } -function model_has_profile(model, pk) { - return pk in model.profiles -} - function model_has_event(model, evid) { return evid in model.all_events } @@ -399,8 +416,7 @@ function new_model() { deletions: {}, deleted: {}, pow: 0, // pow difficulty target - profiles: {}, // pubkey => profile data - profile_events: {}, // pubkey => event id - use with all_events + profiles: new Map(), // pubkey => profile data contacts: { event: null, friends: new Set(), diff --git a/js/ui/render.js b/js/ui/render.js index 6805788..4a80e8c 100644 --- a/js/ui/render.js +++ b/js/ui/render.js @@ -5,17 +5,23 @@ function render_replying_to(model, ev) { if (!(ev.refs && ev.refs.reply)) return ""; - if (ev.kind === KIND_CHATROOM) - return render_replying_to_chat(model, ev); let pubkeys = ev.refs.pubkeys || [] if (pubkeys.length === 0 && ev.refs.reply) { const replying_to = model.all_events[ev.refs.reply] - if (!replying_to) - return html`
reply to ${ev.refs.reply}
`; - pubkeys = [replying_to.pubkey] + // If there is no profile being replied to, it is simply a reply to an + // event itself, thus render it differently. + if (!replying_to) { + return html` + replying in thread + + ${fmt_pubkey(ev.refs.reply)}`; + } else { + pubkeys = [replying_to.pubkey]; + } } const names = pubkeys.map((pk) => { - return render_mentioned_name(pk, model.profiles[pk]); + return render_name(pk, model_get_profile(model, pk).data); }).join(", ") return ` @@ -24,19 +30,19 @@ function render_replying_to(model, ev) { ` } -function render_share(damus, ev, opts) { - const shared_ev = damus.all_events[ev.refs && ev.refs.root] +function render_share(model, ev, opts) { + const shared_ev = model.all_events[ev.refs && ev.refs.root] // If the shared event hasn't been resolved or leads to a circular event // kind we will skip out on it. if (!shared_ev || shared_ev.kind == KIND_SHARE) return ""; opts.shared = { pubkey: ev.pubkey, - profile: damus.profiles[ev.pubkey], + profile: model_get_profile(model, ev.pubkey), share_time: ev.created_at, share_evid: ev.id, } - return render_event(damus, shared_ev, opts) + return render_event(model, shared_ev, opts) } function render_shared_by(ev, opts) { @@ -52,22 +58,18 @@ function render_event(model, ev, opts={}) { return render_share(model, ev, opts); } - const thread_root = (ev.refs && ev.refs.root) || ev.id; - const profile = model.profiles[ev.pubkey]; + const profile = model_get_profile(model, ev.pubkey); const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000) const border_bottom = opts.is_composing ? "" : "bottom-border"; let thread_btn = ""; return html`
- $${render_pfp(ev.pubkey, profile)} + $${render_pfp(ev.pubkey, profile.data)}
- $${render_name(ev.pubkey, profile)} + $${render_name(ev.pubkey, profile.data)} ${delta} -
$${render_event_body(model, ev, opts)} @@ -77,15 +79,15 @@ function render_event(model, ev, opts={}) { } function render_event_nointeract(model, ev, opts={}) { - const profile = model.profiles[ev.pubkey]; + const profile = model_get_profiles(model, ev.pubkey); const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000) return html`
- $${render_pfp(ev.pubkey, profile)} + $${render_pfp(ev.pubkey, profile.data)}
- $${render_name(ev.pubkey, profile)} + $${render_name(ev.pubkey, profile.data)} ${delta}
@@ -132,7 +134,7 @@ function render_reaction_group(model, emoji, reactions, reacting_to) { break; } const pubkey = reactions[k].pubkey; - str += render_pfp(pubkey, model.profiles[pubkey], {noclick:true}); + str += render_pfp(pubkey, model_get_profile(model, pubkey).data, {noclick:true}); } let onclick = render_react_onclick(model.pubkey, reacting_to.id, emoji, reactions); @@ -149,6 +151,7 @@ function render_action_bar(model, ev, opts={}) { const { pubkey } = model; let { can_delete, shared } = opts; // TODO rewrite all of the toggle heart code. It's mine & I hate it. + const thread_root = (ev.refs && ev.refs.root) || ev.id; const reaction = model_get_reacts_to(model, pubkey, ev.id, R_HEART); const liked = !!reaction; const reaction_id = reaction ? reaction.id : ""; @@ -181,7 +184,15 @@ function render_action_bar(model, ev, opts={}) { ` } - return str + "
"; + return str + ` + +
`; } function render_reactions_inner(model, ev) { diff --git a/js/ui/state.js b/js/ui/state.js index 63f9f1d..fca710b 100644 --- a/js/ui/state.js +++ b/js/ui/state.js @@ -234,26 +234,29 @@ function view_render_event(model, ev, force=false) { return el; } -function view_timeline_update_profiles(model, ev) { +function view_timeline_update_profiles(model, pubkey) { let xs, html; const el = view_get_timeline_el(); - const pk = ev.pubkey; - const p = model.profiles[pk]; - const name = fmt_profile_name(p, fmt_pubkey(pk)); - const pic = get_picture(pk, p) + const p = model_get_profile(model, pubkey); + const name = fmt_profile_name(p.data, fmt_pubkey(pubkey)); + const pic = get_picture(pubkey, p.data) for (const evid in model.elements) { - if (model.all_events[evid].pubkey != pk) + if (!event_contains_pubkey(model.all_events[evid], pubkey)) continue; const el = model.elements[evid]; - find_node(`.username[data-pubkey='${pk}']`, el).innerText = name; - // TODO Sometimes this fails and I don't know why - let img = find_node(`img.pfp[data-pubkey='${pk}']`, el); - if (img) - img.src = pic; + let xs; + xs = find_nodes(`.username[data-pubkey='${pubkey}']`, el) + xs.forEach((el)=> { + el.innerText = name; + }); + xs = find_nodes(`img[data-pubkey='${pubkey}']`, el) + xs.forEach((el)=> { + el.src = pic; + }); } // Update the profile view if it's active - if (el.dataset.mode == VM_USER && el.dataset.pubkey == pk) { - view_update_profile(model, pk); + if (el.dataset.mode == VM_USER && el.dataset.pubkey == pubkey) { + view_update_profile(model, pubkey); } } diff --git a/js/ui/util.js b/js/ui/util.js index 7948b81..f9ce014 100644 --- a/js/ui/util.js +++ b/js/ui/util.js @@ -62,7 +62,8 @@ function update_notification_markers(active) { */ function show_profile(pk) { switch_view("profile"); - const profile = DAMUS.profiles[pk]; + const model = DAMUS; + const profile = model_get_profile(model, pk).data; const el = find_node("#profile-view"); // TODO show loading indicator then render @@ -85,7 +86,7 @@ function show_profile(pk) { btn_follow.dataset.pk = pk; // TODO check follow status btn_follow.innerText = 1 == 1 ? "Follow" : "Unfollow"; - btn_follow.classList.toggle("hide", pk == DAMUS.pubkey); + btn_follow.classList.toggle("hide", pk == model.pubkey); } /* newlines_to_br takes a string and converts all newlines to HTML 'br' tags. @@ -215,7 +216,7 @@ function reply_all(evid) { } function redraw_my_pfp(model) { - const p = model.profiles[model.pubkey] + const p = model_get_profile(model, model.pubkey).data; const html = render_pfp(model.pubkey, p || {}); const el = document.querySelector(".my-userpic"); el.innerHTML = html; @@ -306,23 +307,23 @@ function close_modal(el) { } function view_update_profile(model, pubkey) { - const profile = model.profiles[pubkey] || {}; + const profile = model_get_profile(pubkey); const el = find_node("[role='profile-info']"); - const name = fmt_profile_name(profile, fmt_pubkey(pubkey)); + const name = fmt_profile_name(profile.data, fmt_pubkey(pubkey)); find_node("#view header > label").innerText = name; - find_node("[role='profile-image']", el).src = get_picture(pubkey, profile); + find_node("[role='profile-image']", el).src = get_picture(pubkey, profile.data); find_nodes("[role='profile-name']", el).forEach(el => { el.innerText = name; }); const el_nip5 = find_node("[role='profile-nip5']", el) - el_nip5.innerText = profile.nip05; - el_nip5.classList.toggle("hide", !profile.nip05); + el_nip5.innerText = profile.data.nip05; + el_nip5.classList.toggle("hide", !profile.data.nip05); const el_desc = find_node("[role='profile-desc']", el) - el_desc.innerHTML = newlines_to_br(linkify(profile.about)); - el_desc.classList.toggle("hide", !profile.about); + el_desc.innerHTML = newlines_to_br(linkify(profile.data.about)); + el_desc.classList.toggle("hide", !profile.data.about); find_node("button[role='copy-pk']", el).dataset.pk = pubkey; find_node("button[role='edit-profile']", el) @@ -331,18 +332,18 @@ function view_update_profile(model, pubkey) { const btn_follow = find_node("button[role='follow-user']", el) btn_follow.dataset.pk = pubkey; // TODO check follow status - btn_follow.innerText = contact_is_friend(DAMUS.contacts, pubkey) ? "Unfollow" : "Follow"; - btn_follow.classList.toggle("hide", pubkey == DAMUS.pubkey); + btn_follow.innerText = contact_is_friend(model.contacts, pubkey) ? "Unfollow" : "Follow"; + btn_follow.classList.toggle("hide", pubkey == model.pubkey); } const PROFILE_FIELDS = ['name', 'picture', 'nip05', 'about']; function show_profile_editor() { - const p = DAMUS.profiles[DAMUS.pubkey]; + const p = model_get_profile(DAMUS, DAMUS.pubkey); const el = find_node("#profile-editor"); el.classList.remove("closed"); for (const key of PROFILE_FIELDS) { - find_node(`[name='${key}']`, el).value = p[key]; + find_node(`[name='${key}']`, el).value = p.data[key]; } } diff --git a/js/util.js b/js/util.js index d85d29d..f3033af 100644 --- a/js/util.js +++ b/js/util.js @@ -103,16 +103,14 @@ function get_since_time(created_at) { * parent element to search on. */ function find_node(selector, parentEl) { - const el = parentEl ? parentEl : document; - return el.querySelector(selector) + return (parentEl || document).querySelector(selector) } /* find_nodes is a short name for document.querySelectorAll, it also takes in a * parent element to search on. */ function find_nodes(selector, parentEl) { - const el = parentEl ? parentEl : document; - return el.querySelectorAll(selector) + return (parentEl || document).querySelectorAll(selector); } /* uuidv4 returns a new uuid v4