diff --git a/web/index.html b/web/index.html
index 9539f73..6488152 100644
--- a/web/index.html
+++ b/web/index.html
@@ -11,9 +11,12 @@
+
+
+
diff --git a/web/js/core.js b/web/js/core.js
new file mode 100644
index 0000000..1bc58c3
--- /dev/null
+++ b/web/js/core.js
@@ -0,0 +1,253 @@
+function get_local_state(key) {
+ if (DAMUS[key] != null)
+ return DAMUS[key]
+ return localStorage.getItem(key)
+}
+
+function set_local_state(key, val) {
+ DAMUS[key] = val
+ localStorage.setItem(key, val)
+}
+
+async function sign_id(privkey, id) {
+ //const digest = nostrjs.hex_decode(id)
+ const sig = await nobleSecp256k1.schnorr.sign(id, privkey)
+ return nostrjs.hex_encode(sig)
+}
+
+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
+ insert_event_sorted(evs, ev) // for uniqueness
+ 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) {
+ DAMUS.pool.send(["EVENT", ev])
+}
+
+/*async function update_profile() {
+ const kind = 0
+ const created_at = new_creation_time()
+ const pubkey = await get_pubkey()
+ const content = JSON.stringify({
+ name: "test",
+ about: "Testing",
+ picture: "",
+ nip05: ""
+ })
+
+ let ev = { pubkey, content, created_at, kind }
+ ev.id = await nostrjs.calculate_id(ev)
+ ev = await sign_event(ev)
+ broadcast_event(ev)
+ // TODO add error checking on updating profile
+}*/
+
+async function sign_event(ev) {
+ if (window.nostr && window.nostr.signEvent) {
+ const signed = await window.nostr.signEvent(ev)
+ if (typeof signed === 'string') {
+ ev.sig = signed
+ return ev
+ }
+ return signed
+ }
+
+ const privkey = get_privkey()
+ ev.sig = await sign_id(privkey, ev.id)
+ return ev
+}
+
+async function send_post() {
+ const input_el = document.querySelector("#post-input")
+ const cw_el = document.querySelector("#content-warning-input")
+ const cw = cw_el.value
+ const content = input_el.value
+ const created_at = new_creation_time()
+ const kind = 1
+ const tags = cw ? [["content-warning", cw]] : []
+ const pubkey = await get_pubkey()
+
+ let post = { pubkey, tags, content, created_at, kind }
+ post.id = await nostrjs.calculate_id(post)
+ post = await sign_event(post)
+ broadcast_event(post)
+
+ input_el.value = ""
+ cw_el.value = ""
+ post_input_changed(input_el)
+}
+
+
+async function create_reply(pubkey, content, from) {
+ const tags = gather_reply_tags(pubkey, from)
+ const created_at = Math.floor(new Date().getTime() / 1000)
+ let kind = from.kind
+
+ // convert emoji replies into reactions
+ if (is_valid_reaction_content(content))
+ kind = 7
+
+ let reply = { pubkey, tags, content, created_at, kind }
+
+ reply.id = await nostrjs.calculate_id(reply)
+ reply = await sign_event(reply)
+ return reply
+}
+
+async function send_reply(content, replying_to) {
+ const ev = DAMUS.all_events[replying_to]
+ if (!ev)
+ return
+
+ const pubkey = await get_pubkey()
+ let reply = await create_reply(pubkey, content, ev)
+
+ broadcast_event(reply)
+ broadcast_related_events(reply)
+}
+
+async function create_deletion_event(pubkey, target, content="") {
+ const created_at = Math.floor(new Date().getTime() / 1000)
+ let kind = 5
+
+ const tags = [["e", target]]
+ let del = { pubkey, tags, content, created_at, kind }
+
+ del.id = await nostrjs.calculate_id(del)
+ del = await sign_event(del)
+ return del
+}
+
+async function delete_post(id, reason) {
+ const ev = DAMUS.all_events[id]
+ if (!ev)
+ return
+
+ const pubkey = await get_pubkey()
+ let del = await create_deletion_event(pubkey, id, reason)
+ broadcast_event(del)
+}
+
+function get_reactions(model, evid) {
+ const reactions_set = model.reactions_to[evid]
+ if (!reactions_set)
+ return ""
+
+ let reactions = []
+ for (const id of reactions_set.keys()) {
+ if (is_deleted(model, id))
+ continue
+ const reaction = model.all_events[id]
+ if (!reaction)
+ continue
+ reactions.push(reaction)
+ }
+
+ const groups = reactions.reduce((grp, r) => {
+ const e = get_reaction_emoji(r)
+ grp[e] = grp[e] || {}
+ grp[e][r.pubkey] = r
+ return grp
+ }, {})
+
+ return groups
+}
+
+function gather_reply_tags(pubkey, from) {
+ let tags = []
+ let ids = new Set()
+
+ if (from.refs && from.refs.root) {
+ tags.push(["e", from.refs.root, "", "root"])
+ ids.add(from.refs.root)
+ }
+
+ tags.push(["e", from.id, "", "reply"])
+ ids.add(from.id)
+
+ for (const tag of from.tags) {
+ if (tag.length >= 2) {
+ if (tag[0] === "p" && tag[1] !== pubkey) {
+ if (!ids.has(tag[1])) {
+ tags.push(["p", tag[1]])
+ ids.add(tag[1])
+ }
+ }
+ }
+ }
+ if (from.pubkey !== pubkey && !ids.has(from.pubkey)) {
+ tags.push(["p", from.pubkey])
+ }
+ return tags
+}
+
+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
+}
+
+function* yield_etags(tags) {
+ for (const tag of tags) {
+ if (tag.length >= 2 && tag[0] === "e")
+ yield tag
+ }
+}
+
+function get_content_warning(tags) {
+ for (const tag of tags) {
+ if (tag.length >= 1 && tag[0] === "content-warning")
+ return tag[1] || ""
+ }
+ return null
+}
+
+async function get_nip05_pubkey(email) {
+ const [user, host] = email.split("@")
+ const url = `https://${host}/.well-known/nostr.json?name=${user}`
+
+ try {
+ const res = await fetch(url)
+ const json = await res.json()
+ log_debug("nip05 data", json)
+ return json.names[user]
+ } catch (e) {
+ log_error("fetching nip05 entry for %s", email, e)
+ throw e
+ }
+}
+
+// TODO rename handle_pubkey to fetch_pubkey
+async function handle_pubkey(pubkey) {
+ if (pubkey[0] === "n")
+ pubkey = bech32_decode(pubkey)
+ if (pubkey.includes("@"))
+ pubkey = await get_nip05_pubkey(pubkey)
+ set_local_state('pubkey', pubkey)
+ return pubkey
+}
+
diff --git a/web/js/damus.js b/web/js/damus.js
index aafb74a..33243b5 100644
--- a/web/js/damus.js
+++ b/web/js/damus.js
@@ -1,4 +1,3 @@
-
let DAMUS
const BOOTSTRAP_RELAYS = [
@@ -12,22 +11,102 @@ const DEFAULT_PROFILE = {
display_name: "Anonymous",
}
+async function damus_web_init() {
+ init_message_textareas();
+ let tries = 0;
+ function init() {
+ // only wait for 500ms max
+ const max_wait = 500
+ const interval = 20
+ if (window.nostr || tries >= (max_wait/interval)) {
+ console.info("init after", tries);
+ damus_web_init_ready();
+ return;
+ }
+ tries++;
+ setTimeout(init, interval);
+ }
+ init();
+}
+
+async function damus_web_init_ready() {
+ const model = init_home_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(),
+ notifications: "notifications",//uuidv4(),
+ unknowns: "unknowns",//uuidv4(),
+ dms: "dms",//uuidv4(),
+ }
+
+ model.ids = ids
+ model.pool = pool
+ load_cache(model)
+ model.view_el = document.querySelector("#view")
+
+ switch_view('home')
+ 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)
+ }
+ })
+ 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
+ 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 init_contacts() {
@@ -83,187 +162,50 @@ function init_home_model() {
}
}
-function update_favicon(path)
-{
- let link = document.querySelector("link[rel~='icon']");
- const head = document.getElementsByTagName('head')[0]
-
- if (!link) {
- link = document.createElement('link');
- link.rel = 'icon';
- head.appendChild(link);
- }
-
- link.href = path;
-}
-
-// update_title updates the document title & visual indicators based on if the
-// number of notifications that are unseen by the user.
-function update_title(model) {
- // TODO rename update_title to update_notification_state or similar
- // TODO only clear notifications once they have seen all targeted events
- if (document.visibilityState === 'visible') {
- model.notifications = 0
- }
-
- const num = model.notifications
- const has_notes = num !== 0
- document.title = has_notes ? `(${num}) Damus` : "Damus";
- update_favicon(has_notes ? "img/damus_notif.svg" : "img/damus.svg");
- update_notification_markers(has_notes)
-}
-
-function notice_chatroom(state, id)
-{
+function notice_chatroom(state, id) {
if (!state.chatrooms[id])
state.chatrooms[id] = {}
}
-async function damus_web_init()
-{
- init_message_textareas();
-
- let tries = 0;
- function init() {
- // only wait for 500ms max
- const max_wait = 500
- const interval = 20
- if (window.nostr || tries >= (max_wait/interval)) {
- console.info("init after", tries);
- damus_web_init_ready();
- return;
- }
- tries++;
- setTimeout(init, interval);
- }
- init();
-}
-
-async function damus_web_init_ready()
-{
- const model = init_home_model()
- DAMUS = model
- model.pubkey = await get_pubkey()
- if (!model.pubkey)
- return
- const {RelayPool} = nostrjs
- const pool = RelayPool(BOOTSTRAP_RELAYS)
- const now = (new Date().getTime()) / 1000
-
- const ids = {
- 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(),
- }
-
- model.ids = ids
-
- model.pool = pool
-
- load_cache(model)
- model.view_el = document.querySelector("#view")
-
- switch_view('home')
-
- 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)
- }
- })
-
- return pool
-}
-
-function process_reaction_event(model, ev)
-{
+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)
-{
- try {
- model.chatrooms[ev.id] = JSON.parse(ev.content)
- } catch (err) {
- log_debug("error processing chatroom creation event", ev, err)
- }
+function process_chatroom_event(model, ev) {
+ model.chatrooms[ev.id] = safe_parse_json(ev.content,
+ "chatroom create event");
}
-function process_json_content(ev)
-{
- try {
- ev.json_content = JSON.parse(ev.content)
- } catch(e) {
- log_debug("error parsing json content for", ev)
- }
+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_deletion_event(model, 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
@@ -273,8 +215,8 @@ function process_deletion_event(model, ev)
}
}
-function is_deleted(model, evid)
-{
+// 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]
@@ -305,7 +247,6 @@ function is_deleted(model, evid)
log_debug(`User ${d_ev.pubkey} tried to delete ${ev.pubkey}'s event ... what?`)
}
}
-
return false
}
@@ -317,25 +258,15 @@ function has_event(damus, evid) {
return evid in damus.all_events
}
-const ID_REG = /^[a-f0-9]{64}$/
-function is_valid_id(evid)
-{
- return ID_REG.test(evid)
-}
-
-function make_unk(hint, ev)
-{
+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)
-{
+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]
@@ -367,19 +298,16 @@ function notice_unknown_ids(damus, ev)
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)
}
@@ -425,43 +353,26 @@ function fetch_unknown_events(damus)
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 shuffle(arr)
-{
- let i = arr.length;
- while (--i > 0) {
- let randIndex = Math.floor(Math.random() * (i + 1));
- [arr[randIndex], arr[i]] = [arr[i], arr[randIndex]];
- }
- return arr;
-}
-
-
function schedule_unknown_refetch(damus)
{
const INTERVAL = 5000
@@ -561,16 +472,6 @@ function should_add_to_explore_timeline(contacts, view, ev, pow)
return passes_spam_filter(contacts, ev, pow)
}
-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 handle_redraw_logic(model, view_name)
{
const view = model.views[view_name]
@@ -586,21 +487,7 @@ function schedule_save_events(damus)
damus.save_timer = setTimeout(save_cache.bind(null, damus), 3000)
}
-function is_valid_time(now_sec, created_at)
-{
- // don't count events far in the future
- if (created_at - now_sec >= 120) {
- return false
- }
- return true
-}
-
-function max(a, b) {
- return a > b ? a : b
-}
-
-function calculate_last_of_kind(evs)
-{
+function calculate_last_of_kind(evs) {
const now_sec = new Date().getTime() / 1000
return Object.keys(evs).reduce((obj, evid) => {
const ev = evs[evid]
@@ -612,8 +499,38 @@ function calculate_last_of_kind(evs)
}, {})
}
-function load_events(damus)
-{
+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'))
@@ -625,14 +542,12 @@ function load_events(damus)
}, {})
}
-function load_cache(damus)
-{
+function load_cache(damus) {
damus.all_events = load_events(damus)
load_timelines(damus)
}
-function save_cache(damus)
-{
+function save_cache(damus) {
save_events(damus)
save_timelines(damus)
}
@@ -775,15 +690,11 @@ 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}
@@ -796,15 +707,14 @@ function send_home_filters(model, relay) {
if (relay) {
last_of_kind =
model.last_event_of_kind[relay] =
- model.last_event_of_kind[relay] || calculate_last_of_kind(model.all_events)
-
+ 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)
+ 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)
@@ -833,9 +743,8 @@ function update_filter_with_since(last_of_kind, filter) {
return since < earliest ? since : earliest
}, initial)
-
if (earliest)
- filter.since = earliest
+ filter.since = earliest;
}
function update_filters_with_since(last_of_kind, filters) {
@@ -844,127 +753,7 @@ function update_filters_with_since(last_of_kind, filters) {
}
}
-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 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 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 get_view_el(name)
-{
- return DAMUS.view_el.querySelector(`#${name}-view`)
-}
-
-function switch_view(name, opts={})
-{
- if (name === DAMUS.current_view) {
- log_debug("Not switching to '%s', we are already there", name)
- return
- }
-
- 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 load_our_relays(our_pubkey, pool, ev) {
- if (ev.pubkey != our_pubkey)
- return
-
- let relays
- try {
- relays = JSON.parse(ev.content)
- } catch (e) {
- log_debug("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 handle_profiles_loaded(ids, model, view, relay) {
// stop asking for profiles
@@ -989,32 +778,8 @@ function handle_profiles_loaded(ids, model, view, relay) {
model.pool.subscribe(ids.explore, explore_filters, relay)
}
-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")
- if (!force && el.dataset.loaded) return;
- el.dataset.loaded = true;
- el.innerHTML = html;
-}
-
-function debounce(f, interval) {
- let timer = null;
- let first = true;
-
- return (...args) => {
- clearTimeout(timer);
- return new Promise((resolve) => {
- timer = setTimeout(() => resolve(f(...args)), first? 0 : interval);
- first = false
- });
- };
-}
-
// load profiles after comment notes are loaded
-function handle_comments_loaded(ids, model, events, relay)
-{
+function handle_comments_loaded(ids, model, events, relay) {
const pubkeys = events.reduce((s, ev) => {
s.add(ev.pubkey)
for (const tag of ev.tags) {
@@ -1031,10 +796,8 @@ function handle_comments_loaded(ids, model, events, relay)
const profile_filter = {kinds: [0,3], authors: authors}
let filters = []
-
if (authors.length > 0)
filters.push(profile_filter)
-
if (filters.length === 0) {
log_debug("No profiles filters to request...")
return
@@ -1045,64 +808,6 @@ function handle_comments_loaded(ids, model, events, relay)
model.pool.subscribe(ids.profiles, filters, relay)
}
-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 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()
- }
-}
-
-async function send_post() {
- const input_el = document.querySelector("#post-input")
- const cw_el = document.querySelector("#content-warning-input")
-
- const cw = cw_el.value
- const content = input_el.value
- const created_at = Math.floor(new Date().getTime() / 1000)
- const kind = 1
- const tags = cw ? [["content-warning", cw]] : []
- const pubkey = await get_pubkey()
- const {pool} = DAMUS
-
- let post = { pubkey, tags, content, created_at, kind }
-
- post.id = await nostrjs.calculate_id(post)
- post = await sign_event(post)
-
- pool.send(["EVENT", post])
-
- input_el.value = ""
- cw_el.value = ""
- post_input_changed(input_el)
-}
-
-async function sign_event(ev) {
- if (window.nostr && window.nostr.signEvent) {
- const signed = await window.nostr.signEvent(ev)
- if (typeof signed === 'string') {
- ev.sig = signed
- return ev
- }
- return signed
- }
-
- const privkey = get_privkey()
- ev.sig = await sign_id(privkey, ev.id)
- return ev
-}
-
function determine_event_refs_positionally(pubkeys, ids)
{
if (ids.length === 1)
@@ -1148,462 +853,46 @@ function determine_event_refs(tags) {
return {root, reply, pubkeys}
}
-function* yield_etags(tags)
-{
- for (const tag of tags) {
- if (tag.length >= 2 && tag[0] === "e")
- yield tag
- }
+function contacts_friend_list(contacts) {
+ return Array.from(contacts.friends)
}
-function expand_thread(id, reply_id) {
- const view = get_current_view()
- const root_id = get_thread_root_id(DAMUS, id)
- if (!root_id) {
- log_debug("could not get root_id for", DAMUS.all_events[id])
- return
+function contacts_friendosphere(contacts) {
+ let s = new Set()
+ let fs = []
+
+ for (const friend of contacts.friends.keys()) {
+ fs.push(friend)
+ s.add(friend)
}
- 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]
- if (!ev) {
- log_debug("expand_thread: no event found?", id)
- return null
+ for (const friend of contacts.friend_of_friends.keys()) {
+ if (!s.has(friend))
+ fs.push(friend)
}
- return ev.refs && ev.refs.root
+ return fs
}
-function get_default_max_depth(damus, view)
-{
- return view.max_depth || damus.max_depth
-}
-
-function get_thread_max_depth(damus, view, root_id)
-{
- if (!view.depths[root_id])
- return get_default_max_depth(damus, view)
-
- return view.depths[root_id]
-}
-
-function delete_post_confirm(evid) {
- if (!confirm("Are you sure you want to delete this post?"))
+function add_contact_if_friend(contacts, ev) {
+ if (!contact_is_friend(contacts, ev.pubkey))
return
- const reason = (prompt("Why you are deleting this? Leave empty to not specify. Type CANCEL to cancel.") || "").trim()
-
- if (reason.toLowerCase() === "cancel")
- return
-
- delete_post(evid, reason)
+ add_friend_contact(contacts, ev)
}
-function shouldnt_render_event(our_pk, view, ev, opts) {
- return !opts.is_composing &&
- !view.expanded.has(ev.id) &&
- view.rendered.has(ev.id)
+function contact_is_friend(contacts, pk) {
+ return contacts.friends.has(pk)
}
-function press_logout() {
- if (confirm("Are you sure you want to sign out?")) {
- localStorage.clear();
- const url = new URL(location.href)
- url.searchParams.delete("pk")
- window.location.href = url.toString()
- }
-}
+function add_friend_contact(contacts, contact) {
+ contacts.friends.add(contact.pubkey)
-async function delete_post(id, reason)
-{
- const ev = DAMUS.all_events[id]
- if (!ev)
- return
-
- const pubkey = await get_pubkey()
- let del = await create_deletion_event(pubkey, id, reason)
- console.log("deleting", ev)
- broadcast_event(del)
-}
-
-function get_reactions(model, evid)
-{
- const reactions_set = model.reactions_to[evid]
- if (!reactions_set)
- return ""
-
- let reactions = []
- for (const id of reactions_set.keys()) {
- if (is_deleted(model, id))
- continue
- const reaction = model.all_events[id]
- if (!reaction)
- continue
- reactions.push(reaction)
- }
-
- const groups = reactions.reduce((grp, r) => {
- const e = get_reaction_emoji(r)
- grp[e] = grp[e] || {}
- grp[e][r.pubkey] = r
- return grp
- }, {})
-
- return groups
-}
-
-function close_reply() {
- const modal = document.querySelector("#reply-modal")
- modal.classList.add("closed");
-}
-
-function gather_reply_tags(pubkey, from) {
- let tags = []
- let ids = new Set()
-
- if (from.refs && from.refs.root) {
- tags.push(["e", from.refs.root, "", "root"])
- ids.add(from.refs.root)
- }
-
- tags.push(["e", from.id, "", "reply"])
- ids.add(from.id)
-
- for (const tag of from.tags) {
- if (tag.length >= 2) {
- if (tag[0] === "p" && tag[1] !== pubkey) {
- if (!ids.has(tag[1])) {
- tags.push(["p", tag[1]])
- ids.add(tag[1])
- }
- }
+ 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])
}
}
- if (from.pubkey !== pubkey && !ids.has(from.pubkey)) {
- tags.push(["p", from.pubkey])
- }
- return tags
-}
-
-async function create_deletion_event(pubkey, target, content="")
-{
- const created_at = Math.floor(new Date().getTime() / 1000)
- let kind = 5
-
- const tags = [["e", target]]
- let del = { pubkey, tags, content, created_at, kind }
-
- del.id = await nostrjs.calculate_id(del)
- del = await sign_event(del)
- return del
-}
-
-async function create_reply(pubkey, content, from) {
- const tags = gather_reply_tags(pubkey, from)
- const created_at = Math.floor(new Date().getTime() / 1000)
- let kind = from.kind
-
- // convert emoji replies into reactions
- if (is_valid_reaction_content(content))
- kind = 7
-
- let reply = { pubkey, tags, content, created_at, kind }
-
- reply.id = await nostrjs.calculate_id(reply)
- reply = await sign_event(reply)
- return reply
-}
-
-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
-}
-
-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
- insert_event_sorted(evs, ev) // for uniqueness
- 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) {
- DAMUS.pool.send(["EVENT", ev])
-}
-
-async function send_reply(content, replying_to)
-{
- const ev = DAMUS.all_events[replying_to]
- if (!ev)
- return
-
- const pubkey = await get_pubkey()
- let reply = await create_reply(pubkey, content, ev)
-
- broadcast_event(reply)
- broadcast_related_events(reply)
-}
-
-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()
-}
-
-function get_local_state(key) {
- if (DAMUS[key] != null)
- return DAMUS[key]
-
- return localStorage.getItem(key)
-}
-
-function set_local_state(key, val) {
- DAMUS[key] = val
- localStorage.setItem(key, val)
-}
-
-function get_qs(loc=location.href) {
- return new URL(loc).searchParams
-}
-
-async function get_nip05_pubkey(email) {
- const [user, host] = email.split("@")
- const url = `https://${host}/.well-known/nostr.json?name=${user}`
-
- try {
- const res = await fetch(url)
- const json = await res.json()
-
- log_debug("nip05 data", json)
- return json.names[user]
- } catch (e) {
- log_error("fetching nip05 entry for %s", email, e)
- throw e
- }
-}
-
-async function handle_pubkey(pubkey) {
- if (pubkey[0] === "n")
- pubkey = bech32_decode(pubkey)
-
- if (pubkey.includes("@"))
- pubkey = await get_nip05_pubkey(pubkey)
-
- set_local_state('pubkey', pubkey)
-
- return pubkey
-}
-
-async function get_pubkey() {
- let pubkey = get_local_state('pubkey')
-
- // qs pk overrides stored key
- const qs_pk = get_qs().get("pk")
- if (qs_pk)
- return await handle_pubkey(qs_pk)
-
- if (pubkey)
- return pubkey
-
- console.log("window.nostr", window.nostr)
- if (window.nostr && window.nostr.getPublicKey) {
- console.log("calling window.nostr.getPublicKey()...")
- const pubkey = await window.nostr.getPublicKey()
- console.log("got %s pubkey from nos2x", pubkey)
- return await handle_pubkey(pubkey)
- }
-
- pubkey = prompt("Enter nostr id (eg: jb55@jb55.com) or pubkey (hex or npub)")
-
- if (!pubkey)
- throw new Error("Need pubkey to continue")
-
- return await handle_pubkey(pubkey)
-}
-
-function get_privkey() {
- let privkey = get_local_state('privkey')
-
- if (privkey)
- return privkey
-
- if (!privkey)
- privkey = prompt("Enter private key")
-
- if (!privkey)
- throw new Error("can't get privkey")
-
- if (privkey[0] === "n") {
- privkey = bech32_decode(privkey)
- }
-
- set_local_state('privkey', privkey)
-
- return privkey
-}
-
-async function sign_id(privkey, id)
-{
- //const digest = nostrjs.hex_decode(id)
- const sig = await nobleSecp256k1.schnorr.sign(id, privkey)
- return nostrjs.hex_encode(sig)
-}
-
-function reply_to(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})
-
- replybox.focus()
-}
-
-function convert_quote_blocks(content, show_media)
-{
- const split = content.split("\n")
- let blockin = false
- return split.reduce((str, line) => {
- if (line !== "" && line[0] === '>') {
- if (!blockin) {
- str += ""
- blockin = true
- }
- str += linkify(sanitize(line.slice(1)), show_media)
- } else {
- if (blockin) {
- blockin = false
- str += ""
- }
- str += linkify(sanitize(line), show_media)
- }
- return str + "
"
- }, "")
-}
-
-function get_content_warning(tags)
-{
- for (const tag of tags) {
- if (tag.length >= 1 && tag[0] === "content-warning")
- return tag[1] || ""
- }
-
- return null
-}
-
-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 format_content(ev, show_media)
-{
- if (ev.kind === 7) {
- if (ev.content === "" || ev.content === "+")
- 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"
- if (cw === "") {
- cwHTML += "."
- } else {
- cwHTML += `: "${cw}".`
- }
- const open = !!DAMUS.cw_open[ev.id]? "open" : ""
- return `
-
- ${cwHTML}
- ${body}
-
- `
- }
-
- return body
-}
-
-function sanitize(content)
-{
- if (!content)
- return ""
- return content.replaceAll("<","<").replaceAll(">",">")
-}
-
-function robohash(pk) {
- return "https://robohash.org/" + pk
-}
-
-function get_picture(pk, profile) {
- if (!profile)
- return robohash(pk)
- if (profile.resolved_picture)
- return profile.resolved_picture
- profile.resolved_picture = sanitize(profile.picture) || robohash(pk)
- 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
}
diff --git a/web/js/ui/fmt.js b/web/js/ui/fmt.js
new file mode 100644
index 0000000..be92714
--- /dev/null
+++ b/web/js/ui/fmt.js
@@ -0,0 +1,74 @@
+function linkify(text, show_media) {
+ return text.replace(URL_REGEX, function(match, p1, p2, p3) {
+ const url = p2+p3
+ const parsed = new URL(url)
+ let html;
+ if (show_media && is_img_url(parsed.pathname)) {
+ html = `
+
+ `;
+ } else if (show_media && is_video_url(parsed.pathname)) {
+ html = `
+
+
+
+ `;
+ } else {
+ html = `${url}`;
+ }
+ return p1+html;
+ })
+}
+
+function format_content(ev, show_media) {
+ if (ev.kind === 7) {
+ if (ev.content === "" || ev.content === "+")
+ 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"
+ if (cw === "") {
+ cwHTML += "."
+ } else {
+ cwHTML += `: "${cw}".`
+ }
+ const open = !!DAMUS.cw_open[ev.id]? "open" : ""
+ return `
+
+ ${cwHTML}
+ ${body}
+
+ `
+ }
+
+ return body
+}
+
+function convert_quote_blocks(content, show_media)
+{
+ const split = content.split("\n")
+ let blockin = false
+ return split.reduce((str, line) => {
+ if (line !== "" && line[0] === '>') {
+ if (!blockin) {
+ str += ""
+ blockin = true
+ }
+ str += linkify(sanitize(line.slice(1)), show_media)
+ } else {
+ if (blockin) {
+ blockin = false
+ str += ""
+ }
+ str += linkify(sanitize(line), show_media)
+ }
+ return str + "
"
+ }, "")
+}
+
diff --git a/web/js/ui/render.js b/web/js/ui/render.js
index 3e2b477..b7dd500 100644
--- a/web/js/ui/render.js
+++ b/web/js/ui/render.js
@@ -14,7 +14,6 @@ function render_timeline_event(damus, view, ev)
}
function render_events(damus, view) {
- log_debug("rendering events")
return view.events
.filter((ev, i) => i < 140)
.map((ev) => render_timeline_event(damus, view, ev)).join("\n")
@@ -350,3 +349,4 @@ function render_loading_spinner()
`
}
+
diff --git a/web/js/ui/state.js b/web/js/ui/state.js
new file mode 100644
index 0000000..cbaa378
--- /dev/null
+++ b/web/js/ui/state.js
@@ -0,0 +1,116 @@
+function get_view_el(name) {
+ return DAMUS.view_el.querySelector(`#${name}-view`)
+}
+
+function get_default_max_depth(damus, view) {
+ return view.max_depth || damus.max_depth
+}
+
+function get_thread_max_depth(damus, view, root_id) {
+ if (!view.depths[root_id])
+ return get_default_max_depth(damus, view)
+
+ 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) {
+ const view = get_current_view()
+ const root_id = get_thread_root_id(DAMUS, id)
+ if (!root_id) {
+ log_debug("could not get root_id for", DAMUS.all_events[id])
+ return
+ }
+ 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]
+ if (!ev) {
+ log_debug("expand_thread: no event found?", id)
+ return null
+ }
+ 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 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 switch_view(name, opts={})
+{
+ if (name === DAMUS.current_view) {
+ log_debug("Not switching to '%s', we are already there", name)
+ return
+ }
+
+ 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]
+}
+
diff --git a/web/js/ui/util.js b/web/js/ui/util.js
index 5e2cac8..1502c15 100644
--- a/web/js/ui/util.js
+++ b/web/js/ui/util.js
@@ -129,3 +129,136 @@ function open_media_preview(url, type) {
function close_media_preview() {
find_node("#media-preview").classList.add("closed");
}
+
+function close_reply() {
+ const modal = document.querySelector("#reply-modal")
+ modal.classList.add("closed");
+}
+
+function press_logout() {
+ if (confirm("Are you sure you want to sign out?")) {
+ localStorage.clear();
+ const url = new URL(location.href)
+ url.searchParams.delete("pk")
+ window.location.href = url.toString()
+ }
+}
+
+function delete_post_confirm(evid) {
+ if (!confirm("Are you sure you want to delete this post?"))
+ return
+
+ const reason = (prompt("Why you are deleting this? Leave empty to not specify. Type CANCEL to cancel.") || "").trim()
+
+ if (reason.toLowerCase() === "cancel")
+ 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()
+}
+
+function reply_to(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})
+
+ replybox.focus()
+}
+
+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")
+ if (!force && el.dataset.loaded) return;
+ el.dataset.loaded = true;
+ el.innerHTML = html;
+}
+
+function update_favicon(path)
+{
+ let link = document.querySelector("link[rel~='icon']");
+ const head = document.getElementsByTagName('head')[0]
+
+ if (!link) {
+ link = document.createElement('link');
+ link.rel = 'icon';
+ head.appendChild(link);
+ }
+
+ link.href = path;
+}
+
+// update_title updates the document title & visual indicators based on if the
+// number of notifications that are unseen by the user.
+function update_title(model) {
+ // TODO rename update_title to update_notification_state or similar
+ // TODO only clear notifications once they have seen all targeted events
+ if (document.visibilityState === 'visible') {
+ model.notifications = 0
+ }
+
+ const num = model.notifications
+ const has_notes = num !== 0
+ document.title = has_notes ? `(${num}) Damus` : "Damus";
+ update_favicon(has_notes ? "img/damus_notif.svg" : "img/damus.svg");
+ update_notification_markers(has_notes)
+}
+
+async function get_pubkey() {
+ let pubkey = get_local_state('pubkey')
+ // qs pk overrides stored key
+ const qs_pk = get_qs().get("pk")
+ if (qs_pk)
+ return await handle_pubkey(qs_pk)
+ if (pubkey)
+ return pubkey
+ if (window.nostr && window.nostr.getPublicKey) {
+ console.log("calling window.nostr.getPublicKey()...")
+ const pubkey = await window.nostr.getPublicKey()
+ console.log("got %s pubkey from nos2x", pubkey)
+ return await handle_pubkey(pubkey)
+ }
+ pubkey = prompt("Enter nostr id (eg: jb55@jb55.com) or pubkey (hex or npub)")
+ if (!pubkey)
+ throw new Error("Need pubkey to continue")
+ return await handle_pubkey(pubkey)
+}
+
+function get_privkey() {
+ let privkey = get_local_state('privkey')
+ if (privkey)
+ return privkey
+ if (!privkey)
+ privkey = prompt("Enter private key")
+ if (!privkey)
+ throw new Error("can't get privkey")
+ if (privkey[0] === "n") {
+ privkey = bech32_decode(privkey)
+ }
+ set_local_state('privkey', privkey)
+ return privkey
+}
+
+
diff --git a/web/js/util.js b/web/js/util.js
index af45d2f..d5e8582 100644
--- a/web/js/util.js
+++ b/web/js/util.js
@@ -1,3 +1,9 @@
+const ID_REG = /^[a-f0-9]{64}$/
+const WORD_REGEX=/\w/
+const IMG_REGEX = /(png|jpeg|jpg|gif|webp)$/i
+const VID_REGEX = /(webm|mp4)$/i
+const URL_REGEX = /(^|\s)(https?:\/\/[^\s]+)[,:)]?(\w|$)/g;
+const REACTION_REGEX = /^[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC3\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC08\uDC26](?:\u200D\u2B1B)?|[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC2\uDECE-\uDEDB\uDEE0-\uDEE8]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)$/
function log_error(fmt, ...args) {
console.error(fmt, ...args)
@@ -7,6 +13,60 @@ function log_debug(fmt, ...args) {
console.debug(fmt, ...args)
}
+function log_info(fmt, ...args) {
+ console.info(fmt, ...args)
+}
+
+function safe_parse_json(data, message) {
+ let value = undefined;
+ try {
+ value = JSON.parse(data);
+ } catch (e) {
+ log_error(`${message} : unable to parse JSON`, err, data);
+ }
+ return value;
+}
+
+function max(a, b) {
+ return a > b ? a : b
+}
+
+/* min returns the minimum value of a or b
+ */
+function min(a, b) {
+ return a < b ? a : b;
+}
+
+/* shuffle shuffles an Array
+ * TODO rename to shuffle_array
+ */
+function shuffle(arr) {
+ let i = arr.length;
+ while (--i > 0) {
+ let randIndex = Math.floor(Math.random() * (i + 1));
+ [arr[randIndex], arr[i]] = [arr[i], arr[randIndex]];
+ }
+ return arr;
+}
+
+function is_valid_time(now_sec, created_at) {
+ // don't count events far in the future
+ if (created_at - now_sec >= 120) {
+ return false
+ }
+ return true
+}
+
+function new_creation_time() {
+ return Math.floor(new Date().getTime() / 1000)
+}
+
+function get_since_time(created_at) {
+ if (created_at == null)
+ return null
+ return created_at - 60 * 10
+}
+
/* find_node is a short name for document.querySelector, it also takes in a
* parent element to search on.
*/
@@ -37,12 +97,6 @@ function bech32_decode(pubkey) {
return nostrjs.hex_encode(bytes)
}
-function get_since_time(created_at) {
- if (created_at == null)
- return null
- return created_at - 60 * 10
-}
-
function zero_bits(b) {
let n = 0;
if (b == 0) return 8;
@@ -65,12 +119,6 @@ function leading_zero_bits(id) {
return total
}
-/* min returns the minimum value of a or b
- */
-function min(a, b) {
- return a < b ? a : b;
-}
-
function difficulty_to_prefix(d) {
const n = Math.floor(d / 4)
let s = ""
@@ -140,6 +188,16 @@ function time_delta(current, previous) {
}
}
+function sanitize(content) {
+ if (!content)
+ return ""
+ return content.replaceAll("<","<").replaceAll(">",">")
+}
+
+function robohash(str) {
+ return "https://robohash.org/" + str
+}
+
function is_valid_reaction_content(content) {
return content === "+" || content === "" || is_emoji(content)
}
@@ -151,43 +209,51 @@ function get_reaction_emoji(ev) {
return ev.content
}
-const IMG_REGEX = /(png|jpeg|jpg|gif|webp)$/i
function is_img_url(path) {
return IMG_REGEX.test(path)
}
-const VID_REGEX = /(webm|mp4)$/i
function is_video_url(path) {
return VID_REGEX.test(path)
}
-const URL_REGEX = /(^|\s)(https?:\/\/[^\s]+)[,:)]?(\w|$)/g;
-function linkify(text, show_media) {
- return text.replace(URL_REGEX, function(match, p1, p2, p3) {
- const url = p2+p3
- const parsed = new URL(url)
- let html;
- if (show_media && is_img_url(parsed.pathname)) {
- html = `
-
- `;
- } else if (show_media && is_video_url(parsed.pathname)) {
- html = `
-
-
-
- `;
- } else {
- html = `${url}`;
- }
- return p1+html;
- })
-}
-
-const WORD_REGEX=/\w/
function is_emoji(str) {
return !WORD_REGEX.test(str) && REACTION_REGEX.test(str)
}
-const REACTION_REGEX = /^[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC3\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC08\uDC26](?:\u200D\u2B1B)?|[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC2\uDECE-\uDEDB\uDEE0-\uDEE8]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)$/
+function is_valid_id(evid)
+{
+ return ID_REG.test(evid)
+}
+
+function get_qs(loc=location.href) {
+ return new URL(loc).searchParams
+}
+
+function get_picture(pk, profile) {
+ if (!profile)
+ return robohash(pk)
+ if (profile.resolved_picture)
+ return profile.resolved_picture
+ profile.resolved_picture = sanitize(profile.picture) || robohash(pk)
+ 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;
+ return (...args) => {
+ clearTimeout(timer);
+ return new Promise((resolve) => {
+ timer = setTimeout(() => resolve(f(...args)), first ? 0 : interval);
+ first = false
+ });
+ };
+}