web: bunch of fixes

Following works, but saving to storage does not. I need to
implement indexddb API to save event data.
This commit is contained in:
Thomas Mathews 2022-12-18 21:26:34 -08:00
parent d02992c7e6
commit 4db7250ccc
11 changed files with 115 additions and 227 deletions

View file

@ -1,89 +1 @@
function load_events(damus) {
if (!('event_cache' in localStorage))
return {}
const cached = JSON.parse(localStorage.getItem('event_cache'))
return cached.reduce((obj, ev) => {
obj[ev.id] = ev
process_event(damus, ev)
return obj
}, {})
}
function load_cache(damus) {
damus.all_events = load_events(damus)
load_timelines(damus)
}
function save_cache(damus) {
save_events(damus)
save_timelines(damus)
}
function save_events(damus)
{
const keys = Object.keys(damus.all_events)
const MAX_KINDS = {
1: 2000,
0: 2000,
6: 100,
4: 100,
5: 100,
7: 100,
}
let counts = {}
let cached = keys.map((key) => {
const ev = damus.all_events[key]
const {sig, pubkey, content, tags, kind, created_at, id} = ev
return {sig, pubkey, content, tags, kind, created_at, id}
})
cached.sort((a,b) => b.created_at - a.created_at)
cached = cached.reduce((cs, ev) => {
counts[ev.kind] = (counts[ev.kind] || 0)+1
if (counts[ev.kind] < MAX_KINDS[ev.kind])
cs.push(ev)
return cs
}, [])
log_debug('saving all events to local storage', cached.length)
localStorage.setItem('event_cache', JSON.stringify(cached))
}
function save_timelines(damus)
{
const views = Object.keys(damus.views).reduce((obj, view_name) => {
const view = damus.views[view_name]
obj[view_name] = view.events.map(e => e.id).slice(0,100)
return obj
}, {})
localStorage.setItem('views', JSON.stringify(views))
}
function load_timelines(damus)
{
if (!('views' in localStorage))
return
const stored_views = JSON.parse(localStorage.getItem('views'))
for (const view_name of Object.keys(damus.views)) {
const view = damus.views[view_name]
view.events = (stored_views[view_name] || []).reduce((evs, evid) => {
const ev = damus.all_events[evid]
if (ev) evs.push(ev)
return evs
}, [])
}
}
function schedule_save_events(damus)
{
if (damus.save_timer)
clearTimeout(damus.save_timer)
damus.save_timer = setTimeout(save_cache.bind(null, damus), 3000)
}

View file

