commit 9788baa322b2c4dd7d5771c3fa17c8d3d937eaf9
parent c075d3dd0eed21b32b1c4373d5ab7b887216256e
Author: William Casarin <jb55@jb55.com>
Date: Fri, 19 Aug 2022 11:15:26 -0700
latest
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
18 files changed, 973 insertions(+), 22 deletions(-)
diff --git 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
@@ -15,7 +15,7 @@
<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/skeleton.css?v=2">
- <link rel="stylesheet" href="css/custom.css?v=4">
+ <link rel="stylesheet" href="css/custom.css?v=5">
</head>
<body>
@@ -66,8 +66,13 @@
<div>
<img style="width: 200px" src="img/app-store-coming-soon.svg" />
</div>
- <div>
- <a href="https://testflight.apple.com/join/CLwjLxWl">Join the TestFlight Beta</a>
+ <div style="margin-top: 20px">
+ <p>
+ <a href="https://damus.io/log">The Damus Log</a>
+ </p>
+ <p>
+ <a href="https://testflight.apple.com/join/CLwjLxWl">Join the TestFlight Beta</a>
+ </p>
</div>
</section>
diff --git a/log/2022-08-02-introducing-damus-log.html b/log/2022-08-02-introducing-damus-log.html
@@ -6,7 +6,7 @@
<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="log.css?v=29">
<link rel="stylesheet" href="comments.css?v=5">
</head>
<body>
@@ -16,6 +16,7 @@
</span>
</section>
<div class="container">
+ <a href="https://damus.io/log" class="date">< The Damus Log</a>
<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
@@ -42,13 +43,25 @@ 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>
+<h3><a id="comment-link" href="nostr:e:">Comments</a></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")
+ const threads = {
+ "the-stuff-loads-better-release": "9941b55c2844f275b7b8714a1c39859088a425ce798f740ea8fea879f9098641",
+ "introducing-damus-log": "4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371",
+ }
+ let relay
+ for (const key of Object.keys(threads)) {
+ if (window.location.href.includes(key)) {
+ const id = threads[key]
+ relay = comments_init(id)
+ document.querySelector("#comment-link").href = 'nostr:e:' + id
+ break
+ }
+ }
</script>
</div> <!-- container -->
</body>
diff --git a/log/2022-08-19-the-stuff-loads-better-release.gmi 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
@@ -0,0 +1,88 @@
+
+<!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=29">
+ <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">
+ <a href="https://damus.io/log" class="date">< The Damus Log</a>
+<h1 id="v0.1.3---the-stuff-loads-better-release">v0.1.3 - The “Stuff
+Loads Better” Release</h1>
+<p>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.</p>
+<p>If you’re not on the testflight already, you can get it here:</p>
+<p><a href="https://testflight.apple.com/join/CLwjLxWl">Damus
+TestFlight</a></p>
+<p>This was the last release before lightning support, so next version
+will be exciting!!</p>
+<p>Anyways, here’s the full changlog!</p>
+<pre><code># 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</code></pre>
+<p><a href="https://damus.io">Damus TestFlight</a></p>
+
+<h3><a id="comment-link" href="nostr:e:">Comments</a></h3>
+ <div id="comments">
+ </div>
+ <script src="nostr.js?v=4" ></script>
+ <script src="comments.js?v=16" ></script>
+ <script>
+ const threads = {
+ "the-stuff-loads-better-release": "9941b55c2844f275b7b8714a1c39859088a425ce798f740ea8fea879f9098641",
+ "introducing-damus-log": "4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371",
+ }
+ let relay
+ for (const key of Object.keys(threads)) {
+ if (window.location.href.includes(key)) {
+ const id = threads[key]
+ relay = comments_init(id)
+ document.querySelector("#comment-link").href = 'nostr:e:' + id
+ break
+ }
+ }
+ </script>
+ </div> <!-- container -->
+ </body>
+</html>
diff --git 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
@@ -6,7 +6,7 @@
<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="log.css?v=29">
<link rel="stylesheet" href="comments.css?v=5">
</head>
<body>
@@ -16,3 +16,4 @@
</span>
</section>
<div class="container">
+ <a href="https://damus.io/log" class="date">< The Damus Log</a>
diff --git a/log/img b/log/img
@@ -0,0 +1 @@
+../img/+
\ No newline at end of file
diff --git a/log/index.html b/log/index.html
diff --git 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
@@ -1,11 +1,23 @@
- <h3>Comments</h3>
+<h3><a id="comment-link" href="nostr:e:">Comments</a></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")
+ const threads = {
+ "the-stuff-loads-better-release": "9941b55c2844f275b7b8714a1c39859088a425ce798f740ea8fea879f9098641",
+ "introducing-damus-log": "4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371",
+ }
+ let relay
+ for (const key of Object.keys(threads)) {
+ if (window.location.href.includes(key)) {
+ const id = threads[key]
+ relay = comments_init(id)
+ document.querySelector("#comment-link").href = 'nostr:e:' + id
+ break
+ }
+ }
</script>
</div> <!-- container -->
</body>
diff --git a/web/Makefile 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
@@ -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("<","<").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 `<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/web/damus.css 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
@@ -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 `
+ <div class="comment">
+ <div class="info">
+ ${render_name(ev.pubkey, profile)}
+ <span>${delta}</span>
+ </div>
+ <img class="pfp" onerror="this.onerror=null;this.src='${robohash(pk)}';" src="${get_picture(pk, 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("<","<").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 `<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/web/img/damus-nobg.svg b/web/img/damus-nobg.svg
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="146.15311mm"
+ height="184.664mm"
+ viewBox="0 0 146.15311 184.66401"
+ version="1.1"
+ id="svg5"
+ inkscape:version="1.2-alpha (0bd5040e, 2022-02-05)"
+ sodipodi:docname="damus-nobg.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview7"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:blackoutopacity="0.0"
+ inkscape:document-units="mm"
+ showgrid="false"
+ inkscape:zoom="0.5946522"
+ inkscape:cx="73.992831"
+ inkscape:cy="206.8436"
+ inkscape:window-width="1435"
+ inkscape:window-height="844"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="layer2" />
+ <defs
+ id="defs2">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient39361">
+ <stop
+ style="stop-color:#0de8ff;stop-opacity:0.78082192;"
+ offset="0"
+ id="stop39357" />
+ <stop
+ style="stop-color:#d600fc;stop-opacity:0.95433789;"
+ offset="1"
+ id="stop39359" />
+ </linearGradient>
+ <inkscape:path-effect
+ effect="bspline"
+ id="path-effect255"
+ is_visible="true"
+ lpeversion="1"
+ weight="33.333333"
+ steps="2"
+ helper_size="0"
+ apply_no_weight="true"
+ apply_with_weight="true"
+ only_selected="false" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient2119">
+ <stop
+ style="stop-color:#1c55ff;stop-opacity:1;"
+ offset="0"
+ id="stop2115" />
+ <stop
+ style="stop-color:#7f35ab;stop-opacity:1;"
+ offset="0.5"
+ id="stop2123" />
+ <stop
+ style="stop-color:#ff0bd6;stop-opacity:1;"
+ offset="1"
+ id="stop2117" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2119"
+ id="linearGradient2121"
+ x1="10.067794"
+ y1="248.81357"
+ x2="246.56145"
+ y2="7.1864405"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient39361"
+ id="linearGradient39367"
+ x1="62.104473"
+ y1="128.78963"
+ x2="208.25758"
+ y2="128.78963"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <g
+ inkscape:label="Background"
+ inkscape:groupmode="layer"
+ id="layer1"
+ sodipodi:insensitive="true"
+ style="display:none"
+ transform="translate(-62.104473,-36.457485)">
+ <rect
+ style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
+ id="rect61"
+ width="256"
+ height="256"
+ x="-5.3875166e-08"
+ y="-1.0775033e-07"
+ ry="0"
+ inkscape:label="Gradient"
+ sodipodi:insensitive="true" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="Logo"
+ sodipodi:insensitive="true"
+ transform="translate(-62.104473,-36.457485)">
+ <path
+ style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
+ id="path253"
+ sodipodi:insensitive="true" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer3"
+ inkscape:label="Poly"
+ sodipodi:insensitive="true"
+ transform="translate(-62.104473,-36.457485)">
+ <path
+ style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
+ id="path4648" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
+ id="path9299" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
+ id="path9301" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
+ id="path9368" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
+ id="path9370" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
+ id="path9372" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
+ id="path9374" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="Vertices"
+ transform="translate(-62.104473,-36.457485)">
+ <circle
+ style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path27764"
+ cx="106.86934"
+ cy="142.38014"
+ r="2.0022209" />
+ <circle
+ style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="circle28773"
+ cx="111.54119"
+ cy="99.221161"
+ r="2.0022209" />
+ <circle
+ style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="circle29091"
+ cx="165.90784"
+ cy="101.36163"
+ r="2.0022209" />
+ </g>
+</svg>
diff --git a/web/index.html b/web/index.html
@@ -1,4 +1,3 @@
-
<!DOCTYPE html>
<html lang="en">
<head>
@@ -6,12 +5,24 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Damus Web</title>
+
+ <link rel="stylesheet" href="damus.css?v=2">
</head>
<body>
- <h1>Damus Web</h1>
- <div id="content">
- </div>
- <script src="index.js"></script>
+ <section class="header">
+ <span class="logo">
+ <img src="img/damus-nobg.svg"/>
+ </span>
+ </section>
+ <div class="container">
+ <div id="posts">
+ </div>
+ </div>
+ <script src="nostr.js?v=1"></script>
+ <script src="damus.js?v=2"></script>
+ <script>
+ const relay = damus_web_init("4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371")
+ </script>
</body>
</html>
diff --git a/web/nostr.js 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))
+ }
+ }
+}
+