damus.io

damus.io website
git clone git://jb55.com/damus.io
Log | Files | Refs

commit c075d3dd0eed21b32b1c4373d5ab7b887216256e
parent b56cd0642feaf33160cd8872191102199a7366ef
Author: William Casarin <jb55@jb55.com>
Date:   Thu,  4 Aug 2022 08:51:56 -0700

add all the things

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
A.DS_Store | 0
Mcss/custom.css | 4++++
Aimg/bitcoin-p2p.png | 0
Aimg/digital-nomad.png | 0
Aimg/encrypted-message.png | 0
Aimg/undercover.png | 0
Mindex.html | 3++-
Alog/.envrc | 1+
Alog/2022-08-02-introducing-damus-log.gmi | 16++++++++++++++++
Alog/2022-08-02-introducing-damus-log.html | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alog/Makefile | 17+++++++++++++++++
Alog/comments.css | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alog/comments.js | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alog/gmi2md | 25+++++++++++++++++++++++++
Alog/head.html | 18++++++++++++++++++
Alog/index.html | 2++
Alog/log.css | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alog/nostr.js | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alog/tail.html | 12++++++++++++
Alog/template-head.html | 10++++++++++
Alog/template-tail.html | 3+++
Aweb/index.html | 17+++++++++++++++++
Aweb/index.js | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
23 files changed, 766 insertions(+), 1 deletion(-)

diff --git a/.DS_Store b/.DS_Store Binary files differ. diff --git 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 Binary files differ. diff --git a/img/digital-nomad.png b/img/digital-nomad.png Binary files differ. diff --git a/img/encrypted-message.png b/img/encrypted-message.png Binary files differ. diff --git a/img/undercover.png b/img/undercover.png Binary files differ. diff --git a/index.html b/index.html @@ -8,9 +8,10 @@ <meta property="og:title" content="Damus"> <meta property="og:description" content="A new social network that you control"> - <meta property="og:image" content="https://damus.io/img/damus.png"> + <meta property="og:image" content="https://damus.io/img/logo.png"> <meta property="og:url" content="https://damus.io"> <meta name="twitter:card" content="summary_large_image"> + <meta name="twitter:image" content="https://damus.io/img/logo.png"> <link rel="stylesheet" href="css/normalize.css"> <link rel="stylesheet" href="css/skeleton.css?v=2"> diff --git a/log/.envrc 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 @@ -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 @@ -0,0 +1,55 @@ + +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <title>The Damus Log</title> + <link rel="stylesheet" href="log.css?v=17"> + <link rel="stylesheet" href="comments.css?v=5"> + </head> + <body> + <section class="header"> + <span class="logo"> + <img src="/img/damus-nobg.svg"/> + </span> + </section> + <div class="container"> +<h1 id="the-damus-log---powered-by-nostr">The Damus Log - Powered by +#nostr</h1> +<p>Hey there, Welcome to the damus log! A blog powered by… nostr! What +does this mean!? What is nostr? Let’s find out!</p> +<p>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!</p> +<p>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.</p> +<p>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.</p> +<p>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:</p> +<p><a href="https://damus.io">damus.io</a></p> +<p>Looking forward to seeing you on nostr!</p> + + <h3>Comments</h3> + <div id="comments"> + </div> + <script src="nostr.js?v=4" ></script> + <script src="comments.js?v=16" ></script> + <script> + const relay = comments_init("4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371") + </script> + </div> <!-- container --> + </body> +</html> diff --git a/log/Makefile 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 @@ -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 @@ -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 ` + <div class="comment"> + <div class="info"> + ${render_name(ev.pubkey, profile)} + <span>${delta}</span> + </div> + <img class="pfp" src="${get_picture(ev.pubkey, profile)}"> + <p> + ${format_content(ev.content)} + </p> + </div> + ` +} + +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 += "<span class='quote'>" + blockin = true + } + str += sanitize(line.slice(1)) + } else { + if (blockin) { + blockin = false + str += "</span>" + } + str += sanitize(line) + } + return str + "<br/>" + }, "") +} + +function format_content(content) +{ + return convert_quote_blocks(content) +} + +function sanitize(content) +{ + if (!content) + return "" + return content.replaceAll("<","&lt;").replaceAll(">","&gt;") +} + +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 `<div class="username">${sanitize(name)}</div>` +} + +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 @@ -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 @@ -0,0 +1,18 @@ + +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <title>The Damus Log</title> + <link rel="stylesheet" href="log.css?v=17"> + <link rel="stylesheet" href="comments.css?v=5"> + </head> + <body> + <section class="header"> + <span class="logo"> + <img src="/img/damus-nobg.svg"/> + </span> + </section> + <div class="container"> diff --git a/log/index.html 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 @@ -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 @@ -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 @@ -0,0 +1,12 @@ + + <h3>Comments</h3> + <div id="comments"> + </div> + <script src="nostr.js?v=4" ></script> + <script src="comments.js?v=16" ></script> + <script> + const relay = comments_init("4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371") + </script> + </div> <!-- container --> + </body> +</html> diff --git a/log/template-head.html b/log/template-head.html @@ -0,0 +1,10 @@ + +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <title>The Damus Log</title> + </head> + <body> diff --git a/log/template-tail.html b/log/template-tail.html @@ -0,0 +1,3 @@ + + </body> +</html> diff --git a/web/index.html b/web/index.html @@ -0,0 +1,17 @@ + +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <title>Damus Web</title> + </head> + <body> + <h1>Damus Web</h1> + <div id="content"> + </div> + <script src="index.js"></script> + </body> +</html> + diff --git a/web/index.js 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 `<ul id="home"> </ul>` +} + +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 `<li>${ev.content}</li>` +} + +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()