latest
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
parent
c075d3dd0e
commit
9788baa322
18 changed files with 1007 additions and 23 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
51
log/2022-08-19-the-stuff-loads-better-release.gmi
Normal file
51
log/2022-08-19-the-stuff-loads-better-release.gmi
Normal file
|
@ -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
|
||||
|
88
log/2022-08-19-the-stuff-loads-better-release.html
Normal file
88
log/2022-08-19-the-stuff-loads-better-release.html
Normal file
|
@ -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>
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
1
log/img
Symbolic link
1
log/img
Symbolic link
|
@ -0,0 +1 @@
|
|||
../img/
|
|
@ -1 +0,0 @@
|
|||
2022-08-02-introducing-damus-log.html
|
35
log/index.html
Normal file
35
log/index.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
<!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=28">
|
||||
<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>The Damus Log</h1>
|
||||
<ul>
|
||||
<li><a href="2022-08-19-the-stuff-loads-better-release.html">v0.1.3 - The "Stuff Loads Better" Release</a><span class="date">2022-08-19</span></li>
|
||||
<li><a href="2022-08-02-introducing-damus-log.html">Introducing The Damus Log</a><span class="date">2022-08-02</span></li>
|
||||
</ul>
|
||||
|
||||
<h3><a href="nostr:e:2ed9b99190f0acf8f5cf768d4edd4be004a1262c6d296f341333e5e94b5ec423">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("2ed9b99190f0acf8f5cf768d4edd4be004a1262c6d296f341333e5e94b5ec423")
|
||||
</script>
|
||||
</div> <!-- container -->
|
||||
</body>
|
||||
</html>
|
27
log/log.css
27
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%;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
4
web/Makefile
Normal file
4
web/Makefile
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
|
||||
dist:
|
||||
rsync -avzP ./ charon:/www/damus.io/web/
|
177
web/comments.js
Normal file
177
web/comments.js
Normal file
|
@ -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';
|
||||
}
|
||||
}
|
122
web/damus.css
Normal file
122
web/damus.css
Normal file
|
@ -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%;
|
||||
}
|
||||
}
|
183
web/damus.js
Normal file
183
web/damus.js
Normal file
|
@ -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';
|
||||
}
|
||||
}
|
186
web/img/damus-nobg.svg
Normal file
186
web/img/damus-nobg.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 6.5 KiB |
|
@ -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">
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="img/damus-nobg.svg"/>
|
||||
</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
<div id="posts">
|
||||
</div>
|
||||
<script src="index.js"></script>
|
||||
</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>
|
||||
|
||||
|
|
73
web/nostr.js
Normal file
73
web/nostr.js
Normal file
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue