From 9788baa322b2c4dd7d5771c3fa17c8d3d937eaf9 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Fri, 19 Aug 2022 11:15:26 -0700 Subject: [PATCH] latest Signed-off-by: William Casarin --- css/custom.css | 10 + index.html | 11 +- log/2022-08-02-introducing-damus-log.html | 19 +- ...2-08-19-the-stuff-loads-better-release.gmi | 51 +++++ ...-08-19-the-stuff-loads-better-release.html | 88 +++++++++ log/gmi2md | 2 +- log/head.html | 3 +- log/img | 1 + log/index.html | 36 +++- log/log.css | 27 ++- log/tail.html | 16 +- web/Makefile | 4 + web/comments.js | 177 +++++++++++++++++ web/damus.css | 122 ++++++++++++ web/damus.js | 183 +++++++++++++++++ web/img/damus-nobg.svg | 186 ++++++++++++++++++ web/index.html | 21 +- web/nostr.js | 73 +++++++ 18 files changed, 1007 insertions(+), 23 deletions(-) create mode 100644 log/2022-08-19-the-stuff-loads-better-release.gmi create mode 100644 log/2022-08-19-the-stuff-loads-better-release.html create mode 120000 log/img mode change 120000 => 100644 log/index.html create mode 100644 web/Makefile create mode 100644 web/comments.js create mode 100644 web/damus.css create mode 100644 web/damus.js create mode 100644 web/img/damus-nobg.svg create mode 100644 web/nostr.js diff --git a/css/custom.css b/css/custom.css index b094132..e3dd6e1 100644 --- a/css/custom.css +++ b/css/custom.css @@ -12,6 +12,16 @@ html { font-family: 'Inter', sans-serif; } font-family: serif; } +a { + text-decoration: underline; + font-family: -system-ui, sans-serif; + color: white; +} + +a:visited { + color: #eee; +} + label { white-space: nowrap; } diff --git a/index.html b/index.html index 0862fa2..d3eaf04 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@ - + @@ -66,8 +66,13 @@
-
- Join the TestFlight Beta +
+

+ The Damus Log +

+

+ Join the TestFlight Beta +

diff --git a/log/2022-08-02-introducing-damus-log.html b/log/2022-08-02-introducing-damus-log.html index cd15478..fd08f99 100644 --- a/log/2022-08-02-introducing-damus-log.html +++ b/log/2022-08-02-introducing-damus-log.html @@ -6,7 +6,7 @@ The Damus Log - + @@ -16,6 +16,7 @@
+ < The Damus Log

The Damus Log - Powered by #nostr

Hey there, Welcome to the damus log! A blog powered by… nostr! What @@ -42,13 +43,25 @@ testflight at the bottom of our homepage:

damus.io

Looking forward to seeing you on nostr!

-

Comments

+

Comments

diff --git a/log/2022-08-19-the-stuff-loads-better-release.gmi b/log/2022-08-19-the-stuff-loads-better-release.gmi new file mode 100644 index 0000000..d154cf0 --- /dev/null +++ b/log/2022-08-19-the-stuff-loads-better-release.gmi @@ -0,0 +1,51 @@ + + +# v0.1.3 - The "Stuff Loads Better" Release + +It's that time again! A new damus release. This one fixes a bunch of annoying issues such as profiles not loading properly in some situations. We also do a much better job at caching profile pictures, so no more weird poppyness and wasting your cell data. + +If you're not on the testflight already, you can get it here: + +=> https://testflight.apple.com/join/CLwjLxWl Damus TestFlight + +This was the last release before lightning support, so next version will be exciting!! + +Anyways, here's the full changlog! + +``` +# Added + + - Support kind 42 chat messages (ArcadeCity). + - Friend icons next to names on some views. Check is friend. Arrows are friend-of-friends + - Load chat view first if content contains #chat + - Cancel button on search box + - Added profile picture cache + - Multiline DM messages + +# Changed + + - #hashtags now use the `t` tag instead of `hashtag` + - Clicking a chatroom quote reply will now expand it instead of jumping to it + - Clicking on a note will now always scroll it to the bottom + - Check note ids and signatures on every note + - use bech32 ids everywhere + - Don't animate scroll in chat view + - Post button is not shown if the content is only whitespace + +# Fixed + + - Fixed thread loading issue when clicking on boosts + - Fixed various issues with chatroom view + - Fix bug where sometimes nested navigation views weren't dismissed when tapping the tab bar + - Fixed minor carousel spacing issue on homescreen + - You can now reference users, notes hashtags in DMs + - Profile pics are now loaded in the background + - Limit post sizes to max 32,000 as an upper bound sanity limit. + - Missing profiles are now loaded everywhere + - No longer parse hashtags in urls + - Logging out now resets your keypair and actually logs out + - Copying text in DMs will now copy the decrypted text +``` + +=> https://damus.io Damus TestFlight + diff --git a/log/2022-08-19-the-stuff-loads-better-release.html b/log/2022-08-19-the-stuff-loads-better-release.html new file mode 100644 index 0000000..34e8e2b --- /dev/null +++ b/log/2022-08-19-the-stuff-loads-better-release.html @@ -0,0 +1,88 @@ + + + + + + + + The Damus Log + + + + +
+ +
+
+ < The Damus Log +

v0.1.3 - The “Stuff +Loads Better” Release

+

It’s that time again! A new damus release. This one fixes a bunch of +annoying issues such as profiles not loading properly in some +situations. We also do a much better job at caching profile pictures, so +no more weird poppyness and wasting your cell data.

+

If you’re not on the testflight already, you can get it here:

+

Damus +TestFlight

+

This was the last release before lightning support, so next version +will be exciting!!

+

Anyways, here’s the full changlog!

+
# Added
+
+     - Support kind 42 chat messages (ArcadeCity).
+     - Friend icons next to names on some views. Check is friend. Arrows are friend-of-friends
+     - Load chat view first if content contains #chat
+     - Cancel button on search box
+     - Added profile picture cache
+     - Multiline DM messages
+
+# Changed
+
+     - #hashtags now use the `t` tag instead of `hashtag`
+     - Clicking a chatroom quote reply will now expand it instead of jumping to it
+     - Clicking on a note will now always scroll it to the bottom
+     - Check note ids and signatures on every note
+     - use bech32 ids everywhere
+     - Don't animate scroll in chat view
+     - Post button is not shown if the content is only whitespace
+
+# Fixed
+
+     - Fixed thread loading issue when clicking on boosts
+     - Fixed various issues with chatroom view
+     - Fix bug where sometimes nested navigation views weren't dismissed when tapping the tab bar
+     - Fixed minor carousel spacing issue on homescreen
+     - You can now reference users, notes hashtags in DMs
+     - Profile pics are now loaded in the background
+     - Limit post sizes to max 32,000 as an upper bound sanity limit.
+     - Missing profiles are now loaded everywhere
+     - No longer parse hashtags in urls
+     - Logging out now resets your keypair and actually logs out
+     - Copying text in DMs will now copy the decrypted text
+

Damus TestFlight

+ +

Comments

+
+
+ + + +
+ + diff --git a/log/gmi2md b/log/gmi2md index c3284e1..2513ffc 100755 --- a/log/gmi2md +++ b/log/gmi2md @@ -1,4 +1,4 @@ -#!/usr/bin/env sed -Ef +#!/usr/bin/env sedef # gmi2md: Sed script to convert text/gemini to markdown. # Based on v0.14.2 of the gemini spec. diff --git a/log/head.html b/log/head.html index 9ed9eb7..e24ccbb 100644 --- a/log/head.html +++ b/log/head.html @@ -6,7 +6,7 @@ The Damus Log - + @@ -16,3 +16,4 @@
+ < The Damus Log diff --git a/log/img b/log/img new file mode 120000 index 0000000..8e83967 --- /dev/null +++ b/log/img @@ -0,0 +1 @@ +../img/ \ No newline at end of file diff --git a/log/index.html b/log/index.html deleted file mode 120000 index 9ec2f89..0000000 --- a/log/index.html +++ /dev/null @@ -1 +0,0 @@ -2022-08-02-introducing-damus-log.html \ No newline at end of file diff --git a/log/index.html b/log/index.html new file mode 100644 index 0000000..8fe1b4e --- /dev/null +++ b/log/index.html @@ -0,0 +1,35 @@ + + + + + + + The Damus Log + + + + +
+ +
+
+ +

The Damus Log

+ + +

Comments

+
+
+ + + +
+ + diff --git a/log/log.css b/log/log.css index 869ebf1..ee4c94d 100644 --- a/log/log.css +++ b/log/log.css @@ -12,17 +12,36 @@ letter-spacing: -0.05em; } +.date { + font-size: 0.7em; + margin-left: 10px; + color: #eee; +} + .logo img { padding-right: 18px; width: 60px; } +a { + font-family: -system-ui, sans-serif; + color: white; +} + +a:visited { + color: #eee; +} + +body { + color: white; + min-height: 800px; +} + html { line-height: 1.5; font-size: 20px; font-family: "Georgia", sans-serif; - color: white; background: linear-gradient(45deg, rgba(28,85,255,1) 0%, rgba(127,53,171,1) 59%, rgba(255,11,214,1) 100%); } .container { @@ -56,12 +75,6 @@ html { p { margin: 1em 0; } -a { - color: #1a1a1a; -} -a:visited { - color: #1a1a1a; -} img { max-width: 100%; } diff --git a/log/tail.html b/log/tail.html index d63a2b0..0b4f04a 100644 --- a/log/tail.html +++ b/log/tail.html @@ -1,11 +1,23 @@ -

Comments

+

Comments

diff --git a/web/Makefile b/web/Makefile new file mode 100644 index 0000000..d1d7486 --- /dev/null +++ b/web/Makefile @@ -0,0 +1,4 @@ + + +dist: + rsync -avzP ./ charon:/www/damus.io/web/ diff --git a/web/comments.js b/web/comments.js new file mode 100644 index 0000000..6855eaa --- /dev/null +++ b/web/comments.js @@ -0,0 +1,177 @@ + +function uuidv4() { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); +} + + +async function comments_init(thread) +{ + const relay = await Relay("wss://relay.damus.io") + const now = (new Date().getTime()) / 1000 + const model = {events: [], profiles: {}} + const comments_id = uuidv4() + const profiles_id = uuidv4() + + model.pool = relay + model.el = document.querySelector("#comments") + + relay.subscribe(comments_id, {kinds: [1], "#e": [thread]}) + + relay.event = (sub_id, ev) => { + if (sub_id === comments_id) { + if (ev.content !== "") + insert_event_sorted(model.events, ev) + if (model.realtime) + render_home_view(model) + } else if (sub_id === profiles_id) { + try { + model.profiles[ev.pubkey] = JSON.parse(ev.content) + } catch { + console.log("failed to parse", ev.content) + } + } + } + + relay.eose = async (sub_id) => { + if (sub_id === comments_id) { + handle_comments_loaded(profiles_id, model) + } else if (sub_id === profiles_id) { + handle_profiles_loaded(profiles_id, model) + } + } + + return relay +} + +function handle_profiles_loaded(profiles_id, model) { + // stop asking for profiles + model.pool.unsubscribe(profiles_id) + model.realtime = true + render_home_view(model) +} + +// load profiles after comment notes are loaded +function handle_comments_loaded(profiles_id, model) +{ + const pubkeys = model.events.reduce((s, ev) => { + s.add(ev.pubkey) + return s + }, new Set()) + const authors = Array.from(pubkeys) + + // load profiles + model.pool.subscribe(profiles_id, {kinds: [0], authors: authors}) +} + +function render_home_view(model) { + model.el.innerHTML = render_events(model) +} + +function render_events(model) { + const render = render_event.bind(null, model) + return model.events.map(render).join("\n") +} + +function render_event(model, ev) { + const profile = model.profiles[ev.pubkey] || { + name: "anon", + display_name: "Anonymous", + } + const delta = time_delta(new Date().getTime(), ev.created_at*1000) + return ` +
+
+ ${render_name(ev.pubkey, profile)} + ${delta} +
+ +

+ ${format_content(ev.content)} +

+
+ ` +} + +function convert_quote_blocks(content) +{ + const split = content.split("\n") + let blockin = false + return split.reduce((str, line) => { + if (line !== "" && line[0] === '>') { + if (!blockin) { + str += "" + blockin = true + } + str += sanitize(line.slice(1)) + } else { + if (blockin) { + blockin = false + str += "" + } + str += sanitize(line) + } + return str + "
" + }, "") +} + +function format_content(content) +{ + return convert_quote_blocks(content) +} + +function sanitize(content) +{ + if (!content) + return "" + return content.replaceAll("<","<").replaceAll(">",">") +} + +function get_picture(pk, profile) +{ + return sanitize(profile.picture) || "https://robohash.org/" + pk +} + +function render_name(pk, profile={}) +{ + const display_name = profile.display_name || profile.user + const username = profile.name || "anon" + const name = display_name || username + + return `
${sanitize(name)}
` +} + +function time_delta(current, previous) { + var msPerMinute = 60 * 1000; + var msPerHour = msPerMinute * 60; + var msPerDay = msPerHour * 24; + var msPerMonth = msPerDay * 30; + var msPerYear = msPerDay * 365; + + var elapsed = current - previous; + + if (elapsed < msPerMinute) { + return Math.round(elapsed/1000) + ' seconds ago'; + } + + else if (elapsed < msPerHour) { + return Math.round(elapsed/msPerMinute) + ' minutes ago'; + } + + else if (elapsed < msPerDay ) { + return Math.round(elapsed/msPerHour ) + ' hours ago'; + } + + else if (elapsed < msPerMonth) { + return Math.round(elapsed/msPerDay) + ' days ago'; + } + + else if (elapsed < msPerYear) { + return Math.round(elapsed/msPerMonth) + ' months ago'; + } + + else { + return Math.round(elapsed/msPerYear ) + ' years ago'; + } +} diff --git a/web/damus.css b/web/damus.css new file mode 100644 index 0000000..eed4c3f --- /dev/null +++ b/web/damus.css @@ -0,0 +1,122 @@ +.header { + display: flex; + margin: 30px 0 30px 0; + flex-direction: column; + align-items: center; +} + +.logo { + margin-bottom: 0; + letter-spacing: -0.05em; +} + +.logo img { + padding-right: 18px; + width: 60px; +} + +html { + line-height: 1.5; + font-size: 20px; + font-family: "Georgia", sans-serif; + + color: white; + background: linear-gradient(45deg, rgba(28,85,255,1) 0%, rgba(127,53,171,1) 59%, rgba(255,11,214,1) 100%); +} +.container { + margin: 0 auto; + max-width: 36em; + hyphens: auto; + word-wrap: break-word; + text-rendering: optimizeLegibility; + font-kerning: normal; +} +@media (max-width: 600px) { + .container { + font-size: 0.9em; + padding: 1em; + } +} +@media print { + .container { + background-color: transparent; + color: black; + font-size: 12pt; + } + p, h2, h3 { + orphans: 3; + widows: 3; + } + h2, h3, h4 { + page-break-after: avoid; + } +} + +.pfp { + width: 60px; + height: 60px; + margin: 0 15px 0 15px; + border-radius: 50%; +} + +.comment { + display: flex; + font-family: system-ui, sans; + margin-bottom: 20px; + flex-wrap: wrap; + align-items: center; +} + +.comment p { + background-color: rgba(255.0,255.0,255.0,0.1); + padding: 10px; + border-radius: 8px; + margin: 0; + width: 55%; +} + +.comment .info { + text-align: right; + width: 18%; + line-height: 0.8em; +} + +.quote { + border-left: 2px solid white; + margin-left: 10px; + padding: 10px; + background-color: rgba(255.0,255.0,255.0,0.1); + display: block; +} + +.comment .info span { + font-size: 11px; + color: rgba(255.0,255.0,255.0,0.7); +} + +@media (max-width: 800px){ + /* Reverse the order of elements in the user comments, + so that the avatar and info appear after the text. */ + .comment .info { + order: 2; + width: 50%; + text-align: left; + } + + .pfp { + order: 1; + margin: 0 15px 0 0; + } + + .comment { + padding: 10px; + border-radius: 8px; + background-color: rgba(255.0,255.0,255.0,0.1); + } + + .comment p { + order: 3; + margin-top: 10px; + width: 100%; + } +} diff --git a/web/damus.js b/web/damus.js new file mode 100644 index 0000000..be61f02 --- /dev/null +++ b/web/damus.js @@ -0,0 +1,183 @@ + + +function uuidv4() { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); +} + + +async function damus_web_init(thread) +{ + const relay = await Relay("wss://relay.damus.io") + const now = (new Date().getTime()) / 1000 + const model = {events: [], profiles: {}} + const comments_id = uuidv4() + const profiles_id = uuidv4() + + model.pool = relay + model.el = document.querySelector("#posts") + + relay.subscribe(comments_id, {kinds: [1], limit: 100}) + + relay.event = (sub_id, ev) => { + if (sub_id === comments_id) { + if (ev.content !== "") + insert_event_sorted(model.events, ev) + if (model.realtime) + render_home_view(model) + } else if (sub_id === profiles_id) { + try { + model.profiles[ev.pubkey] = JSON.parse(ev.content) + } catch { + console.log("failed to parse", ev.content) + } + } + } + + relay.eose = async (sub_id) => { + if (sub_id === comments_id) { + handle_comments_loaded(profiles_id, model) + } else if (sub_id === profiles_id) { + handle_profiles_loaded(profiles_id, model) + } + } + + return relay +} + +function handle_profiles_loaded(profiles_id, model) { + // stop asking for profiles + model.pool.unsubscribe(profiles_id) + model.realtime = true + render_home_view(model) +} + +// load profiles after comment notes are loaded +function handle_comments_loaded(profiles_id, model) +{ + const pubkeys = model.events.reduce((s, ev) => { + s.add(ev.pubkey) + return s + }, new Set()) + const authors = Array.from(pubkeys) + + // load profiles + model.pool.subscribe(profiles_id, {kinds: [0], authors: authors}) +} + +function render_home_view(model) { + model.el.innerHTML = render_events(model) +} + +function render_events(model) { + const render = render_event.bind(null, model) + return model.events.map(render).join("\n") +} + +function render_event(model, ev) { + const profile = model.profiles[ev.pubkey] || { + name: "anon", + display_name: "Anonymous", + } + const delta = time_delta(new Date().getTime(), ev.created_at*1000) + const pk = ev.pubkey + return ` +
+
+ ${render_name(ev.pubkey, profile)} + ${delta} +
+ +

+ ${format_content(ev.content)} +

+
+ ` +} + +function convert_quote_blocks(content) +{ + const split = content.split("\n") + let blockin = false + return split.reduce((str, line) => { + if (line !== "" && line[0] === '>') { + if (!blockin) { + str += "" + blockin = true + } + str += sanitize(line.slice(1)) + } else { + if (blockin) { + blockin = false + str += "" + } + str += sanitize(line) + } + return str + "
" + }, "") +} + +function format_content(content) +{ + return convert_quote_blocks(content) +} + +function sanitize(content) +{ + if (!content) + return "" + return content.replaceAll("<","<").replaceAll(">",">") +} + +function robohash(pk) { + return "https://robohash.org/" + pk +} + +function get_picture(pk, profile) +{ + return sanitize(profile.picture) || robohash(pk) +} + +function render_name(pk, profile={}) +{ + const display_name = profile.display_name || profile.user + const username = profile.name || "anon" + const name = display_name || username + + return `
${sanitize(name)}
` +} + +function time_delta(current, previous) { + var msPerMinute = 60 * 1000; + var msPerHour = msPerMinute * 60; + var msPerDay = msPerHour * 24; + var msPerMonth = msPerDay * 30; + var msPerYear = msPerDay * 365; + + var elapsed = current - previous; + + if (elapsed < msPerMinute) { + return Math.round(elapsed/1000) + ' seconds ago'; + } + + else if (elapsed < msPerHour) { + return Math.round(elapsed/msPerMinute) + ' minutes ago'; + } + + else if (elapsed < msPerDay ) { + return Math.round(elapsed/msPerHour ) + ' hours ago'; + } + + else if (elapsed < msPerMonth) { + return Math.round(elapsed/msPerDay) + ' days ago'; + } + + else if (elapsed < msPerYear) { + return Math.round(elapsed/msPerMonth) + ' months ago'; + } + + else { + return Math.round(elapsed/msPerYear ) + ' years ago'; + } +} diff --git a/web/img/damus-nobg.svg b/web/img/damus-nobg.svg new file mode 100644 index 0000000..5f14838 --- /dev/null +++ b/web/img/damus-nobg.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/index.html b/web/index.html index 241336f..fed0173 100644 --- a/web/index.html +++ b/web/index.html @@ -1,4 +1,3 @@ - @@ -6,12 +5,24 @@ Damus Web + + -

Damus Web

-
-
- +
+ +
+
+
+
+
+ + + diff --git a/web/nostr.js b/web/nostr.js new file mode 100644 index 0000000..1e5b70b --- /dev/null +++ b/web/nostr.js @@ -0,0 +1,73 @@ + +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 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_nostr_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] + this.ws.send(JSON.stringify(tosend)) +} + +Relay.prototype.unsubscribe = function relay_unsubscribe(sub_id) { + const tosend = ["CLOSE", sub_id] + this.ws.send(JSON.stringify(tosend)) +} + +function handle_nostr_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)) + } + } +} +