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
+
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
+
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
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
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
+
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 `
+
+ `
+}
+
+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 `
+
+ `
+}
+
+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))
+ }
+ }
+}
+
+ ${format_content(ev.content)} +
+