@ -1,44 +1,12 @@
function contacts_friend_list(contacts) {
return Array.from(contacts.friends)
}
function contacts_friendosphere(contacts) {
let s = new Set()
let fs = []
for (const friend of contacts.friends.keys()) {
fs.push(friend)
s.add(friend)
}
for (const friend of contacts.friend_of_friends.keys()) {
if (!s.has(friend))
fs.push(friend)
}
return fs
}
function add_contact_if_friend(contacts, ev) {
if (!contact_is_friend(contacts, ev.pubkey))
return
add_friend_contact(contacts, ev)
}
// TODO track friend sphere another way by using graph nodes
function contact_is_friend(contacts, pk) {
return contacts.friends.has(pk)
}
function add_friend_contact(contacts, contact) {
contacts.friends.add(contact.pubkey)
for (const tag of contact.tags) {
if (tag.length >= 2 && tag[0] == "p") {
if (!contact_is_friend(contacts, tag[1]))
contacts.friend_of_friends.add(tag[1])
}
}
}
function load_our_contacts(contacts, our_pubkey, ev) {
function contacts_process_event(contacts, our_pubkey, ev) {
if (ev.pubkey !== our_pubkey)
return
return;
contacts.event = ev
for (const tag of ev.tags) {
if (tag.length > 1 && tag[0] === "p") {
@ -47,3 +15,14 @@ function load_our_contacts(contacts, our_pubkey, ev) {
}
}
/* contacts_push_relay sends your contact list to the desired relay.
*/
function contacts_push_relay(contacts, relay) {
log_warn("contacts_push_relay not implemented");
}
/* contacts_save commits the contacts data to storage.
*/
function contacts_save(contacts) {
log_warn("contacts_save not implemented");
}

View file

@ -4,6 +4,7 @@ const KIND_RELAY = 2;
const KIND_CONTACT = 3;
const KIND_DM = 4;
const KIND_DELETE = 5;
const KIND_SHARE = 6;
const KIND_REACTION = 7;
const KIND_CHATROOM = 42;
@ -16,6 +17,7 @@ const STANDARD_KINDS = [
KIND_NOTE,
KIND_DELETE,
KIND_REACTION,
KIND_SHARE,
];
function get_local_state(key) {
@ -60,7 +62,7 @@ function broadcast_event(ev) {
DAMUS.pool.send(["EVENT", ev])
}
/*async function update_profile() {
async function update_profile() {
const kind = 0
const created_at = new_creation_time()
const pubkey = await get_pubkey()
@ -74,9 +76,9 @@ function broadcast_event(ev) {
let ev = { pubkey, content, created_at, kind }
ev.id = await nostrjs.calculate_id(ev)
ev = await sign_event(ev)
broadcast_event(ev)
model_get_my_relay(DAMUS, );
// TODO add error checking on updating profile
}*/
}
async function sign_event(ev) {
if (window.nostr && window.nostr.signEvent) {

View file

@ -2,11 +2,12 @@ let DAMUS
const BOOTSTRAP_RELAYS = [
"wss://nostr.rdfriedl.com",
//"wss://relay.damus.io",
"wss://relay.damus.io",
"wss://nostr-relay.wlvs.space",
"wss://nostr-pub.wellorder.net"
]
// TODO autogenerate these constants with a bash script
const IMG_EVENT_LIKED = "icon/event-liked.svg";
const IMG_EVENT_LIKE = "icon/event-like.svg";
@ -15,8 +16,8 @@ async function damus_web_init() {
let tries = 0;
function init() {
// only wait for 500ms max
const max_wait = 500
const interval = 20
const max_wait = 500;
const interval = 20;
if (window.nostr || tries >= (max_wait/interval)) {
console.info("init after", tries);
damus_web_init_ready();
@ -131,24 +132,27 @@ function on_pool_event(relay, sub_id, ev) {
const model = DAMUS;
const { ids, pool } = model;
if (new Date(ev.created_at * 1000) > new Date()) {
// Simply ignore any events that happened in the future.
return;
}
// Process event and apply side effects
if (!model.all_events[ev.id]) {
model.all_events[ev.id] = ev;
model_process_event(model, ev);
// schedule_save_events(model);
}
}
function on_eose_profiles(ids, model, relay) {
const prefix = difficulty_to_prefix(model.pow);
const fofs = Array.from(model.contacts.friend_of_friends);
const standard_kinds = [1,42,5,6,7];
let pow_filter = {kinds: standard_kinds, limit: 50};
let pow_filter = {kinds: STANDARD_KINDS, limit: 50};
if (model.pow > 0)
pow_filter.ids = [ prefix ];
let explore_filters = [ pow_filter ];
if (fofs.length > 0)
explore_filters.push({kinds: standard_kinds, authors: fofs, limit: 50});
explore_filters.push({kinds: STANDARD_KINDS, authors: fofs, limit: 50});
model.pool.subscribe(ids.explore, explore_filters, relay);
}
@ -163,9 +167,12 @@ function on_eose_comments(ids, model, events, relay) {
}
return s;
}, new Set());
const authors = Array.from(pubkeys)
// load profiles and noticed chatrooms
const profile_filter = {kinds: [0,3], authors: authors};
const authors = Array.from(pubkeys)
const profile_filter = {
kinds: [KIND_METADATA, KIND_CONTACT],
authors: authors
};
let filters = [];
if (authors.length > 0)
filters.push(profile_filter);

View file

@ -6,7 +6,7 @@ function filters_subscribe(filters, pool, relays=undefined) {
function filters_new_default(model) {
const { pubkey, ids, contacts } = model;
const friends = contacts_friend_list(contacts);
const friends = Array.from(contacts);
friends.push(pubkey);
const f = {};
f[ids.home] = filters_new_friends(friends);

View file

@ -58,8 +58,7 @@ function model_process_event_profile(model, ev) {
}
function model_process_event_contact(model, ev) {
add_contact_if_friend(model.contacts, ev)
load_our_contacts(model.contacts, model.pubkey, ev)
contacts_process_event(model.contacts, model.pubkey, ev)
load_our_relays(model.pubkey, model.pool, ev)
}
@ -230,26 +229,26 @@ function model_events_arr(model) {
function new_model() {
return {
all_events: {},
done_init: {},
notifications: 0,
max_depth: 2,
all_events: {},
reactions_to: {},
chatrooms: {},
unknown_ids: {},
unknown_pks: {},
deletions: {},
but_wait_theres_more: 0,
pow: 0, // pow difficulty target
deleted: {},
profiles: {},
profile_events: {},
but_wait_theres_more: 0,
last_event_of_kind: {},
pow: 0, // pow difficulty target
profiles: {}, // pubkey => profile data
profile_events: {}, // pubkey => event id - use with all_events
contacts: {
event: null,
friends: new Set(),
friend_of_friends: new Set(),
},
invalidated: [],
invalidated: [], // event ids which should be added/removed
};
}

View file

@ -1,4 +1,4 @@
function linkify(text, show_media) {
function linkify(text="", show_media=false) {
return text.replace(URL_REGEX, function(match, p1, p2, p3) {
const url = p2+p3;
let parsed;

View file

@ -35,19 +35,15 @@ function render_thread_collapsed(model, ev, opts)
</div>`
}
function render_replied_events(damus, view, ev, opts)
{
/*function render_replied_events(damus, view, ev, opts) {
if (!(ev.refs && ev.refs.reply))
return ""
return "";
const reply_ev = damus.all_events[ev.refs.reply]
if (!reply_ev)
return ""
return "";
opts.replies = opts.replies == null ? 1 : opts.replies + 1
if (!(opts.max_depth == null || opts.replies < opts.max_depth))
return render_thread_collapsed(damus, ev, opts)
return render_thread_collapsed(damus, ev, opts);
opts.is_reply = true
return render_event(damus, view, reply_ev, opts)
}
@ -60,15 +56,13 @@ function render_replying_to_chat(damus, ev) {
const to_users = pks.length === 0 ? "" : ` to ${names}`
return `<div class="replying-to">replying${to_users} in <span class="chatroom-name">${roomname}</span></div>`
}
}*/
function render_replying_to(model, ev) {
if (!(ev.refs && ev.refs.reply))
return ""
if (ev.kind === 42)
return render_replying_to_chat(model, ev)
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]
@ -76,11 +70,9 @@ function render_replying_to(model, ev) {
return `<div class="replying-to small-txt">reply to ${ev.refs.reply}</div>`;
pubkeys = [replying_to.pubkey]
}
const names = pubkeys.map((pk) => {
return render_mentioned_name(pk, model.profiles[pk]);
}).join(", ")
return `
<span class="replying-to small-txt">
replying to ${names}
@ -88,53 +80,44 @@ function render_replying_to(model, ev) {
`
}
function render_unknown_event(damus, ev) {
return "Unknown event " + ev.kind
}
function render_share(damus, view, ev, opts) {
// TODO validate content
const shared_ev = damus.all_events[ev.refs && ev.refs.root]
// share isn't resolved yet. that's ok, we can render this when we have
// the event
if (!shared_ev)
return ""
// 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]
}
return render_event(damus, view, shared_ev, opts)
return render_event(damus, shared_ev, opts)
}
function render_comment_body(damus, ev, opts) {
const can_delete = damus.pubkey === ev.pubkey;
function render_comment_body(model, ev, opts) {
const can_delete = model.pubkey === ev.pubkey;
const bar = !event_can_reply(ev) || opts.nobar ?
"" : render_action_bar(damus, ev, {can_delete});
const show_media = !opts.is_composing
"" : render_action_bar(model, ev, {can_delete});
// Only show media for content that is by friends.
const show_media = !opts.is_composing &&
model.contacts.friends.has(ev.pubkey);
return `
<div>
${render_replying_to(damus, ev)}
${render_replying_to(model, ev)}
${render_shared_by(ev, opts)}
</div>
<p>
${format_content(ev, show_media)}
</p>
${render_reactions(damus, ev)}
${bar}
`
${render_reactions(model, ev)}
${bar}`
}
function render_shared_by(ev, opts) {
const b = opts.shared
if (!b) {
return ""
}
return `
<div class="shared-by">
Shared by ${render_name(b.pubkey, b.profile)}
</div>
`
if (!opts.shared)
return "";
const { profile, pubkey } = opts.shared
return `<div class="shared-by">Shared by ${render_name(pubkey, profile)}
</div>`
}
function render_event(model, ev, opts={}) {
@ -144,6 +127,10 @@ function render_event(model, ev, opts={}) {
reply_line_bot,
} = opts
if (ev.kind == KIND_SHARE) {
return render_share(model, ev, opts);
}
const thread_root = (ev.refs && ev.refs.root) || ev.id;
const profile = model.profiles[ev.pubkey];
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
@ -267,11 +254,7 @@ function render_reactions_inner(model, ev) {
}
function render_reactions(model, ev) {
return `
<div class="reactions">
${render_reactions_inner(model, ev)}
</div>
`
return `<div class="reactions">${render_reactions_inner(model, ev)}</div>`
}
// Utility Methods
@ -286,18 +269,14 @@ function render_username(pk, profile)
}
function render_mentioned_name(pk, profile) {
return render_name(pk, profile, "@");
return render_name(pk, profile, "");
}
function render_name(pk, profile, prefix="") {
return `
<span>
${prefix}
<span class="username clickable" data-pubkey="${pk}"
onclick="open_profile('${pk}')">
${fmt_profile_name(profile, fmt_pubkey(pk))}
</span>
</span>`
// Beware of whitespace.
return `<span>${prefix}<span class="username clickable" data-pubkey="${pk}"
onclick="open_profile('${pk}')"
>${fmt_profile_name(profile, fmt_pubkey(pk))}</span></span>`
}
function render_pfp(pk, profile, opts={}) {

View file

@ -75,6 +75,7 @@ function view_timeline_update(model) {
// Dumb function to insert needed events
let visible_count = 0;
const all = model_events_arr(model);
const left_overs = [];
while (model.invalidated.length > 0) {
var evid = model.invalidated.pop();
var ev = model.all_events[evid];
@ -84,13 +85,19 @@ function view_timeline_update(model) {
continue;
}
// if event is in el already, do nothing or update?
// If event is in el already, do nothing or update?
let ev_el = find_node("#ev"+evid, el);
if (ev_el) {
continue;
} else {
let div = document.createElement("div");
div.innerHTML = render_event(model, ev, {});
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");
@ -112,6 +119,7 @@ function view_timeline_update(model) {
el.insertBefore(ev_el, prior_el);
}
}
model.invalidated = model.invalidated.concat(left_overs);
if (visible_count > 0)
find_node("#view .loading-events").classList.add("hide");
@ -206,7 +214,7 @@ function view_mode_contains_event(model, ev, mode, opts={}) {
}
function event_is_renderable(ev={}) {
return ev.kind == KIND_NOTE;
return ev.kind == KIND_NOTE || ev.kind == KIND_SHARE;
return ev.kind == KIND_NOTE ||
ev.kind == KIND_REACTION ||
ev.kind == KIND_DELETE;

View file

@ -103,7 +103,16 @@ function click_copy_pk(el) {
* to the target public key of the element's dataset.pk field.
*/
function click_toggle_follow_user(el) {
alert("sorry not implemented");
const { contacts } = DAMUS;
const pubkey = el.dataset.pk;
const is_friend = contacts.friends.has(pubkey);
if (is_friend) {
contacts.friends.delete(pubkey);
} else {
contacts.friends.add(pubkey);
}
el.innerText = is_friend ? "Follow" : "Unfollow";
contacts_save();
}
/* click_event opens the thread view from the element's specified element id
@ -156,29 +165,22 @@ function press_logout() {
function delete_post_confirm(evid) {
if (!confirm("Are you sure you want to delete this post?"))
return
return;
const reason = (prompt("Why you are deleting this? Leave empty to not specify. Type CANCEL to cancel.") || "").trim()
if (reason.toLowerCase() === "cancel")
return
return;
delete_post(evid, reason)
}
async function do_send_reply() {
const modal = document.querySelector("#reply-modal")
const replying_to = modal.querySelector("#replying-to")
const evid = replying_to.dataset.evid
const reply_content_el = document.querySelector("#reply-content")
const content = reply_content_el.value
await send_reply(content, evid)
reply_content_el.value = ""
close_reply()
const modal = document.querySelector("#reply-modal");
const replying_to = modal.querySelector("#replying-to");
const evid = replying_to.dataset.evid;
const reply_content_el = document.querySelector("#reply-content");
const content = reply_content_el.value;
await send_reply(content, evid);
reply_content_el.value = "";
close_reply();
}
function reply_to(evid) {
@ -290,7 +292,7 @@ function open_profile(pubkey) {
el_nip5.classList.toggle("hide", !profile.nip05);
const el_desc = find_node("[role='profile-desc']", el)
el_desc.innerHTML = newlines_to_br(profile.about);
el_desc.innerHTML = newlines_to_br(linkify(profile.about));
el_desc.classList.toggle("hide", !profile.about);
find_node("button[role='copy-pk']", el).dataset.pk = pubkey;

File diff suppressed because one or more lines are too long