diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index af2e08a..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 30021f0..1fa18c4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ TODO.bak *.mp4 channels/index.html node_modules +.DS_Store diff --git a/web/Makefile b/web/Makefile index eed71f9..775251c 100644 --- a/web/Makefile +++ b/web/Makefile @@ -11,8 +11,4 @@ emojiregex: fake dist: rsync -avzP ./ charon:/www/damus.io/web/ -distv2: - rsync -avzP ./ charon:/www/damus.io/webv2/ - - .PHONY: fake diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..280d0d9 --- /dev/null +++ b/web/README.md @@ -0,0 +1,37 @@ +# Damus PWA + +Here lies the code for the Damus web app, a client for the Nostr protocol. The +goal of this client is to be a better version of Twitter, but not to reproduce +all of it's functionality. + +## Contribution Guide + +There are rules to contributing to this client. Please ensure you read them +before making changes and supplying patch notes. + +1. No transpilers. All source code should work out of the box. +2. Keep source code organised. Refer to the folder structure. If you have a + question, ask it. +3. Do not include your personal tools in the source code. Use your own scripts + outside of the project. This does not include build tools such as Make. +4. Spaces, no tabs. +5. Do not include binary files. +6. No NPM (and kin) environments. If you need a file from an external resource + mark the location in the "sources" file and add it to the repo. +7. Do not write code using experimental browser APIs. +8. Do not write animations in JavaScript, CSS only. Keep them short and snappy. + Animations should not be a forefront, but an enjoyable addition. +9. All new & modified code should be properly documented. +10. Source code should be readable in the browser. + +TODO Write about code style requirements & add number of spaces. + +## Style Guide + +TODO Write about the style guide. + +## Terminology + + * Sign Out - Not log out, logout, log off, etc. + * Sign In - Not Login, Log In, etc. + diff --git a/web/damus.css b/web/css/damus.css similarity index 100% rename from web/damus.css rename to web/css/damus.css diff --git a/web/fontawesome.css b/web/css/fontawesome.css similarity index 100% rename from web/fontawesome.css rename to web/css/fontawesome.css diff --git a/web/styles.css b/web/css/styles.css similarity index 100% rename from web/styles.css rename to web/css/styles.css diff --git a/web/webfonts/fa-brands-400.ttf b/web/css/webfonts/fa-brands-400.ttf similarity index 100% rename from web/webfonts/fa-brands-400.ttf rename to web/css/webfonts/fa-brands-400.ttf diff --git a/web/webfonts/fa-brands-400.woff2 b/web/css/webfonts/fa-brands-400.woff2 similarity index 100% rename from web/webfonts/fa-brands-400.woff2 rename to web/css/webfonts/fa-brands-400.woff2 diff --git a/web/webfonts/fa-regular-400.ttf b/web/css/webfonts/fa-regular-400.ttf similarity index 100% rename from web/webfonts/fa-regular-400.ttf rename to web/css/webfonts/fa-regular-400.ttf diff --git a/web/webfonts/fa-regular-400.woff2 b/web/css/webfonts/fa-regular-400.woff2 similarity index 100% rename from web/webfonts/fa-regular-400.woff2 rename to web/css/webfonts/fa-regular-400.woff2 diff --git a/web/webfonts/fa-solid-900.ttf b/web/css/webfonts/fa-solid-900.ttf similarity index 100% rename from web/webfonts/fa-solid-900.ttf rename to web/css/webfonts/fa-solid-900.ttf diff --git a/web/webfonts/fa-solid-900.woff2 b/web/css/webfonts/fa-solid-900.woff2 similarity index 100% rename from web/webfonts/fa-solid-900.woff2 rename to web/css/webfonts/fa-solid-900.woff2 diff --git a/web/webfonts/fa-v4compatibility.ttf b/web/css/webfonts/fa-v4compatibility.ttf similarity index 100% rename from web/webfonts/fa-v4compatibility.ttf rename to web/css/webfonts/fa-v4compatibility.ttf diff --git a/web/webfonts/fa-v4compatibility.woff2 b/web/css/webfonts/fa-v4compatibility.woff2 similarity index 100% rename from web/webfonts/fa-v4compatibility.woff2 rename to web/css/webfonts/fa-v4compatibility.woff2 diff --git a/web/index.html b/web/index.html index a8e78f2..119bdfe 100644 --- a/web/index.html +++ b/web/index.html @@ -4,72 +4,78 @@ Damus - - - + + + + + + + + + -
-
- +
+
+ +
+
+
+
+
+ diff --git a/web/index.js b/web/index.js deleted file mode 100644 index 1afee15..0000000 --- a/web/index.js +++ /dev/null @@ -1,111 +0,0 @@ - -async function damus_init() -{ - const relay = await Relay("wss://relay.damus.io") - const now = (new Date().getTime()) / 1000 - const el = document.querySelector("#content") - const model = {events: []} - - el.innerHTML = render_initial_content() - model.el = el.querySelector("#home") - - relay.subscribe("test_sub_id", {kinds: [1], limit: 20}) - relay.event = (sub_id, ev) => { - insert_event_sorted(model.events, ev) - if (model.realtime) - render_home_view(model) - } - - relay.eose = () => { - model.realtime = true - render_home_view(model) - } - - return relay -} - -function render_home_view(model) { - model.el.innerHTML = render_events(model.events) -} - -function render_initial_content() { - return `` -} - -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 render_events(evs) { - return evs.map(render_event).join("\n") -} - -function render_event(ev) { - return `
  • ${ev.content}
  • ` -} - -function Relay(relay, opts={}) -{ - if (!(this instanceof Relay)) - return new Relay(relay, opts) - - this.relay = relay - this.opts = opts - - const me = this - return new Promise((resolve, reject) => { - const ws = me.ws = new WebSocket(relay); - let resolved = false - ws.onmessage = (m) => { handle_message(me, m) } - ws.onclose = () => { me.close && me.close() } - ws.onerror = () => { me.error && me.error() } - ws.onopen = () => { - if (resolved) { - me.open.bind(me) - return - } - - resolved = true - resolve(me) - } - }) -} - -Relay.prototype.subscribe = function relay_subscribe(sub_id, ...filters) { - const tosend = ["REQ", sub_id, ...filters] - console.log("sending", tosend) - this.ws.send(JSON.stringify(tosend)) -} - -function handle_message(relay, msg) -{ - const data = JSON.parse(msg.data) - if (data.length >= 2) { - switch (data[0]) { - case "EVENT": - if (data.length < 3) - return - return relay.event && relay.event(data[1], data[2]) - case "EOSE": - return relay.eose && relay.eose(data[1]) - case "NOTICE": - return relay.note && relay.note(...data.slice(1)) - } - } -} - -const relay = damus_init() diff --git a/web/bech32.js b/web/js/bech32.js similarity index 100% rename from web/bech32.js rename to web/js/bech32.js diff --git a/web/damus.js b/web/js/damus.js similarity index 79% rename from web/damus.js rename to web/js/damus.js index 957fbe3..3f5d0c4 100644 --- a/web/damus.js +++ b/web/js/damus.js @@ -1,6 +1,12 @@ let DAMUS +const BOOTSTRAP_RELAYS = [ + "wss://relay.damus.io", + "wss://nostr-relay.wlvs.space", + "wss://nostr-pub.wellorder.net" +] + function uuidv4() { return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) @@ -54,12 +60,6 @@ function init_home_model() { } } -const BOOTSTRAP_RELAYS = [ - "wss://relay.damus.io", - "wss://nostr-relay.wlvs.space", - "wss://nostr-pub.wellorder.net" -] - function update_favicon(path) { let link = document.querySelector("link[rel~='icon']"); @@ -638,46 +638,11 @@ async function sign_event(ev) { return ev } -function render_home_view(model) { - return ` -
    -
    -
    - -
    - - - -
    -
    -
    -
    - ` -} - function post_input_changed(el) { document.querySelector("#post-button").disabled = el.value === "" } -function render_home_event(model, ev) -{ - let max_depth = 3 - if (ev.refs && ev.refs.root && model.expanded.has(ev.refs.root)) { - max_depth = null - } - - return render_event(model, ev, {max_depth}) -} - -function render_events(model) { - return model.events - .filter((ev, i) => i < 140) - .map((ev) => render_home_event(model, ev)).join("\n") -} - function determine_event_refs_positionally(pubkeys, ids) { if (ids.length === 1) @@ -716,15 +681,6 @@ function determine_event_refs(tags) { return {root, reply, pubkeys} } -function render_reply_line_top(has_top_line) { - const classes = has_top_line ? "" : "invisible" - return `
    ` -} - -function render_reply_line_bot() { - return `
    ` -} - function can_reply(ev) { return ev.kind === 1 || ev.kind === 42 } @@ -734,18 +690,6 @@ const DEFAULT_PROFILE = { display_name: "Anonymous", } -function render_thread_collapsed(model, reply_ev, opts) -{ - if (opts.is_composing) - return "" - return ` -
    -
    - More messages in thread available. Click to expand. -
    -
    ` -} - function* yield_etags(tags) { for (const tag of tags) { @@ -764,58 +708,6 @@ function expand_thread(id) { redraw_events(DAMUS) } -function render_replied_events(model, ev, opts) -{ - if (!(ev.refs && ev.refs.reply)) - return "" - - const reply_ev = model.all_events[ev.refs.reply] - if (!reply_ev) - return "" - - opts.replies = opts.replies == null ? 1 : opts.replies + 1 - if (!(opts.max_depth == null || opts.replies < opts.max_depth)) - return render_thread_collapsed(model, reply_ev, opts) - - opts.is_reply = true - return render_event(model, reply_ev, opts) -} - -function render_replying_to_chat(model, ev) { - const chatroom = (ev.refs.root && model.chatrooms[ev.refs.root]) || {} - const roomname = chatroom.name || ev.refs.root || "??" - const pks = ev.refs.pubkeys || [] - const names = pks.map(pk => render_mentioned_name(pk, model.profiles[pk])).join(", ") - const to_users = pks.length === 0 ? "" : ` to ${names}` - - return `
    replying${to_users} in ${roomname}
    ` -} - -function render_replying_to(model, ev) { - if (!(ev.refs && ev.refs.reply)) - return "" - - if (ev.kind === 42) - return render_replying_to_chat(model, ev) - - let pubkeys = ev.refs.pubkeys || [] - if (pubkeys.length === 0 && ev.refs.reply) { - const replying_to = model.all_events[ev.refs.reply] - if (!replying_to) - return `
    reply to ${ev.refs.reply}
    ` - - pubkeys = [replying_to.pubkey] - } - - const names = ev.refs.pubkeys.map(pk => render_mentioned_name(pk, model.profiles[pk])).join(", ") - - return ` - - replying to ${names} - - ` -} - function delete_post_confirm(evid) { if (!confirm("Are you sure you want to delete this post?")) return @@ -828,30 +720,6 @@ function delete_post_confirm(evid) { delete_post(evid, reason) } -function render_unknown_event(model, ev) { - return "Unknown event" -} - -function render_boost(model, ev, opts) { - //todo validate content - if (!ev.json_content) - return render_unknown_event(ev) - - //const profile = model.profiles[ev.pubkey] - opts.is_boost_event = true - opts.boosted = { - pubkey: ev.pubkey, - profile: model.profiles[ev.pubkey] - } - return render_event(model, ev.json_content, opts) - //return ` - //
    - //
    Reposted by ${render_name_plain(ev.pubkey, profile)}
    - //${render_event(model, ev.json_content, opts)} - //
    - //` -} - function shouldnt_render_event(model, ev, opts) { return !opts.is_boost_event && !opts.is_composing && @@ -859,58 +727,6 @@ function shouldnt_render_event(model, ev, opts) { model.rendered[ev.id] } -function render_deleted_name() { - return "???" -} - -function render_deleted_pfp() { - return `
    😵
    ` -} - -function render_comment_body(model, ev, opts) { - const can_delete = model.pubkey === ev.pubkey; - const bar = !can_reply(ev) || opts.nobar? "" : render_action_bar(ev, can_delete) - const show_media = !opts.is_composing - - return ` -
    - ${render_replying_to(model, ev)} - ${render_boosted_by(model, ev, opts)} -
    -

    - ${format_content(ev, show_media)} -

    - ${render_reactions(model, ev)} - ${bar} - ` -} - -function render_boosted_by(model, ev, opts) { - const b = opts.boosted - if (!b) { - return "" - } - // TODO encapsulate username as link/button! - return ` -
    Shared by - ${render_name_plain(b.pubkey, b.profile)} -
    - ` -} - -function render_deleted_comment_body(ev, deleted) { - if (deleted.content) { - const show_media = false - return ` -
    - This comment was deleted. Reason: -
    ${format_content(deleted, show_media)}
    -
    - ` - } - return `
    This comment was deleted
    ` -} - function press_logout() { if (confirm("Are you sure you want to logout?")) { localStorage.clear(); @@ -920,60 +736,6 @@ function press_logout() { } } -function render_event(model, ev, opts={}) { - if (ev.kind === 6) - return render_boost(model, ev, opts) - if (shouldnt_render_event(model, ev, opts)) - return "" - delete opts.is_boost_event - model.rendered[ev.id] = true - const profile = model.profiles[ev.pubkey] || DEFAULT_PROFILE - 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()) || "" - - const deleted = is_deleted(model, ev.id) - if (deleted && !opts.is_reply) - return "" - - const replied_events = render_replied_events(model, ev, opts) - - let name = "???" - if (!deleted) { - name = render_name_plain(ev.pubkey, profile) - } - - const has_top_line = replied_events !== "" - const border_bottom = has_bot_line ? "" : "bottom-border"; - return ` - ${replied_events} -
    -
    - ${render_reply_line_top(has_top_line)} - ${deleted ? render_deleted_pfp() : render_pfp(ev.pubkey, profile)} - ${reply_line_bot} -
    -
    -
    - - ${name} - - ${delta} -
    -
    - ${deleted ? render_deleted_comment_body(ev, deleted) : render_comment_body(model, ev, opts)} -
    -
    -
    - ` -} - -function render_pfp(pk, profile, size="normal") { - const name = render_name_plain(pk, profile) - return `` -} - 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])?))?)$/ const WORD_REGEX=/\w/ @@ -994,30 +756,6 @@ function get_reaction_emoji(ev) { return ev.content } -function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) { - const reaction = reactions[our_pubkey] - if (!reaction) { - return `onclick="send_reply('${emoji}', '${reacting_to}')"` - } else { - return `onclick="delete_post('${reaction.id}')"` - } -} - -function render_reaction_group(model, emoji, reactions, reacting_to) { - const pfps = Object.keys(reactions).map((pk) => render_reaction(model, reactions[pk])) - - let onclick = render_react_onclick(model.pubkey, reacting_to.id, emoji, reactions) - - return ` - - - ${emoji} - - ${pfps.join("\n")} - - ` -} - async function delete_post(id, reason) { const ev = DAMUS.all_events[id] @@ -1030,15 +768,6 @@ async function delete_post(id, reason) broadcast_event(del) } -function render_reaction(model, reaction) { - const profile = model.profiles[reaction.pubkey] || DEFAULT_PROFILE - let emoji = reaction.content[0] - if (reaction.content === "+" || reaction.content === "") - emoji = "❤️" - - return render_pfp(reaction.pubkey, profile, "small") -} - function get_reactions(model, evid) { const reactions_set = model.reactions_to[evid] @@ -1065,21 +794,6 @@ function get_reactions(model, evid) return groups } -function render_reactions(model, ev) { - const groups = get_reactions(model, ev.id) - let str = "" - - for (const emoji of Object.keys(groups)) { - str += render_reaction_group(model, emoji, groups[emoji], ev) - } - - return ` -
    - ${str} -
    - ` -} - function close_reply() { const modal = document.querySelector("#reply-modal") modal.classList.add("closed"); @@ -1289,30 +1003,11 @@ function reply_to(evid) { const replying_to = modal.querySelector("#replying-to") replying_to.dataset.evid = evid + const ev = DAMUS.all_events[evid] replying_to.innerHTML = render_event(DAMUS, ev, {is_composing: true, nobar: true, max_depth: 1}) } -function render_action_bar(ev, can_delete) { - let delete_html = "" - if (can_delete) - delete_html = ` + + + + +
    + ` +} + +function render_home_event(model, ev) +{ + let max_depth = 3 + if (ev.refs && ev.refs.root && model.expanded.has(ev.refs.root)) { + max_depth = null + } + + return render_event(model, ev, {max_depth}) +} + +function render_events(model) { + return model.events + .filter((ev, i) => i < 140) + .map((ev) => render_home_event(model, ev)).join("\n") +} + +function render_reply_line_top(has_top_line) { + const classes = has_top_line ? "" : "invisible" + return `
    ` +} + +function render_reply_line_bot() { + return `
    ` +} + +function render_thread_collapsed(model, reply_ev, opts) +{ + if (opts.is_composing) + return "" + return `
    +
    + More messages in thread available. Click to expand. +
    +
    ` +} + +function render_replied_events(model, ev, opts) +{ + if (!(ev.refs && ev.refs.reply)) + return "" + + const reply_ev = model.all_events[ev.refs.reply] + if (!reply_ev) + return "" + + opts.replies = opts.replies == null ? 1 : opts.replies + 1 + if (!(opts.max_depth == null || opts.replies < opts.max_depth)) + return render_thread_collapsed(model, reply_ev, opts) + + opts.is_reply = true + return render_event(model, reply_ev, opts) +} + +function render_replying_to_chat(model, ev) { + const chatroom = (ev.refs.root && model.chatrooms[ev.refs.root]) || {} + const roomname = chatroom.name || ev.refs.root || "??" + const pks = ev.refs.pubkeys || [] + const names = pks.map(pk => render_mentioned_name(pk, model.profiles[pk])).join(", ") + const to_users = pks.length === 0 ? "" : ` to ${names}` + + return `
    replying${to_users} in ${roomname}
    ` +} + +function render_replying_to(model, ev) { + if (!(ev.refs && ev.refs.reply)) + return "" + + if (ev.kind === 42) + return render_replying_to_chat(model, ev) + + let pubkeys = ev.refs.pubkeys || [] + if (pubkeys.length === 0 && ev.refs.reply) { + const replying_to = model.all_events[ev.refs.reply] + if (!replying_to) + return `
    reply to ${ev.refs.reply}
    ` + + pubkeys = [replying_to.pubkey] + } + + const names = ev.refs.pubkeys.map(pk => render_mentioned_name(pk, model.profiles[pk])).join(", ") + + return ` + + replying to ${names} + + ` +} + +function render_unknown_event(model, ev) { + return "Unknown event" +} + +function render_boost(model, ev, opts) { + //todo validate content + if (!ev.json_content) + return render_unknown_event(ev) + + //const profile = model.profiles[ev.pubkey] + opts.is_boost_event = true + opts.boosted = { + pubkey: ev.pubkey, + profile: model.profiles[ev.pubkey] + } + return render_event(model, ev.json_content, opts) + //return ` + //
    + //
    Reposted by ${render_name_plain(ev.pubkey, profile)}
    + //${render_event(model, ev.json_content, opts)} + //
    + //` +} + +function render_comment_body(model, ev, opts) { + const can_delete = model.pubkey === ev.pubkey; + const bar = !can_reply(ev) || opts.nobar? "" : render_action_bar(ev, can_delete) + const show_media = !opts.is_composing + + return ` +
    + ${render_replying_to(model, ev)} + ${render_boosted_by(model, ev, opts)} +
    +

    + ${format_content(ev, show_media)} +

    + ${render_reactions(model, ev)} + ${bar} + ` +} + +function render_boosted_by(model, ev, opts) { + const b = opts.boosted + if (!b) { + return "" + } + // TODO encapsulate username as link/button! + return ` +
    Shared by + ${render_name_plain(b.pubkey, b.profile)} +
    + ` +} + +function render_deleted_comment_body(ev, deleted) { + if (deleted.content) { + const show_media = false + return ` +
    + This comment was deleted. Reason: +
    ${format_content(deleted, show_media)}
    +
    + ` + } + return `
    This comment was deleted
    ` +} + +function render_event(model, ev, opts={}) { + if (ev.kind === 6) + return render_boost(model, ev, opts) + if (shouldnt_render_event(model, ev, opts)) + return "" + delete opts.is_boost_event + model.rendered[ev.id] = true + const profile = model.profiles[ev.pubkey] || DEFAULT_PROFILE + 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()) || "" + + const deleted = is_deleted(model, ev.id) + if (deleted && !opts.is_reply) + return "" + + const replied_events = render_replied_events(model, ev, opts) + + let name = "???" + if (!deleted) { + name = render_name_plain(ev.pubkey, profile) + } + + const has_top_line = replied_events !== "" + const border_bottom = has_bot_line ? "" : "bottom-border"; + return ` + ${replied_events} +
    +
    + ${render_reply_line_top(has_top_line)} + ${deleted ? render_deleted_pfp() : render_pfp(ev.pubkey, profile)} + ${reply_line_bot} +
    +
    +
    + + ${name} + + ${delta} +
    +
    + ${deleted ? render_deleted_comment_body(ev, deleted) : render_comment_body(model, ev, opts)} +
    +
    +
    + ` +} + +function render_pfp(pk, profile, size="normal") { + const name = render_name_plain(pk, profile) + return `` +} + +function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) { + const reaction = reactions[our_pubkey] + if (!reaction) { + return `onclick="send_reply('${emoji}', '${reacting_to}')"` + } else { + return `onclick="delete_post('${reaction.id}')"` + } +} + +function render_reaction_group(model, emoji, reactions, reacting_to) { + const pfps = Object.keys(reactions).map((pk) => render_reaction(model, reactions[pk])) + + let onclick = render_react_onclick(model.pubkey, reacting_to.id, emoji, reactions) + + return ` + + + ${emoji} + + ${pfps.join("\n")} + + ` +} + +function render_reaction(model, reaction) { + const profile = model.profiles[reaction.pubkey] || DEFAULT_PROFILE + let emoji = reaction.content[0] + if (reaction.content === "+" || reaction.content === "") + emoji = "❤️" + + return render_pfp(reaction.pubkey, profile, "small") +} + +function render_action_bar(ev, can_delete) { + let delete_html = "" + if (can_delete) + delete_html = `