diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..af2e08a
Binary files /dev/null and b/.DS_Store differ
diff --git a/css/custom.css b/css/custom.css
index 89a50d6..b094132 100644
--- a/css/custom.css
+++ b/css/custom.css
@@ -8,6 +8,10 @@ html { font-family: 'Inter', sans-serif; }
max-width: 800px;
}
+.blog-container {
+ font-family: serif;
+}
+
label {
white-space: nowrap;
}
diff --git a/img/bitcoin-p2p.png b/img/bitcoin-p2p.png
new file mode 100644
index 0000000..b989e78
Binary files /dev/null and b/img/bitcoin-p2p.png differ
diff --git a/img/digital-nomad.png b/img/digital-nomad.png
new file mode 100644
index 0000000..e2e2f21
Binary files /dev/null and b/img/digital-nomad.png differ
diff --git a/img/encrypted-message.png b/img/encrypted-message.png
new file mode 100644
index 0000000..756d585
Binary files /dev/null and b/img/encrypted-message.png differ
diff --git a/img/undercover.png b/img/undercover.png
new file mode 100644
index 0000000..eafa17c
Binary files /dev/null and b/img/undercover.png differ
diff --git a/index.html b/index.html
index a57e8ce..0862fa2 100644
--- a/index.html
+++ b/index.html
@@ -8,9 +8,10 @@
-
+
+
diff --git a/log/.envrc b/log/.envrc
new file mode 100644
index 0000000..42644cd
--- /dev/null
+++ b/log/.envrc
@@ -0,0 +1 @@
+export PATH=$PWD:$PATH
diff --git a/log/2022-08-02-introducing-damus-log.gmi b/log/2022-08-02-introducing-damus-log.gmi
new file mode 100644
index 0000000..0f08dc7
--- /dev/null
+++ b/log/2022-08-02-introducing-damus-log.gmi
@@ -0,0 +1,16 @@
+
+# The Damus Log - Powered by #nostr
+
+Hey there, Welcome to the damus log! A blog powered by... nostr! What does this mean!? What is nostr? Let's find out!
+
+nostr is what powers damus, an iOS nostr client we're working on. It's a fancy pants new internet protocol designed to be the email of social networks. Imagine if email was controlled by a single company. Everyone would have to use the same email client (probably something like gmail), and a single company would have complete control over all your data... everyone's data!
+
+This isn't good, this is why the internet today was originally built on these decentralized protocols. Things like websites and email are all available on different platforms, clients and servers. This freedom to pick and choose prevents any single company to have complete control over our data.
+
+nostr is an attempt to do the same for social networks themselves. It provides a censorship resistant, real-time database. Anyone can run a nostr relay and no single relay is in control of the data. It's quite ingenious if we say so ourselves.
+
+We like it so much we've made our blog nostr-powered! The comments below are from the nostr network. You can comment on it from the damus client itself! If you're interested in trying it out, try out the testflight at the bottom of our homepage:
+
+=> https://damus.io damus.io
+
+Looking forward to seeing you on nostr!
diff --git a/log/2022-08-02-introducing-damus-log.html b/log/2022-08-02-introducing-damus-log.html
new file mode 100644
index 0000000..cd15478
--- /dev/null
+++ b/log/2022-08-02-introducing-damus-log.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+ The Damus Log
+
+
+
+
+
+
+
The Damus Log - Powered by
+#nostr
+
Hey there, Welcome to the damus log! A blog powered by… nostr! What
+does this mean!? What is nostr? Let’s find out!
+
nostr is what powers damus, an iOS nostr client we’re working on.
+It’s a fancy pants new internet protocol designed to be the email of
+social networks. Imagine if email was controlled by a single company.
+Everyone would have to use the same email client (probably something
+like gmail), and a single company would have complete control over all
+your data… everyone’s data!
+
This isn’t good, this is why the internet today was originally built
+on these decentralized protocols. Things like websites and email are all
+available on different platforms, clients and servers. This freedom to
+pick and choose prevents any single company to have complete control
+over our data.
+
nostr is an attempt to do the same for social networks themselves. It
+provides a censorship resistant, real-time database. Anyone can run a
+nostr relay and no single relay is in control of the data. It’s quite
+ingenious if we say so ourselves.
+
We like it so much we’ve made our blog nostr-powered! The comments
+below are from the nostr network. You can comment on it from the damus
+client itself! If you’re interested in trying it out, try out the
+testflight at the bottom of our homepage:
+
damus.io
+
Looking forward to seeing you on nostr!
+
+
Comments
+
+
+
+
+
+
+
diff --git a/log/Makefile b/log/Makefile
new file mode 100644
index 0000000..3377a2a
--- /dev/null
+++ b/log/Makefile
@@ -0,0 +1,17 @@
+
+POSTS=$(wildcard *.gmi)
+HTMLS=$(POSTS:.gmi=.html)
+
+
+all: $(HTMLS)
+
+clean: fake
+ rm -f $(HTMLS)
+
+dist: all
+ rsync -avzP ./ charon:/www/damus.io/log/
+
+%.html: %.gmi head.html tail.html
+ ./gmi2md < $< | pandoc -f markdown -t html -o - | cat head.html - tail.html > $@
+
+.PHONY: fake
diff --git a/log/comments.css b/log/comments.css
new file mode 100644
index 0000000..b9c60fa
--- /dev/null
+++ b/log/comments.css
@@ -0,0 +1,69 @@
+
+.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/log/comments.js b/log/comments.js
new file mode 100644
index 0000000..6855eaa
--- /dev/null
+++ b/log/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/log/gmi2md b/log/gmi2md
new file mode 100755
index 0000000..c3284e1
--- /dev/null
+++ b/log/gmi2md
@@ -0,0 +1,25 @@
+#!/usr/bin/env sed -Ef
+
+# gmi2md: Sed script to convert text/gemini to markdown.
+# Based on v0.14.2 of the gemini spec.
+#
+# This script is dedicated to the public domain according to the terms of CC0:
+# https://creativecommons.org/publicdomain/zero/1.0/
+
+x
+/^```/ {
+ x
+ /^```/ {
+ x
+ s/.*//
+ x
+ }
+ b
+}
+g
+
+/^=>/ {
+ s/[][()]/\\&/g
+ s/^=>\s*([^[:space:]]+)\s*$/[\1](\1)/
+ s/^=>\s*([^[:space:]]+)\s+(.+)/[\2](\1)/
+}
diff --git a/log/head.html b/log/head.html
new file mode 100644
index 0000000..9ed9eb7
--- /dev/null
+++ b/log/head.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ The Damus Log
+
+
+
+
+
+
diff --git a/log/index.html b/log/index.html
new file mode 120000
index 0000000..9ec2f89
--- /dev/null
+++ b/log/index.html
@@ -0,0 +1 @@
+2022-08-02-introducing-damus-log.html
\ No newline at end of file
diff --git a/log/log.css b/log/log.css
new file mode 100644
index 0000000..869ebf1
--- /dev/null
+++ b/log/log.css
@@ -0,0 +1,154 @@
+@import url('https://rsms.me/inter/inter.css');
+
+.header {
+ display: flex;
+ margin: 50px 0 0 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;
+ }
+}
+p {
+ margin: 1em 0;
+}
+a {
+ color: #1a1a1a;
+}
+a:visited {
+ color: #1a1a1a;
+}
+img {
+ max-width: 100%;
+}
+h1, h2, h3, h4, h5, h6 {
+ font-family: 'Inter', system-ui, sans-serif;
+ margin-top: 1.4em;
+}
+h5, h6 {
+ font-size: 1em;
+ font-style: italic;
+}
+h6 {
+ font-weight: normal;
+}
+ol, ul {
+ padding-left: 1.7em;
+ margin-top: 1em;
+}
+li > ol, li > ul {
+ margin-top: 0;
+}
+blockquote {
+ margin: 1em 0 1em 1.7em;
+ padding-left: 1em;
+ border-left: 2px solid #e6e6e6;
+ color: #606060;
+}
+code {
+ font-family: Menlo, Monaco, 'Lucida Console', Consolas, monospace;
+ font-size: 85%;
+ margin: 0;
+}
+pre {
+ margin: 1em 0;
+ overflow: auto;
+}
+pre code {
+ padding: 0;
+ overflow: visible;
+}
+.sourceCode {
+ background-color: transparent;
+ overflow: visible;
+}
+hr {
+ background-color: #1a1a1a;
+ border: none;
+ height: 1px;
+ margin: 1em 0;
+}
+table {
+ margin: 1em 0;
+ border-collapse: collapse;
+ width: 100%;
+ overflow-x: auto;
+ display: block;
+ font-variant-numeric: lining-nums tabular-nums;
+}
+table caption {
+ margin-bottom: 0.75em;
+}
+tbody {
+ margin-top: 0.5em;
+ border-top: 1px solid #1a1a1a;
+ border-bottom: 1px solid #1a1a1a;
+}
+th {
+ border-top: 1px solid #1a1a1a;
+ padding: 0.25em 0.5em 0.25em 0.5em;
+}
+td {
+ padding: 0.125em 0.5em 0.25em 0.5em;
+}
+header {
+ margin-bottom: 4em;
+ text-align: center;
+}
+#TOC li {
+ list-style: none;
+}
+#TOC a:not(:hover) {
+ text-decoration: none;
+}
+code{white-space: pre-wrap;}
+span.smallcaps{font-variant: small-caps;}
+span.underline{text-decoration: underline;}
+div.column{display: inline-block; vertical-align: top; width: 50%;}
+div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
+ul.task-list{list-style: none;}
+.display.math{display: block; text-align: center; margin: 0.5rem auto;}
diff --git a/log/nostr.js b/log/nostr.js
new file mode 100644
index 0000000..1e5b70b
--- /dev/null
+++ b/log/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))
+ }
+ }
+}
+
diff --git a/log/tail.html b/log/tail.html
new file mode 100644
index 0000000..d63a2b0
--- /dev/null
+++ b/log/tail.html
@@ -0,0 +1,12 @@
+
+
Comments
+
+
+
+
+
+
+
diff --git a/log/template-head.html b/log/template-head.html
new file mode 100644
index 0000000..c4a2bd0
--- /dev/null
+++ b/log/template-head.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+ The Damus Log
+
+
diff --git a/log/template-tail.html b/log/template-tail.html
new file mode 100644
index 0000000..5179ade
--- /dev/null
+++ b/log/template-tail.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..241336f
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ Damus Web
+
+
+ Damus Web
+
+
+
+
+
+
diff --git a/web/index.js b/web/index.js
new file mode 100644
index 0000000..1afee15
--- /dev/null
+++ b/web/index.js
@@ -0,0 +1,111 @@
+
+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()
+ ${format_content(ev.content)} +
+