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