-
diff --git a/web/js/cache.js b/web/js/cache.js
new file mode 100644
index 0000000..a0982cc
--- /dev/null
+++ b/web/js/cache.js
@@ -0,0 +1,89 @@
+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)
+}
+
+
diff --git a/web/js/contacts.js b/web/js/contacts.js
new file mode 100644
index 0000000..56529f3
--- /dev/null
+++ b/web/js/contacts.js
@@ -0,0 +1,49 @@
+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)
+}
+
+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) {
+ if (ev.pubkey !== our_pubkey)
+ return
+ contacts.event = ev
+ for (const tag of ev.tags) {
+ if (tag.length > 1 && tag[0] === "p") {
+ contacts.friends.add(tag[1])
+ }
+ }
+}
+
diff --git a/web/js/core.js b/web/js/core.js
index 1bc58c3..5cefa03 100644
--- a/web/js/core.js
+++ b/web/js/core.js
@@ -1,3 +1,8 @@
+const KIND_METADATA = 0;
+const KIND_NOTE = 1;
+const KIND_SERVER = 2;
+const KIND_REACTION = 7;
+
function get_local_state(key) {
if (DAMUS[key] != null)
return DAMUS[key]
@@ -197,17 +202,13 @@ function gather_reply_tags(pubkey, from) {
return tags
}
-function get_tag_event(tag)
-{
+function get_tag_event(tag) {
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 null
}
diff --git a/web/js/damus.js b/web/js/damus.js
index 33243b5..80fdc74 100644
--- a/web/js/damus.js
+++ b/web/js/damus.js
@@ -1,9 +1,12 @@
let DAMUS
+const STANDARD_KINDS = [1, 42, 5, 6, 7];
+
const BOOTSTRAP_RELAYS = [
- "wss://relay.damus.io",
- "wss://nostr-relay.wlvs.space",
- "wss://nostr-pub.wellorder.net"
+ "wss://nostr.rdfriedl.com",
+ //"wss://relay.damus.io",
+ //"wss://nostr-relay.wlvs.space",
+ //"wss://nostr-pub.wellorder.net"
]
const DEFAULT_PROFILE = {
@@ -30,869 +33,154 @@ async function damus_web_init() {
}
async function damus_web_init_ready() {
- const model = init_home_model()
+ const model = new_model()
DAMUS = model
model.pubkey = await get_pubkey()
if (!model.pubkey)
return
const {RelayPool} = nostrjs
const pool = RelayPool(BOOTSTRAP_RELAYS)
- const now = new_creation_time()
const ids = {
- comments: "comments",//uuidv4(),
- profiles: "profiles",//uuidv4(),
- explore: "explore",//uuidv4(),
- refevents: "refevents",//uuidv4(),
- account: "account",//uuidv4(),
- home: "home",//uuidv4(),
- contacts: "contacts",//uuidv4(),
+ comments: "comments",//uuidv4(),
+ profiles: "profiles",//uuidv4(),
+ explore: "explore",//uuidv4(),
+ refevents: "refevents",//uuidv4(),
+ account: "account",//uuidv4(),
+ home: "home",//uuidv4(),
+ contacts: "contacts",//uuidv4(),
notifications: "notifications",//uuidv4(),
- unknowns: "unknowns",//uuidv4(),
- dms: "dms",//uuidv4(),
+ unknowns: "unknowns",//uuidv4(),
+ dms: "dms",//uuidv4(),
}
model.ids = ids
model.pool = pool
- load_cache(model)
model.view_el = document.querySelector("#view")
+ //load_cache(model)
- switch_view('home')
+ view_timeline_apply_mode(model, VM_FRIENDS);
document.addEventListener('visibilitychange', () => {
update_title(model)
- })
-
- pool.on('open', (relay) => {
- //let authors = followers
- // TODO: fetch contact list
- log_debug("relay connected", relay.url)
- if (!model.done_init[relay]) {
- send_initial_filters(ids.account, model.pubkey, relay)
- } else {
- send_home_filters(model, relay)
- }
- //relay.subscribe(comments_id, {kinds: [1,42], limit: 100})
});
- pool.on('event', (relay, sub_id, ev) => {
- handle_home_event(model, relay, sub_id, ev)
- })
- pool.on('notice', (relay, notice) => {
- log_debug("NOTICE", relay, notice)
- })
- pool.on('eose', async (relay, sub_id) => {
- if (sub_id === ids.home) {
- //log_debug("got home EOSE from %s", relay.url)
- const events = model.views.home.events
- handle_comments_loaded(ids, model, events, relay)
- } else if (sub_id === ids.profiles) {
- //log_debug("got profiles EOSE from %s", relay.url)
- const view = get_current_view()
- handle_profiles_loaded(ids, model, view, relay)
- } else if (sub_id === ids.unknowns) {
- model.pool.unsubscribe(ids.unknowns, relay)
- }
- })
+ update_timestamps();
+ pool.on("open", on_pool_open);
+ pool.on("event", on_pool_event);
+ pool.on("notice", on_pool_notice);
+ pool.on("eose", on_pool_eose);
+ pool.on("ok", on_pool_ok);
return pool
}
-function insert_event_sorted(evs, new_ev) {
- for (let i = 0; i < evs.length; i++) {
- const ev = evs[i]
- if (new_ev.id === ev.id) {
- return false
- }
- if (new_ev.created_at > ev.created_at) {
- evs.splice(i, 0, new_ev)
- return true
- }
- }
- evs.push(new_ev)
- return true
+function update_timestamps() {
+ setTimeout(() => {
+ update_timestamps();
+ view_timeline_update_timestamps();
+ }, 60 * 1000);
}
-function init_contacts() {
- return {
- event: null,
- friends: new Set(),
- friend_of_friends: new Set(),
- }
-}
-
-function init_timeline(name) {
- return {
- name,
- events: [],
- rendered: new Set(),
- depths: {},
- expanded: new Set(),
- }
-}
-
-function init_home_model() {
- return {
- done_init: {},
- notifications: 0,
- max_depth: 2,
- all_events: {},
- reactions_to: {},
- chatrooms: {},
- unknown_ids: {},
- unknown_pks: {},
- deletions: {},
- but_wait_theres_more: 0,
- cw_open: {},
- views: {
- home: init_timeline('home'),
- explore: {
- ...init_timeline('explore'),
- seen: new Set(),
- },
- notifications: {
- ...init_timeline('notifications'),
- max_depth: 1,
- },
- profile: init_timeline('profile'),
- thread: init_timeline('thread'),
- },
- pow: 0, // pow difficulty target
- deleted: {},
- profiles: {},
- profile_events: {},
- last_event_of_kind: {},
- contacts: init_contacts()
- }
-}
-
-function notice_chatroom(state, id) {
- if (!state.chatrooms[id])
- state.chatrooms[id] = {}
-}
-
-function process_reaction_event(model, ev) {
- if (!is_valid_reaction_content(ev.content))
- return
- let last = {}
- for (const tag of ev.tags) {
- if (tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"))
- last[tag[0]] = tag[1]
- }
- if (last.e) {
- model.reactions_to[last.e] = model.reactions_to[last.e] || new Set()
- model.reactions_to[last.e].add(ev.id)
- }
-}
-
-function process_chatroom_event(model, ev) {
- model.chatrooms[ev.id] = safe_parse_json(ev.content,
- "chatroom create event");
-}
-
-function process_contact_event(model, ev) {
- load_our_contacts(model.contacts, model.pubkey, ev)
- load_our_relays(model.pubkey, model.pool, ev)
- add_contact_if_friend(model.contacts, ev)
-}
-
-function process_json_content(ev) {
- ev.json_content = safe_parse_json(ev.content, "event json_content");
-}
-
-function process_deletion_event(model, ev) {
- for (const tag of ev.tags) {
- if (tag.length >= 2 && tag[0] === "e") {
- const evid = tag[1]
- // we've already recorded this one as a valid deleted
- // event we can just ignore it
- if (model.deleted[evid])
- continue
- let ds = model.deletions[evid] =
- (model.deletions[evid] || new Set())
- // add the deletion event id to the deletion set of
- // this event we will use this to determine if this
- // event is valid later in case we don't have the
- // deleted event yet.
- ds.add(ev.id)
- }
- }
-}
-
-// TODO rename is_deleted to is_event_deleted
-function is_deleted(model, evid) {
- // we've already know it's deleted
- if (model.deleted[evid])
- return model.deleted[evid]
-
- const ev = model.all_events[evid]
- if (!ev)
- return false
-
- // all deletion events
- const ds = model.deletions[ev.id]
- if (!ds)
- return false
-
- // find valid deletion events
- for (const id of ds.keys()) {
- const d_ev = model.all_events[id]
- if (!d_ev)
- continue
-
- // only allow deletes from the user who created it
- if (d_ev.pubkey === ev.pubkey) {
- model.deleted[ev.id] = d_ev
- log_debug("received deletion for", ev)
- // clean up deletion data that we don't need anymore
- delete model.deletions[ev.id]
- return true
- } else {
- log_debug(`User ${d_ev.pubkey} tried to delete ${ev.pubkey}'s event ... what?`)
- }
- }
- return false
-}
-
-function has_profile(damus, pk) {
- return pk in damus.profiles
-}
-
-function has_event(damus, evid) {
- return evid in damus.all_events
-}
-
-function make_unk(hint, ev) {
- const attempts = 0
- const parent_created = ev.created_at
- if (hint && hint !== "")
- return {attempts, hint: hint.trim().toLowerCase(), parent_created}
- return {attempts, parent_created}
-}
-
-function notice_unknown_ids(damus, ev) {
- // make sure this event itself is removed from unknowns
- if (ev.kind === 0)
- delete damus.unknown_pks[ev.pubkey]
- delete damus.unknown_ids[ev.id]
-
- let got_some = false
-
- for (const tag of ev.tags) {
- if (tag.length >= 2) {
- if (tag[0] === "p") {
- const pk = tag[1]
- if (!has_profile(damus, pk) && is_valid_id(pk)) {
- got_some = true
- damus.unknown_pks[pk] = make_unk(tag[2], ev)
- }
- } else if (tag[0] === "e") {
- const evid = tag[1]
- if (!has_event(damus, evid) && is_valid_id(evid)) {
- got_some = true
- damus.unknown_ids[evid] = make_unk(tag[2], ev)
- }
- }
- }
- }
-
- return got_some
-}
-
-function gather_unknown_hints(damus, pks, evids)
-{
- let relays = new Set()
- for (const pk of pks) {
- const unk = damus.unknown_pks[pk]
- if (unk && unk.hint && unk.hint !== "")
- relays.add(unk.hint)
- }
- for (const evid of evids) {
- const unk = damus.unknown_ids[evid]
- if (unk && unk.hint && unk.hint !== "")
- relays.add(unk.hint)
- }
- return Array.from(relays)
-}
-
-function get_non_expired_unknowns(unks, type)
-{
- const MAX_ATTEMPTS = 2
-
- function sort_parent_created(a_id, b_id) {
- const a = unks[a_id]
- const b = unks[b_id]
- return b.parent_created - a.parent_created
- }
-
- let new_expired = 0
- const ids = Object.keys(unks).sort(sort_parent_created).reduce((ids, unk_id) => {
- if (ids.length >= 255)
- return ids
-
- const unk = unks[unk_id]
- if (unk.attempts >= MAX_ATTEMPTS) {
- if (!unk.expired) {
- unk.expired = true
- new_expired++
- }
- return ids
- }
-
- unk.attempts++
-
- ids.push(unk_id)
- return ids
- }, [])
-
- if (new_expired !== 0)
- log_debug("Gave up looking for %d %s", new_expired, type)
-
- return ids
-}
-
-function fetch_unknown_events(damus)
-{
- let filters = []
-
- const pks = get_non_expired_unknowns(damus.unknown_pks, 'profiles')
- const evids = get_non_expired_unknowns(damus.unknown_ids, 'events')
- const relays = gather_unknown_hints(damus, pks, evids)
- for (const relay of relays) {
- if (!damus.pool.has(relay)) {
- log_debug("adding %s to relays to fetch unknown events", relay)
- damus.pool.add(relay)
- }
- }
- if (evids.length !== 0) {
- const unk_kinds = [1,5,6,7,40,42]
- filters.push({ids: evids, kinds: unk_kinds})
- filters.push({"#e": evids, kinds: [1,42], limit: 100})
- }
- if (pks.length !== 0)
- filters.push({authors: pks, kinds:[0]})
- if (filters.length === 0)
- return
- log_debug("fetching unknowns", filters)
- damus.pool.subscribe('unknowns', filters)
-}
-
-function schedule_unknown_refetch(damus)
-{
- const INTERVAL = 5000
- if (!damus.unknown_timer) {
- log_debug("fetching unknown events now and in %d seconds", INTERVAL / 1000)
-
- damus.unknown_timer = setTimeout(() => {
- fetch_unknown_events(damus)
-
- setTimeout(() => {
- delete damus.unknown_timer
- if (damus.but_wait_theres_more > 0) {
- damus.but_wait_theres_more = 0
- schedule_unknown_refetch(damus)
- }
- }, INTERVAL)
- }, INTERVAL)
-
- fetch_unknown_events(damus)
+function on_pool_open(relay) {
+ log_info("opened relay", relay);
+ const model = DAMUS;
+ // We check the cache if we have init anything, if not we do our inital
+ // otherwise we do a get since last
+ if (!model.done_init[relay]) {
+ // if we have not init'd the relay simply get our initial filters
+ relay.subscribe(model.ids.account, filter_new_initial(model.pk));
} else {
- damus.but_wait_theres_more++
+ // otherwise let's get everythign since the last time
+ model_subscribe_defaults(model, relay);
}
}
-function process_event(damus, ev)
-{
- ev.refs = determine_event_refs(ev.tags)
- const notified = was_pubkey_notified(damus.pubkey, ev)
- ev.notified = notified
-
- const got_some_unknowns = notice_unknown_ids(damus, ev)
- if (got_some_unknowns)
- schedule_unknown_refetch(damus)
-
- ev.pow = calculate_pow(ev)
-
- if (ev.kind === 7)
- process_reaction_event(damus, ev)
- else if (ev.kind === 42 && ev.refs && ev.refs.root)
- notice_chatroom(damus, ev.refs.root)
- else if (ev.kind === 40)
- process_chatroom_event(damus, ev)
- else if (ev.kind === 5)
- process_deletion_event(damus, ev)
- else if (ev.kind === 0)
- process_profile_event(damus, ev)
- else if (ev.kind === 3)
- process_contact_event(damus, ev)
-
- const last_notified = get_local_state('last_notified_date')
- if (notified && (last_notified == null || ((ev.created_at*1000) > last_notified))) {
- set_local_state('last_notified_date', new Date().getTime())
- damus.notifications++
- update_title(damus)
- }
+function on_pool_notice(relay, notice) {
+ log_info("notice", relay.url, notice);
+ // DO NOTHING
}
-function was_pubkey_notified(pubkey, ev)
-{
- if (!(ev.kind === 1 || ev.kind === 42))
- return false
-
- if (ev.pubkey === pubkey)
- return false
-
- for (const tag of ev.tags) {
- if (tag.length >= 2 && tag[0] === "p" && tag[1] === pubkey)
- return true
- }
-
- return false
-}
-
-function should_add_to_notification_timeline(our_pk, contacts, ev, pow)
-{
- if (!should_add_to_timeline(ev))
- return false
-
- // TODO: add items that don't pass spam filter to "message requests"
- // Then we will need a way to whitelist people as an alternative to
- // following them
- return passes_spam_filter(contacts, ev, pow)
-}
-
-function should_add_to_explore_timeline(contacts, view, ev, pow)
-{
- if (!should_add_to_timeline(ev))
- return false
-
- if (view.seen.has(ev.pubkey))
- return false
-
- // hide friends for 0-pow situations
- if (pow === 0 && contacts.friends.has(ev.pubkey))
- return false
-
- return passes_spam_filter(contacts, ev, pow)
-}
-
-function handle_redraw_logic(model, view_name)
-{
- const view = model.views[view_name]
- if (view.redraw_timer)
- clearTimeout(view.redraw_timer)
- view.redraw_timer = setTimeout(redraw_events.bind(null, model, view), 600)
-}
-
-function schedule_save_events(damus)
-{
- if (damus.save_timer)
- clearTimeout(damus.save_timer)
- damus.save_timer = setTimeout(save_cache.bind(null, damus), 3000)
-}
-
-function calculate_last_of_kind(evs) {
- const now_sec = new Date().getTime() / 1000
- return Object.keys(evs).reduce((obj, evid) => {
- const ev = evs[evid]
- if (!is_valid_time(now_sec, ev.created_at))
- return obj
- const prev = obj[ev.kind] || 0
- obj[ev.kind] = get_since_time(max(ev.created_at, prev))
- return obj
- }, {})
-}
-
-function load_our_relays(our_pubkey, pool, ev) {
- if (ev.pubkey != our_pubkey)
- return
-
- let relays
- try {
- relays = JSON.parse(ev.content)
- } catch (e) {
- log_error("error loading relays", e)
- return
- }
-
- for (const relay of Object.keys(relays)) {
- if (!pool.has(relay)) {
- log_debug("adding relay", relay)
- pool.add(relay)
- }
- }
-}
-
-function load_our_contacts(contacts, our_pubkey, ev) {
- if (ev.pubkey !== our_pubkey)
- return
- contacts.event = ev
- for (const tag of ev.tags) {
- if (tag.length > 1 && tag[0] === "p") {
- contacts.friends.add(tag[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 handle_home_event(model, relay, sub_id, ev) {
- const ids = model.ids
-
- // ignore duplicates
- if (!has_event(model, ev.id)) {
- model.all_events[ev.id] = ev
- process_event(model, ev)
- schedule_save_events(model)
- }
-
- ev = model.all_events[ev.id]
-
- let is_new = true
+// on_pool_eose occurs when all storage from a relay has been sent to the
+// client.
+async function on_pool_eose(relay, sub_id) {
+ //console.info("eose", relay.url, sub_id);
+ const model = DAMUS;
+ const { ids, pool } = model;
switch (sub_id) {
- case model.ids.explore:
- const view = model.views.explore
-
- // show more things in explore timeline
- if (should_add_to_explore_timeline(model.contacts, view, ev, model.pow)) {
- view.seen.add(ev.pubkey)
- is_new = insert_event_sorted(view.events, ev)
- }
-
- if (is_new)
- handle_redraw_logic(model, 'explore')
- break;
-
- case model.ids.notifications:
- if (should_add_to_notification_timeline(model.pubkey, model.contacts, ev, model.pow))
- is_new = insert_event_sorted(model.views.notifications.events, ev)
-
- if (is_new)
- handle_redraw_logic(model, 'notifications')
- break;
-
- case model.ids.home:
- if (should_add_to_timeline(ev))
- is_new = insert_event_sorted(model.views.home.events, ev)
-
- if (is_new)
- handle_redraw_logic(model, 'home')
- break;
- case model.ids.account:
- switch (ev.kind) {
- case 3:
- model.done_init[relay] = true
- model.pool.unsubscribe(model.ids.account, relay)
- send_home_filters(model, relay)
- break
- }
- break
- case model.ids.profiles:
- break
+ case ids.home:
+ const events = model_events_arr(model);
+ // TODO filter out events to friends of friends
+ on_eose_comments(ids, model, events, relay)
+ pool.unsubscribe(ids.home, relay);
+ break;
+ case ids.profiles:
+ model.pool.unsubscribe(ids.profiles, relay);
+ on_eose_profiles(ids, model, relay)
+ break;
+ case ids.unknown:
+ pool.unsubscribe(ids.unknowns, relay);
+ break;
}
}
-function process_profile_event(model, ev) {
- const prev_ev = model.all_events[model.profile_events[ev.pubkey]]
- if (prev_ev && prev_ev.created_at > ev.created_at)
- return
+function on_pool_ok(relay) {
+ console.log("OK", arguments);
+}
- model.profile_events[ev.pubkey] = ev.id
- try {
- model.profiles[ev.pubkey] = JSON.parse(ev.content)
- } catch(e) {
- log_debug("failed to parse profile contents", ev)
+function on_pool_event(relay, sub_id, ev) {
+ const model = DAMUS;
+ const { ids, pool } = model;
+
+ // 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);
+ }
+
+ // Update subscriptions
+ switch (sub_id) {
+ case ids.account:
+ model.done_init[relay] = true;
+ pool.unsubscribe(ids.account, relay);
+ model_subscribe_defaults(model, relay);
+ break;
}
}
-function send_initial_filters(account_id, pubkey, relay) {
- const filter = {authors: [pubkey], kinds: [3], limit: 1}
- //console.log("sending initial filter", filter)
- relay.subscribe(account_id, filter)
-}
-
-function send_home_filters(model, relay) {
- const ids = model.ids
- const friends = contacts_friend_list(model.contacts)
- friends.push(model.pubkey)
- const contacts_filter = {kinds: [0], authors: friends}
- const dms_filter = {kinds: [4], "#p": [ model.pubkey ], limit: 100}
- const our_dms_filter = {kinds: [4], authors: [ model.pubkey ], limit: 100}
- const standard_kinds = [1,42,5,6,7]
- const home_filter = {kinds: standard_kinds, authors: friends, limit: 500}
- // TODO: include pow+fof spam filtering in notifications query
- const notifications_filter = {kinds: standard_kinds, "#p": [model.pubkey], limit: 100}
-
- let home_filters = [home_filter]
- let notifications_filters = [notifications_filter]
- let contacts_filters = [contacts_filter]
- let dms_filters = [dms_filter, our_dms_filter]
-
- let last_of_kind = {}
- if (relay) {
- last_of_kind =
- model.last_event_of_kind[relay] =
- model.last_event_of_kind[relay] || calculate_last_of_kind(model.all_events);
- log_debug("last_of_kind", last_of_kind)
- }
-
- update_filters_with_since(last_of_kind, home_filters)
- update_filters_with_since(last_of_kind, contacts_filters)
- update_filters_with_since(last_of_kind, notifications_filters)
- update_filters_with_since(last_of_kind, dms_filters)
-
- const subto = relay? [relay] : undefined
- model.pool.subscribe(ids.home, home_filters, subto)
- model.pool.subscribe(ids.contacts, contacts_filters, subto)
- model.pool.subscribe(ids.notifications, notifications_filters, subto)
- model.pool.subscribe(ids.dms, dms_filters, subto)
-}
-
-function update_filter_with_since(last_of_kind, filter) {
- const kinds = filter.kinds || []
- let initial = null
- let earliest = kinds.reduce((earliest, kind) => {
- const last_created_at = last_of_kind[kind]
- let since = get_since_time(last_created_at)
-
- if (!earliest) {
- if (since === null)
- return null
-
- return since
- }
-
- if (since === null)
- return earliest
-
- return since < earliest ? since : earliest
-
- }, initial)
- if (earliest)
- filter.since = earliest;
-}
-
-function update_filters_with_since(last_of_kind, filters) {
- for (const filter of filters) {
- update_filter_with_since(last_of_kind, filter)
- }
-}
-
-
-
-function handle_profiles_loaded(ids, model, view, relay) {
- // stop asking for profiles
- model.pool.unsubscribe(ids.profiles, relay)
-
- //redraw_events(model, view)
- redraw_my_pfp(model)
-
- 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}
+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};
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})
- }
-
- model.pool.subscribe(ids.explore, explore_filters, relay)
+ pow_filter.ids = [ prefix ];
+ let explore_filters = [ pow_filter ];
+ if (fofs.length > 0)
+ explore_filters.push({kinds: standard_kinds, authors: fofs, limit: 50});
+ model.pool.subscribe(ids.explore, explore_filters, relay);
}
-// load profiles after comment notes are loaded
-function handle_comments_loaded(ids, model, events, relay) {
+function on_eose_comments(ids, model, events, relay) {
const pubkeys = events.reduce((s, ev) => {
- s.add(ev.pubkey)
+ s.add(ev.pubkey);
for (const tag of ev.tags) {
if (tag.length >= 2 && tag[0] === "p") {
if (!model.profile_events[tag[1]])
- s.add(tag[1])
+ s.add(tag[1]);
}
}
- return s
- }, new Set())
+ return s;
+ }, new Set());
const authors = Array.from(pubkeys)
-
// load profiles and noticed chatrooms
- const profile_filter = {kinds: [0,3], authors: authors}
-
- let filters = []
+ const profile_filter = {kinds: [0,3], authors: authors};
+ let filters = [];
if (authors.length > 0)
- filters.push(profile_filter)
+ filters.push(profile_filter);
if (filters.length === 0) {
- log_debug("No profiles filters to request...")
+ //log_debug("No profiles filters to request...")
return
}
-
//console.log("subscribe", profiles_id, filter, relay)
- log_debug("subscribing to profiles on %s", relay.url)
+ //log_debug("subscribing to profiles on %s", relay.url)
model.pool.subscribe(ids.profiles, filters, relay)
}
-function determine_event_refs_positionally(pubkeys, ids)
-{
- if (ids.length === 1)
- return {root: ids[0], reply: ids[0], pubkeys}
- else if (ids.length >= 2)
- return {root: ids[0], reply: ids[1], pubkeys}
-
- return {pubkeys}
-}
-
-function determine_event_refs(tags) {
- let positional_ids = []
- let pubkeys = []
- let root
- let reply
- let i = 0
-
- for (const tag of tags) {
- if (tag.length >= 4 && tag[0] == "e") {
- positional_ids.push(tag[1])
- if (tag[3] === "root") {
- root = tag[1]
- } else if (tag[3] === "reply") {
- reply = tag[1]
- }
- } else if (tag.length >= 2 && tag[0] == "e") {
- positional_ids.push(tag[1])
- } else if (tag.length >= 2 && tag[0] == "p") {
- pubkeys.push(tag[1])
- }
-
- i++
- }
-
- if (!(root && reply) && positional_ids.length > 0)
- return determine_event_refs_positionally(pubkeys, positional_ids)
-
- /*
- if (reply && !root)
- root = reply
- */
-
- return {root, reply, pubkeys}
-}
-
-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)
-}
-
-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])
- }
- }
-}
-
diff --git a/web/js/event.js b/web/js/event.js
new file mode 100644
index 0000000..7634bca
--- /dev/null
+++ b/web/js/event.js
@@ -0,0 +1,122 @@
+/* EVENT */
+
+/* event_refs_pubkey checks if the event (ev) is directed at the public key
+ * (pubkey) by checking its type, tags, and key.
+ */
+function event_refs_pubkey(ev, pubkey) {
+ if (!(ev.kind === 1 || ev.kind === 42))
+ return false
+ if (ev.pubkey === pubkey)
+ return false
+ for (const tag of ev.tags) {
+ if (tag.length >= 2 && tag[0] === "p" && tag[1] === pubkey)
+ return true
+ }
+ return false
+}
+
+function event_calculate_pow(ev) {
+ const id_bits = leading_zero_bits(ev.id)
+ for (const tag of ev.tags) {
+ if (tag.length >= 3 && tag[0] === "nonce") {
+ const target = +tag[2]
+ if (isNaN(target))
+ return 0
+
+ // if our nonce target is smaller than the difficulty,
+ // then we use the nonce target as the actual difficulty
+ return min(target, id_bits)
+ }
+ }
+
+ // not a valid pow if we don't have a difficulty target
+ return 0
+}
+
+/* event_can_reply returns a boolean value based on if you can "reply" to the
+ * event in the manner of a chat.
+ */
+function event_can_reply(ev) {
+ return ev.kind === 1 || ev.kind === 42
+}
+
+/* event_is_timeline returns a boolean based on if the event should be rendered
+ * in a GUI
+ */
+function event_is_timeline(ev) {
+ return ev.kind === 1 || ev.kind === 42 || ev.kind === 6
+}
+
+function event_get_tag_refs(tags) {
+ let ids = []
+ let pubkeys = []
+ let root, reply
+ for (const tag of tags) {
+ if (tag.length >= 4 && tag[0] == "e") {
+ ids.push(tag[1])
+ if (tag[3] === "root") {
+ root = tag[1]
+ } else if (tag[3] === "reply") {
+ reply = tag[1]
+ }
+ } else if (tag.length >= 2 && tag[0] == "e") {
+ ids.push(tag[1])
+ } else if (tag.length >= 2 && tag[0] == "p") {
+ pubkeys.push(tag[1])
+ }
+ }
+ if (!(root && reply) && ids.length > 0) {
+ if (ids.length === 1)
+ return {root: ids[0], reply: ids[0], pubkeys}
+ else if (ids.length >= 2)
+ return {root: ids[0], reply: ids[1], pubkeys}
+ return {pubkeys}
+ }
+ return {root, reply, pubkeys}
+}
+
+function event_is_spam(ev, contacts, pow) {
+ if (contacts.friend_of_friends.has(ev.pubkey))
+ return true
+ return ev.pow >= pow
+}
+
+function event_cmp_created(a, b) {
+ if (a.created_at > b.created_at)
+ return 1;
+ if (a.created_at < b.created_at)
+ return -1;
+ return 0;
+}
+
+/* event_refs_event checks if event A references event B in its tags.
+ */
+function event_refs_event(a, b) {
+ for (const tag of a.tags) {
+ if (tag.length >= 2 && tag[0] === "e" && tag[1] == b.id)
+ return true;
+ }
+ return false;
+}
+
+function event_get_last_tags(ev) {
+ let o = {};
+ for (const tag of ev.tags) {
+ if (tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"))
+ o[tag[0]] = tag[1];
+ }
+ return o;
+}
+
+/* event_get_reacted_to returns the reacted to event & pubkey (e, p). Returns
+ * undefined if invalid or incomplete.
+ */
+function event_parse_reaction(ev) {
+ if (!is_valid_reaction_content(ev.content) || ev.kind != 7)
+ return;
+ const o = event_get_last_tags(ev);
+ if (o["e"] && o["p"]) {
+ return o;
+ }
+}
+
diff --git a/web/js/filters.js b/web/js/filters.js
new file mode 100644
index 0000000..d37d2e0
--- /dev/null
+++ b/web/js/filters.js
@@ -0,0 +1,106 @@
+function filters_subscribe(filters, pool, relays=undefined) {
+ for (const key in filters) {
+ pool.subscribe(key, filters[key], relays);
+ }
+}
+
+function filters_new_default(model) {
+ const { pubkey, ids, contacts } = model;
+ const friends = contacts_friend_list(contacts);
+ friends.push(pubkey);
+ const f = {};
+ f[ids.home] = filters_new_friends(friends);
+ f[ids.contacts] = filters_new_contacts(friends);
+ f[ids.dms] = filters_new_dms(pubkey);
+ f[ids.notifications] = filters_new_notifications(pubkey);
+ return f;
+}
+
+function filters_new_default_since(model, cache) {
+ const filters = filters_new_default(model);
+ for (const key in filters) {
+ filters[key] = filters_set_since(filters[key], cache);
+ }
+ return filters;
+}
+
+function filter_new_initial(pubkey) {
+ return {
+ authors: [pubkey],
+ kinds: [3],
+ limit: 1,
+ };
+}
+
+function filters_new_contacts(friends) {
+ return [{
+ kinds: [0],
+ authors: friends,
+ }]
+}
+
+function filters_new_dms(pubkey, limit=100) {
+ return [
+ { // dms we sent
+ kinds: [4],
+ limit,
+ authors: [pubkey],
+ }, { // replys to us
+ kinds: [4],
+ limit: limit,
+ "#p": [pubkey],
+ }]
+}
+
+function filters_new_friends(friends, limit=500) {
+ return [{
+ kinds: STANDARD_KINDS,
+ authors: friends,
+ limit,
+ }]
+}
+
+function filters_new_notifications(pubkey, limit=100) {
+ return [{
+ kinds: STANDARD_KINDS,
+ "#p": [pubkey],
+ limit,
+ }]
+}
+
+function filters_set_since(filters=[], cache={}) {
+ filters.forEach((filter) => {
+ const since = get_earliest_since_time(filter.kinds, cache)
+ delete filter.since;
+ if (since) filter.since = since;
+ });
+ return filters
+}
+
+
+function get_earliest_since_time(kinds=[], cache={}) {
+ const earliest = kinds.reduce((a, kind) => {
+ const b = get_since_time(cache[kind]);
+ if (!a) {
+ return b;
+ }
+ return b < a ? b : a;
+ }, undefined);
+ return earliest;
+}
+
+/* calculate_last_of_kind returns a map of kinds to time, where time is the
+ * last time it saw that kind.
+ */
+function calculate_last_of_kind(evs) {
+ const now_sec = new Date().getTime() / 1000
+ return Object.keys(evs).reduce((obj, evid) => {
+ const ev = evs[evid]
+ if (!is_valid_time(now_sec, ev.created_at))
+ return obj
+ const prev = obj[ev.kind] || 0
+ obj[ev.kind] = get_since_time(max(ev.created_at, prev))
+ return obj
+ }, {})
+}
+
diff --git a/web/js/lib.js b/web/js/lib.js
new file mode 100644
index 0000000..6f0586d
--- /dev/null
+++ b/web/js/lib.js
@@ -0,0 +1,91 @@
+function load_our_relays(our_pubkey, pool, ev) {
+ if (ev.pubkey != our_pubkey)
+ return
+
+ let relays
+ try {
+ relays = JSON.parse(ev.content)
+ } catch (e) {
+ log_error("error loading relays", e)
+ return
+ }
+
+ for (const relay of Object.keys(relays)) {
+ if (!pool.has(relay)) {
+ log_debug("adding relay", relay)
+ pool.add(relay)
+ }
+ }
+}
+
+function notice_chatroom(state, id) {
+ if (!state.chatrooms[id])
+ state.chatrooms[id] = {}
+}
+
+function should_add_to_notification_timeline(our_pk, contacts, ev, pow)
+{
+ if (!should_add_to_timeline(ev))
+ return false
+
+ // TODO: add items that don't pass spam filter to "message requests"
+ // Then we will need a way to whitelist people as an alternative to
+ // following them
+ return passes_spam_filter(contacts, ev, pow)
+}
+
+function should_add_to_explore_timeline(contacts, view, ev, pow)
+{
+ if (!should_add_to_timeline(ev))
+ return false
+
+ if (view.seen.has(ev.pubkey))
+ return false
+
+ // hide friends for 0-pow situations
+ if (pow === 0 && contacts.friends.has(ev.pubkey))
+ return false
+
+ return passes_spam_filter(contacts, ev, pow)
+}
+
+function handle_redraw_logic(model, view_name)
+{
+ const view = model.views[view_name]
+ if (view.redraw_timer)
+ clearTimeout(view.redraw_timer)
+ view.redraw_timer = setTimeout(redraw_events.bind(null, model, view), 600)
+}
+
+/* DEPRECATED */
+
+function is_deleted(model, evid) {
+ log_warn("is_deleted deprecated, use model_is_event_deleted");
+ return model_is_event_deleted(model, evid);
+}
+function process_event(model, ev) {
+ log_warn("process_event deprecated, use event_process");
+ return model_process_event(model, ev);
+}
+function calculate_pow(ev) {
+ log_warn("calculate_pow deprecated, use event_calculate_pow");
+ return event_calculate_pow(ev);
+}
+function insert_event_sorted(evs, new_ev) {
+ log_warn("insert_event_sorted deprecated, use events_insert_sorted");
+ events_insert_sorted(evs, new_ev);
+}
+function can_reply(ev) {
+ log_warn("can_reply is deprecated, use event_can_reply");
+ return event_can_reply(ev);
+}
+function should_add_to_timeline(ev) {
+ // TODO rename should_add_to_timeline to is_timeline_event
+ log_warn("should_add_to_timeline is deprecated, use event_is_timeline");
+ return event_is_timeline(ev);
+}
+function passes_spam_filter(contacts, ev, pow) {
+ log_warn("passes_spam_filter deprecated, use event_is_spam");
+ return !event_is_spam(ev, contacts, pow);
+}
+
diff --git a/web/js/model.js b/web/js/model.js
new file mode 100644
index 0000000..e7b9ddc
--- /dev/null
+++ b/web/js/model.js
@@ -0,0 +1,266 @@
+/* model_process_event is the main point where events are post-processed from
+ * a relay. Additionally other side effects happen such as notification checks
+ * and fetching of unknown pubkey profiles.
+ */
+function model_process_event(model, ev) {
+ ev.refs = event_get_tag_refs(ev.tags);
+ ev.pow = event_calculate_pow(ev);
+
+ // TODO this doesn't actually work because of async nature
+ ev.is_spam = !event_is_spam(ev, model.contacts, model.pow);
+
+ // Process specific event needs based on it's kind. Not using a map because
+ // integers can't be used.
+ let fn;
+ switch(ev.kind) {
+ case 0:
+ fn = model_process_event_profile;
+ break;
+ case 3:
+ fn = model_process_event_contact;
+ break;
+ case 5:
+ fn = model_process_event_deletion;
+ break;
+ case 7:
+ fn = model_process_event_reaction;
+ break;
+ }
+ if (fn)
+ fn(model, ev);
+
+ // not handling chatrooms for now
+ // else if (ev.kind === 42 && ev.refs && ev.refs.root)
+ // notice_chatroom(damus, ev.refs.root)
+ // else if (ev.kind === 40)
+ // event_process_chatroom(damus, ev)
+
+ // check if the event that just came in should notify the user and is newer
+ // than the last recorded notification event, if it is notify
+ const notify_user = event_refs_pubkey(ev, model.pubkey)
+ const last_notified = get_local_state('last_notified_date')
+ ev.notified = notify_user;
+ if (notify_user && (last_notified == null || ((ev.created_at*1000) > last_notified))) {
+ set_local_state('last_notified_date', new Date().getTime());
+ model.notifications++;
+ update_title(model);
+ }
+
+ // If we find some unknown ids lets schedule their subscription for info
+ if (model_event_has_unknown_ids(model, ev))
+ schedule_unknown_refetch(model);
+
+ // Refresh timeline
+ model.invalidated.push(ev.id);
+ clearTimeout(inv_timer);
+ inv_timer = setTimeout(() => {
+ view_timeline_update(model);
+ }, 1000);
+}
+let inv_timer;
+
+//function process_chatroom_event(model, ev) {
+// model.chatrooms[ev.id] = safe_parse_json(ev.content,
+// "chatroom create event");
+//}
+
+/* model_process_event_profile updates the matching profile with the contents found
+ * in the event.
+ */
+function model_process_event_profile(model, ev) {
+ 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")
+ view_timeline_update_profiles(model, ev);
+}
+
+/* model_process_event_reaction updates the reactions dictionary
+ */
+function model_process_event_reaction(model, ev) {
+ let reaction = event_parse_reaction(ev);
+ if (!reaction)
+ return;
+ if (!model.reactions_to[reaction.e])
+ model.reactions_to[reaction.e] = new Set();
+ model.reactions_to[reaction.e].add(ev.id);
+ view_timeline_update_reaction(model, ev);
+}
+
+function model_process_event_contact(model, ev) {
+ load_our_contacts(model.contacts, model.pubkey, ev)
+ load_our_relays(model.pubkey, model.pool, ev)
+ add_contact_if_friend(model.contacts, ev)
+}
+
+/* event_process_deletion updates the list of deleted events.
+ */
+function model_process_event_deletion(model, ev) {
+ for (const tag of ev.tags) {
+ if (tag.length >= 2 && tag[0] === "e") {
+ const evid = tag[1]
+ // we've already recorded this one as a valid deleted
+ // event we can just ignore it
+ if (model.deleted[evid])
+ continue
+ let ds = model.deletions[evid] =
+ (model.deletions[evid] || new Set())
+ // add the deletion event id to the deletion set of
+ // this event we will use this to determine if this
+ // event is valid later in case we don't have the
+ // deleted event yet.
+ ds.add(ev.id)
+ }
+ }
+}
+
+/* model_event_has_unknown_ids checks the event if there are any referenced keys with
+ * unknown user profiles in the provided scope.
+ */
+function model_event_has_unknown_ids(damus, ev) {
+ // make sure this event itself is removed from unknowns
+ if (ev.kind === 0)
+ delete damus.unknown_pks[ev.pubkey]
+ delete damus.unknown_ids[ev.id]
+ let got_some = false
+ for (const tag of ev.tags) {
+ if (tag.length >= 2) {
+ if (tag[0] === "p") {
+ const pk = tag[1]
+ if (!model_has_profile(damus, pk) && is_valid_id(pk)) {
+ got_some = true
+ damus.unknown_pks[pk] = make_unk(tag[2], ev)
+ }
+ } else if (tag[0] === "e") {
+ const evid = tag[1]
+ if (!model_has_event(damus, evid) && is_valid_id(evid)) {
+ got_some = true
+ damus.unknown_ids[evid] = make_unk(tag[2], ev)
+ }
+ }
+ }
+ }
+ return got_some
+}
+
+function model_is_event_deleted(model, evid) {
+ // we've already know it's deleted
+ if (model.deleted[evid])
+ return model.deleted[evid]
+
+ const ev = model.all_events[evid]
+ if (!ev)
+ return false
+
+ // all deletion events
+ const ds = model.deletions[ev.id]
+ if (!ds)
+ return false
+
+ // find valid deletion events
+ for (const id of ds.keys()) {
+ const d_ev = model.all_events[id]
+ if (!d_ev)
+ continue
+
+ // only allow deletes from the user who created it
+ if (d_ev.pubkey === ev.pubkey) {
+ model.deleted[ev.id] = d_ev
+ log_debug("received deletion for", ev)
+ // clean up deletion data that we don't need anymore
+ delete model.deletions[ev.id]
+ return true
+ } else {
+ log_debug(`User ${d_ev.pubkey} tried to delete ${ev.pubkey}'s event ... what?`)
+ }
+ }
+ 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
+}
+
+/* model_relay_update_lok returns a map of kinds found in all events based on
+ * the last seen event of each kind. It also updates the model's cached value.
+ * If the cached value is found it returns that instead
+ */
+function model_relay_update_lok(model, relay) {
+ let last_of_kind = model.last_event_of_kind[relay];
+ if (!last_of_kind) {
+ last_of_kind = model.last_event_of_kind[relay]
+ = calculate_last_of_kind(model.all_events);
+ }
+ return last_of_kind;
+}
+
+function model_subscribe_defaults(model, relay) {
+ const lok = model_relay_update_lok(model, relay);
+ const filters = filters_new_default_since(model, lok);
+ filters_subscribe(filters, model.pool, [relay]);
+}
+
+function test_model_events_arr() {
+ const arr = model_events_arr({all_events: {
+ "c": {name: "c", created_at: 2},
+ "a": {name: "a", created_at: 0},
+ "b": {name: "b", created_at: 1},
+ "e": {name: "e", created_at: 4},
+ "d": {name: "d", created_at: 3},
+ }});
+ let last;
+ while(arr.length > 0) {
+ let ev = arr.pop();
+ log_debug("test:", ev.name, ev.created_at);
+ if (!last) {
+ last = ev;
+ continue;
+ }
+ if (ev.created_at > last.created_at) {
+ log_error(`ev ${ev.name} should be before ${last.name}`);
+ }
+ last = ev;
+ }
+}
+
+function model_events_arr(model) {
+ const events = model.all_events;
+ let arr = [];
+ for (const evid in events) {
+ const ev = events[evid];
+ const i = arr_bsearch_insert(arr, ev, event_cmp_created);
+ arr.splice(i, 0, ev);
+ }
+ return arr;
+}
+
+function new_model() {
+ return {
+ 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: {},
+ last_event_of_kind: {},
+ contacts: {
+ event: null,
+ friends: new Set(),
+ friend_of_friends: new Set(),
+ },
+ invalidated: [],
+ }
+}
diff --git a/web/js/ui/fmt.js b/web/js/ui/fmt.js
index be92714..3a23733 100644
--- a/web/js/ui/fmt.js
+++ b/web/js/ui/fmt.js
@@ -1,7 +1,12 @@
function linkify(text, show_media) {
return text.replace(URL_REGEX, function(match, p1, p2, p3) {
- const url = p2+p3
- const parsed = new URL(url)
+ const url = p2+p3;
+ let parsed;
+ try {
+ parsed = new URL(url)
+ } catch (err) {
+ return match;
+ }
let html;
if (show_media && is_img_url(parsed.pathname)) {
html = `
@@ -26,10 +31,8 @@ function format_content(ev, show_media) {
return "❤️"
return sanitize(ev.content.trim())
}
-
const content = ev.content.trim()
const body = convert_quote_blocks(content, show_media)
-
let cw = get_content_warning(ev.tags)
if (cw !== null) {
let cwHTML = "Content Warning"
@@ -38,15 +41,13 @@ function format_content(ev, show_media) {
} else {
cwHTML += `: "
${cw}".`
}
- const open = !!DAMUS.cw_open[ev.id]? "open" : ""
return `
-
+
${cwHTML}
${body}
`
}
-
return body
}
diff --git a/web/js/ui/render.js b/web/js/ui/render.js
index b7dd500..ac2beba 100644
--- a/web/js/ui/render.js
+++ b/web/js/ui/render.js
@@ -2,7 +2,7 @@
// is done by simple string manipulations & templates. If you need to write
// loops simply write it in code and return strings.
-function render_timeline_event(damus, view, ev)
+/*function render_timeline_event(damus, view, ev)
{
const root_id = get_thread_root_id(damus, ev.id)
const max_depth = root_id ? get_thread_max_depth(damus, view, root_id) : get_default_max_depth(damus, view)
@@ -17,7 +17,7 @@ function render_events(damus, view) {
return view.events
.filter((ev, i) => i < 140)
.map((ev) => render_timeline_event(damus, view, ev)).join("\n")
-}
+}*/
function render_reply_line_top(has_top_line) {
const classes = has_top_line ? "" : "invisible"
@@ -97,7 +97,7 @@ function render_unknown_event(damus, ev) {
}
function render_share(damus, view, ev, opts) {
- //todo validate content
+ // 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
@@ -113,7 +113,7 @@ function render_share(damus, view, ev, opts) {
function render_comment_body(damus, ev, opts) {
const can_delete = damus.pubkey === ev.pubkey;
- const bar = !can_reply(ev) || opts.nobar? "" : render_action_bar(damus, ev, can_delete)
+ const bar = !can_reply(ev) || opts.nobar ? "" : render_action_bar(damus, ev, {can_delete})
const show_media = !opts.is_composing
return `
@@ -157,7 +157,7 @@ function render_deleted_comment_body(ev, deleted) {
`
}
-function render_event(damus, view, ev, opts={}) {
+/*function render_event(damus, view, ev, opts={}) {
if (ev.kind === 6)
return render_share(damus, view, ev, opts)
if (shouldnt_render_event(damus.pubkey, view, ev, opts))
@@ -165,11 +165,9 @@ function render_event(damus, view, ev, opts={}) {
view.rendered.add(ev.id)
- const profile = damus.profiles[ev.pubkey]
- const delta = time_delta(new Date().getTime(), ev.created_at*1000)
-
const has_bot_line = opts.is_reply
const reply_line_bot = (has_bot_line && render_reply_line_bot()) || ""
+ if (opts.is_composing) has_bot_line = true;
const deleted = is_deleted(damus, ev.id)
if (deleted && !opts.is_reply)
@@ -183,29 +181,67 @@ function render_event(damus, view, ev, opts={}) {
}
const has_top_line = replied_events !== ""
- const border_bottom = opts.is_composing || has_bot_line ? "" : "bottom-border";
return `
${replied_events}
-
+ ` + render_event2(damus, ev, {
+ deleted,
+ has_top_line,
+ has_bot_line,
+ reply_line_bot,
+ });
+}*/
+
+function render_event(model, ev, opts={}) {
+ let {
+ has_bot_line,
+ has_top_line,
+ reply_line_bot,
+ } = 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)
+ const border_bottom = opts.is_composing || has_bot_line ? "" : "bottom-border";
+ let thread_btn = "";
+ if (!reply_line_bot) reply_line_bot = '';
+ return `
${render_reply_line_top(has_top_line)}
- ${deleted ? render_deleted_pfp() : render_pfp(ev.pubkey, profile)}
+ ${render_pfp(ev.pubkey, profile)}
${reply_line_bot}
${render_name(ev.pubkey, profile)}
-
${delta}
-
-
- `
+
`
+}
+
+function render_event_nointeract(model, ev, opts={}) {
+ const profile = model.profiles[ev.pubkey];
+ const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
+ return `
+
+ ${render_pfp(ev.pubkey, profile)}
+
+
+
+ ${render_name(ev.pubkey, profile)}
+ ${delta}
+
+
+
+
`
}
function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) {
@@ -241,7 +277,8 @@ function render_reaction(model, reaction) {
return render_pfp(reaction.pubkey, profile)
}
-function render_action_bar(damus, ev, can_delete) {
+function render_action_bar(model, ev, opts={}) {
+ let { can_delete } = opts;
let delete_html = ""
if (can_delete)
delete_html = `
@@ -249,10 +286,9 @@ function render_action_bar(damus, ev, can_delete) {
`
- const groups = get_reactions(damus, ev.id)
- const like = "❤️"
- const likes = groups[like] || {}
- const react_onclick = render_react_onclick(damus.pubkey, ev.id, like, likes)
+ const groups = get_reactions(model, ev.id)
+ const react_onclick = render_react_onclick(model.pubkey, ev.id,
+ "❤️", groups["❤️"] || {})
return `
@@ -317,8 +353,12 @@ function render_mentioned_name(pk, profile) {
function render_name(pk, profile, prefix="") {
return `
- ${prefix}${render_name_plain(profile)}
+
+ ${prefix}
+
+ ${render_name_plain(profile)}
+
`
}
@@ -328,9 +368,13 @@ function render_deleted_name() {
function render_pfp(pk, profile) {
const name = render_name_plain(profile)
- return `
`
+ src="${get_picture(pk, profile)}"/>`
}
function render_deleted_pfp() {
@@ -340,8 +384,7 @@ function render_deleted_pfp() {
`
}
-function render_loading_spinner()
-{
+function render_loading_spinner() {
return `
diff --git a/web/js/ui/state.js b/web/js/ui/state.js
index cbaa378..50fc75f 100644
--- a/web/js/ui/state.js
+++ b/web/js/ui/state.js
@@ -13,25 +13,7 @@ function get_thread_max_depth(damus, view, root_id) {
return view.depths[root_id]
}
-function shouldnt_render_event(our_pk, view, ev, opts) {
- return !opts.is_composing &&
- !view.expanded.has(ev.id) &&
- view.rendered.has(ev.id)
-}
-
-function toggle_content_warning(el) {
- const id = el.id.split("_")[1]
- const ev = DAMUS.all_events[id]
-
- if (!ev) {
- log_debug("could not find content-warning event", id)
- return
- }
-
- DAMUS.cw_open[id] = el.open
-}
-
-function expand_thread(id, reply_id) {
+/*function expand_thread(id, reply_id) {
const view = get_current_view()
const root_id = get_thread_root_id(DAMUS, id)
if (!root_id) {
@@ -41,7 +23,7 @@ function expand_thread(id, reply_id) {
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]
@@ -52,65 +34,190 @@ function get_thread_root_id(damus, id) {
return ev.refs && ev.refs.root
}
-function redraw_events(damus, view) {
- //log_debug("redrawing events for", view)
- view.rendered = new Set()
- const events_el = damus.view_el.querySelector(`#${view.name}-view > .events`)
- events_el.innerHTML = render_events(damus, view)
+function switch_view(mode, opts) {
+ log_warn("switch_view deprecated, use view_timeline_apply_mode");
+ view_timeline_apply_mode(DAMUS, mode, opts);
}
-function redraw_timeline_events(damus, name) {
- const view = DAMUS.views[name]
- const events_el = damus.view_el.querySelector(`#${name}-view > .events`)
- if (view.events.length > 0) {
- redraw_events(damus, view)
- } else {
- events_el.innerHTML = render_loading_spinner()
+function view_get_timeline_el() {
+ return find_node("#timeline");
+}
+
+function view_timeline_update_profiles(model, ev) {
+ let xs, html;
+ const el = view_get_timeline_el();
+ const pk = ev.pubkey;
+ const p = model.profiles[pk];
+
+ // If it's my pubkey let's redraw my pfp that is not located in the view
+ if (pk == model.pubkey) {
+ redraw_my_pfp(model);
+ }
+
+ // Update displayed names
+ xs = el.querySelectorAll(`.username[data-pubkey='${pk}']`)
+ html = render_name_plain(p);
+ 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;
}
}
-function switch_view(name, opts={})
-{
- if (name === DAMUS.current_view) {
- log_debug("Not switching to '%s', we are already there", name)
- return
+function view_timeline_update_timestamps(model) {
+ const el = view_get_timeline_el();
+ let xs = el.querySelectorAll(".timestamp");
+ let now = new Date().getTime();
+ for (const x of xs) {
+ let t = parseInt(x.dataset.timestamp)
+ x.innerText = fmt_since_str(now, t*1000);
}
-
- const last = get_current_view()
- if (!last) {
- // render initial
- DAMUS.current_view = name
- redraw_timeline_events(DAMUS, name)
- return
- }
-
- log_debug("switching to '%s' by hiding '%s'", name, DAMUS.current_view)
-
- DAMUS.current_view = name
- const current = get_current_view()
- const last_el = get_view_el(last.name)
- const current_el = get_view_el(current.name)
-
- if (last_el)
- last_el.classList.add("hide");
-
- // TODO accomodate views that do not render events
- // TODO find out if having multiple event divs is slow
- //redraw_timeline_events(DAMUS, name)
-
- find_node("#nav > div[data-active]").dataset.active = name;
-
- if (current_el)
- current_el.classList.remove("hide");
}
-function get_current_view()
-{
- // TODO resolve memory & html descriptencies
- // Currently there is tracking of which divs are visible in HTML/CSS and
- // which is active in memory, simply resolve this by finding the visible
- // element instead of tracking it in memory (or remove dom elements). This
- // would simplify state tracking IMO - Thomas
- return DAMUS.views[DAMUS.current_view]
+function view_timeline_update_reaction(model, ev) {
+ // TODO loop through elements with ev reactions to and update them
+}
+
+const VM_FRIENDS = "friends";
+const VM_EXPLORE = "explore";
+const VM_NOTIFICATIONS = "notifications";
+const VM_THREAD = "thread";
+const VM_USER = "user";
+// friends: mine + only events that are from my contacts
+// explore: all events
+// notifications: reactions & replys
+// thread: all events in response to target event
+// user: all events by pubkey
+
+function view_mode_contains_event(model, ev, mode, opts={}) {
+ switch(mode) {
+ case VM_EXPLORE:
+ return ev.kind != KIND_REACTION;
+ case VM_USER:
+ return opts.pubkey && ev.pubkey == opts.pubkey;
+ case VM_FRIENDS:
+ return ev.pubkey == model.pubkey || contact_is_friend(model.contacts, ev.pubkey);
+ case VM_THREAD:
+ return ev.id == opts.thread_id || (ev.refs && (
+ ev.refs.root == opts.thread_id ||
+ ev.refs.reply == opts.thread_id));
+ //event_refs_event(ev, opts.thread_id);
+ case VM_NOTIFICATIONS:
+ return event_refs_pubkey(ev, model.pubkey);
+ }
+ return false;
+}
+
+function view_timeline_apply_mode(model, mode, opts={}) {
+ let xs;
+ const { pubkey, thread_id } = opts;
+ const el = view_get_timeline_el();
+
+ el.dataset.mode = mode;
+ switch(mode) {
+ case VM_THREAD:
+ el.dataset.threadId = thread_id;
+ case VM_USER:
+ el.dataset.pubkey = pubkey;
+ break;
+ default:
+ delete el.dataset.threadId;
+ delete el.dataset.pubkey;
+ break;
+ }
+
+ const names = {};
+ names[VM_FRIENDS] = "Home";
+ names[VM_EXPLORE] = "Explore";
+ names[VM_NOTIFICATIONS] = "Notifications";
+ names[VM_USER] = "Profile";
+ names[VM_THREAD] = "Thread";
+ find_node("#view header > label").innerText = mode == VM_USER ? render_name_plain(DAMUS.profiles[opts.pubkey]) : names[mode];
+ find_node("#nav > div[data-active]").dataset.active = names[mode].toLowerCase();
+ find_node("#view [role='profile-info']").classList.toggle("hide", mode != VM_USER);
+ find_node("#newpost").classList.toggle("hide", mode != VM_FRIENDS);
+
+ 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));
+ }
+}
+
+/* view_timeline_update iterates through invalidated event ids and either adds
+ * or removes them from the timeline.
+ */
+function view_timeline_update(model) {
+ const el = view_get_timeline_el();
+ const mode = el.dataset.mode;
+ const opts = {
+ thread_id: el.dataset.threadId,
+ 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;
+ const all = model_events_arr(model);
+ while (model.invalidated.length > 0) {
+ var evid = model.invalidated.pop();
+ var ev = model.all_events[evid];
+ if (!event_is_renderable(ev) || model_is_event_deleted(model, evid)) {
+ // TODO check deleted
+ let x = find_node("#ev"+evid, el);
+ if (x) el.removeChild(x);
+ continue;
+ }
+
+ // 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, {});
+ 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.appendChild(ev_el);
+ } else {
+ el.insertBefore(ev_el, prior_el);
+ }
+ }
+
+ if (visible_count > 0)
+ find_node("#view .loading-events").classList.add("hide");
+}
+
+function event_is_renderable(ev={}) {
+ if (ev.is_spam) return false;
+ if (ev.kind != 1) return false;
+ return true;
}
diff --git a/web/js/ui/util.js b/web/js/ui/util.js
index 1502c15..bb13200 100644
--- a/web/js/ui/util.js
+++ b/web/js/ui/util.js
@@ -172,17 +172,16 @@ async function do_send_reply() {
}
function reply_to(evid) {
+ const ev = DAMUS.all_events[evid]
const modal = document.querySelector("#reply-modal")
const replybox = modal.querySelector("#reply-content")
- modal.classList.remove("closed")
const replying_to = modal.querySelector("#replying-to")
-
replying_to.dataset.evid = evid
-
- const ev = DAMUS.all_events[evid]
- const view = get_current_view()
- replying_to.innerHTML = render_event(DAMUS, view, ev, {is_composing: true, nobar: true, max_depth: 1})
-
+ replying_to.innerHTML = render_event_nointeract(DAMUS, ev, {
+ is_composing: true,
+ nobar: true
+ });
+ modal.classList.remove("closed")
replybox.focus()
}
@@ -190,7 +189,7 @@ function redraw_my_pfp(model, force = false) {
const p = model.profiles[model.pubkey]
if (!p) return;
const html = render_pfp(model.pubkey, p);
- const el = document.querySelector(".my-userpic")
+ const el = document.querySelector(".my-userpic");
if (!force && el.dataset.loaded) return;
el.dataset.loaded = true;
el.innerHTML = html;
@@ -261,4 +260,35 @@ function get_privkey() {
return privkey
}
+function open_thread(thread_id) {
+ view_timeline_apply_mode(DAMUS, VM_THREAD, { thread_id });
+}
+function open_profile(pubkey) {
+ view_timeline_apply_mode(DAMUS, VM_USER, { pubkey });
+
+ const profile = DAMUS.profiles[pubkey];
+ const el = find_node("[role='profile-info']");
+ // TODO show loading indicator then render
+
+ find_node("[role='profile-image']", el).src = get_picture(pubkey, profile);
+ find_nodes("[role='profile-name']", el).forEach(el => {
+ el.innerText = render_name_plain(profile);
+ });
+
+ const el_nip5 = find_node("[role='profile-nip5']", el)
+ el_nip5.innerText = profile.nip05;
+ 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.classList.toggle("hide", !profile.about);
+
+ find_node("button[role='copy-pk']", el).dataset.pk = 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);
+}
diff --git a/web/js/unknowns.js b/web/js/unknowns.js
new file mode 100644
index 0000000..c2e4aa9
--- /dev/null
+++ b/web/js/unknowns.js
@@ -0,0 +1,107 @@
+function make_unk(hint, ev) {
+ const attempts = 0
+ const parent_created = ev.created_at
+ if (hint && hint !== "")
+ return {attempts, hint: hint.trim().toLowerCase(), parent_created}
+ return {attempts, parent_created}
+}
+
+function gather_unknown_hints(damus, pks, evids)
+{
+ let relays = new Set()
+ for (const pk of pks) {
+ const unk = damus.unknown_pks[pk]
+ if (unk && unk.hint && unk.hint !== "")
+ relays.add(unk.hint)
+ }
+ for (const evid of evids) {
+ const unk = damus.unknown_ids[evid]
+ if (unk && unk.hint && unk.hint !== "")
+ relays.add(unk.hint)
+ }
+ return Array.from(relays)
+}
+
+function get_non_expired_unknowns(unks, type)
+{
+ const MAX_ATTEMPTS = 2
+
+ function sort_parent_created(a_id, b_id) {
+ const a = unks[a_id]
+ const b = unks[b_id]
+ return b.parent_created - a.parent_created
+ }
+
+ let new_expired = 0
+ const ids = Object.keys(unks).sort(sort_parent_created).reduce((ids, unk_id) => {
+ if (ids.length >= 255)
+ return ids
+
+ const unk = unks[unk_id]
+ if (unk.attempts >= MAX_ATTEMPTS) {
+ if (!unk.expired) {
+ unk.expired = true
+ new_expired++
+ }
+ return ids
+ }
+
+ unk.attempts++
+
+ ids.push(unk_id)
+ return ids
+ }, [])
+
+ if (new_expired !== 0)
+ log_debug("Gave up looking for %d %s", new_expired, type)
+
+ return ids
+}
+
+function fetch_unknown_events(damus)
+{
+ let filters = []
+ const pks = get_non_expired_unknowns(damus.unknown_pks, 'profiles')
+ const evids = get_non_expired_unknowns(damus.unknown_ids, 'events')
+ const relays = gather_unknown_hints(damus, pks, evids)
+ for (const relay of relays) {
+ if (!damus.pool.has(relay)) {
+ log_debug("adding %s to relays to fetch unknown events", relay)
+ damus.pool.add(relay)
+ }
+ }
+ if (evids.length !== 0) {
+ const unk_kinds = [1,5,6,7,40,42] // TODO don't hardcode event kinds
+ filters.push({ids: evids, kinds: unk_kinds})
+ filters.push({"#e": evids, kinds: [1,42], limit: 100})
+ }
+ if (pks.length !== 0)
+ filters.push({authors: pks, kinds:[0]})
+ if (filters.length === 0)
+ return
+ log_debug("fetching unknowns", filters)
+ damus.pool.subscribe('unknowns', filters)
+}
+
+function schedule_unknown_refetch(damus)
+{
+ const INTERVAL = 5000
+ if (!damus.unknown_timer) {
+ log_debug("fetching unknown events now and in %d seconds", INTERVAL / 1000)
+ damus.unknown_timer = setTimeout(() => {
+ fetch_unknown_events(damus)
+
+ setTimeout(() => {
+ delete damus.unknown_timer
+ if (damus.but_wait_theres_more > 0) {
+ damus.but_wait_theres_more = 0
+ schedule_unknown_refetch(damus)
+ }
+ }, INTERVAL)
+ }, INTERVAL)
+ fetch_unknown_events(damus)
+ } else {
+ damus.but_wait_theres_more++
+ }
+}
+
diff --git a/web/js/util.js b/web/js/util.js
index d5e8582..0cb6665 100644
--- a/web/js/util.js
+++ b/web/js/util.js
@@ -8,20 +8,21 @@ const REACTION_REGEX = /^[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\
function log_error(fmt, ...args) {
console.error(fmt, ...args)
}
-
function log_debug(fmt, ...args) {
console.debug(fmt, ...args)
}
-
function log_info(fmt, ...args) {
console.info(fmt, ...args)
}
+function log_warn(fmt, ...args) {
+ console.warn(fmt, ...args)
+}
function safe_parse_json(data, message) {
let value = undefined;
try {
value = JSON.parse(data);
- } catch (e) {
+ } catch (err) {
log_error(`${message} : unable to parse JSON`, err, data);
}
return value;
@@ -49,6 +50,25 @@ function shuffle(arr) {
return arr;
}
+/* arr_bsearch_insert finds the point in the array that an item should be
+ * inserted at based on the 'cmp' function used.
+ */
+function arr_bsearch_insert(arr, item, cmp) {
+ let start = 0;
+ let end = arr.length - 1;
+ while (start <= end) {
+ let middle = parseInt((start + end) / 2);
+ let x = cmp(item, arr[middle])
+ if (x > 0)
+ start = middle + 1;
+ else if (x < 0)
+ end = middle - 1;
+ else
+ return middle;
+ }
+ return start;
+}
+
function is_valid_time(now_sec, created_at) {
// don't count events far in the future
if (created_at - now_sec >= 120) {
@@ -128,39 +148,14 @@ function difficulty_to_prefix(d) {
return s
}
-function calculate_pow(ev) {
- const id_bits = leading_zero_bits(ev.id)
- for (const tag of ev.tags) {
- if (tag.length >= 3 && tag[0] === "nonce") {
- const target = +tag[2]
- if (isNaN(target))
- return 0
-
- // if our nonce target is smaller than the difficulty,
- // then we use the nonce target as the actual difficulty
- return min(target, id_bits)
- }
- }
-
- // not a valid pow if we don't have a difficulty target
- return 0
-}
-
-/* can_reply returns a boolean value based on if you can "reply" to the event
- * in the manner of a chat.
- */
-function can_reply(ev) {
- return ev.kind === 1 || ev.kind === 42
-}
-
-function should_add_to_timeline(ev) {
- // TODO rename should_add_to_timeline to is_timeline_event
- return ev.kind === 1 || ev.kind === 42 || ev.kind === 6
-}
-
/* time_delta returns a string of the time of current since previous.
*/
function time_delta(current, previous) {
+ log_warn("time_delta deprecated, use fmt_since_str");
+ fmt_since_str(current, previous);
+}
+
+function fmt_since_str(current, previous) {
var msPerMinute = 60 * 1000;
var msPerHour = msPerMinute * 60;
var msPerDay = msPerHour * 24;
@@ -239,12 +234,6 @@ function get_picture(pk, profile) {
return profile.resolved_picture
}
-function passes_spam_filter(contacts, ev, pow) {
- if (contacts.friend_of_friends.has(ev.pubkey))
- return true
- return ev.pow >= pow
-}
-
function debounce(f, interval) {
let timer = null;
let first = true;
@@ -257,3 +246,8 @@ function debounce(f, interval) {
};
}
+function process_json_content(ev) {
+ ev.json_content = safe_parse_json(ev.content, "event json_content");
+}
+
+