getting ready

- added favicon.ico to get rid of anoying logs in the future
- clicking the idea bubble brings back the text input field
- stop accidentally submitting the reply form
- updated idea cloud to dark/light mode neutral gray
- contact list not needed yet, disabled
- add a detailed list of reactions in star tooltip
- removed debug debounce was for testing chronological feed order
- started username prototype
OFF0 2 years ago
parent e7ad8e468b
commit 4a710ee5d1
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -14,10 +14,11 @@ export const options = {
'src/assets/heart-fill.svg',
'src/assets/star.svg',
'src/assets/star-fill.svg',
'src/favicon.ico',
],
outdir: 'dist',
//entryNames: '[name]-[hash]', TODO: replace urls in index.html with hashed paths
loader: {'.html': 'copy', '.svg': 'copy'},
loader: {'.html': 'copy', '.svg': 'copy', '.ico': 'copy'},
bundle: true,
platform: 'browser',
minify: false, // TODO: true for release and enable sourcemap

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 1000 1000" version="1.1" viewBox="0 0 1e3 1e3" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path d="m240.4 673.3c-67.5 0-122.5 44.8-122.5 100 0 55.1 55 100 122.5 100s122.5-44.8 122.5-100c0-55.1-54.9-100-122.5-100zm0 160.3c-45.7 0-82.8-27-82.8-60.3 0-33.2 37.1-60.3 82.8-60.3s82.8 27 82.8 60.3c0 33.2-37.1 60.3-82.8 60.3z"/><path d="m108.5 865.9c-37.1 0-67.3 25.5-67.3 56.9s30.2 56.9 67.3 56.9 67.3-25.5 67.3-56.9-30.2-56.9-67.3-56.9zm0 74c-15.8 0-27.6-9.1-27.6-17.2s11.3-17.2 27.6-17.2c15.8 0 27.6 9.1 27.6 17.2s-11.8 17.2-27.6 17.2z"/><path d="m989.1 391c-5.4-48.7-35.5-92-83-120.4 0-5.9-0.3-11.5-0.9-16.8-10.2-91.8-93.6-161.2-194.7-162.8-38.1-50-102.5-77.1-171.9-69.3-35.3 4-68 16.6-95.5 36.8-28.5-25-67.5-37.4-108.8-32.8-58.6 6.5-106.6 47.2-119.1 99.1-130.6 36.7-217.5 153.8-203.8 277.4 13.7 122.4 120.2 216.1 254.6 225.3 28.3 24.1 69.3 35.8 112.4 31.1 19-2.1 37.5-7.5 54.1-15.7 31.8 22.3 72.4 34.4 115.8 34.4 8.6 0 17.2-0.5 25.8-1.4 62.8-7 117.7-38.7 147-84.1 12 1.6 24.1 2.4 36.2 2.4 10.1 0 20.3-0.6 30.4-1.7 122-13.6 212.3-104 201.4-201.5zm-205.6 162c-8.7 1-17.4 1.4-26 1.4-14.4 0-28.8-1.3-42.8-4l-14.5-2.7-6.8 13.2c-20.8 40.4-68.1 69.3-123.5 75.5-47 5.2-92.4-7.1-122.8-32.4l-10.9-9.1-12.1 7.4c-14.6 9-31.9 14.8-49.9 16.8-34 3.8-66.1-6-86-25.3l-5.4-5.3-7.5-0.3c-118.2-5.4-212.5-85.4-224.2-190.5-12-107.6 68-209.7 186.1-237.6l13.6-3.2 1.6-13.8c4.7-40.1 41.1-72.9 86.6-78 34.5-3.8 67.7 9 87.7 32.8l13.2 15.8 15.3-13.8c24.1-21.7 54.6-35.2 88.2-38.9 57.5-6.4 112.3 18.1 140.8 60.8l6.1 9.2 17.1-0.5c82.3 0 150.6 54.8 158.7 127.5 0.8 6.8 0.9 14.2 0.4 22.6l-0.8 12.9 11.5 6c42 21.9 68.6 56.7 72.9 95.8 8 75.9-66.6 146.5-166.6 157.7z"/>
<path fill="#7f7f7f" d="m240.4 673.3c-67.5 0-122.5 44.8-122.5 100 0 55.1 55 100 122.5 100s122.5-44.8 122.5-100c0-55.1-54.9-100-122.5-100zm0 160.3c-45.7 0-82.8-27-82.8-60.3 0-33.2 37.1-60.3 82.8-60.3s82.8 27 82.8 60.3c0 33.2-37.1 60.3-82.8 60.3z"/>
<path fill="#7f7f7f" d="m108.5 865.9c-37.1 0-67.3 25.5-67.3 56.9s30.2 56.9 67.3 56.9 67.3-25.5 67.3-56.9-30.2-56.9-67.3-56.9zm0 74c-15.8 0-27.6-9.1-27.6-17.2s11.3-17.2 27.6-17.2c15.8 0 27.6 9.1 27.6 17.2s-11.8 17.2-27.6 17.2z"/>
<path fill="#7f7f7f" d="m989.1 391c-5.4-48.7-35.5-92-83-120.4 0-5.9-0.3-11.5-0.9-16.8-10.2-91.8-93.6-161.2-194.7-162.8-38.1-50-102.5-77.1-171.9-69.3-35.3 4-68 16.6-95.5 36.8-28.5-25-67.5-37.4-108.8-32.8-58.6 6.5-106.6 47.2-119.1 99.1-130.6 36.7-217.5 153.8-203.8 277.4 13.7 122.4 120.2 216.1 254.6 225.3 28.3 24.1 69.3 35.8 112.4 31.1 19-2.1 37.5-7.5 54.1-15.7 31.8 22.3 72.4 34.4 115.8 34.4 8.6 0 17.2-0.5 25.8-1.4 62.8-7 117.7-38.7 147-84.1 12 1.6 24.1 2.4 36.2 2.4 10.1 0 20.3-0.6 30.4-1.7 122-13.6 212.3-104 201.4-201.5zm-205.6 162c-8.7 1-17.4 1.4-26 1.4-14.4 0-28.8-1.3-42.8-4l-14.5-2.7-6.8 13.2c-20.8 40.4-68.1 69.3-123.5 75.5-47 5.2-92.4-7.1-122.8-32.4l-10.9-9.1-12.1 7.4c-14.6 9-31.9 14.8-49.9 16.8-34 3.8-66.1-6-86-25.3l-5.4-5.3-7.5-0.3c-118.2-5.4-212.5-85.4-224.2-190.5-12-107.6 68-209.7 186.1-237.6l13.6-3.2 1.6-13.8c4.7-40.1 41.1-72.9 86.6-78 34.5-3.8 67.7 9 87.7 32.8l13.2 15.8 15.3-13.8c24.1-21.7 54.6-35.2 88.2-38.9 57.5-6.4 112.3 18.1 140.8 60.8l6.1 9.2 17.1-0.5c82.3 0 150.6 54.8 158.7 127.5 0.8 6.8 0.9 14.2 0.4 22.6l-0.8 12.9 11.5 6c42 21.9 68.6 56.7 72.9 95.8 8 75.9-66.6 146.5-166.6 157.7z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -17,6 +17,8 @@
max-width: var(--size);
max-width: var(--size);
}
.mbox-updated-contact .mbox-img,
.mbox-recommend-server .mbox-img {
--size: 4.5ch;
margin-left: 3ch;
@ -27,7 +29,6 @@
flex-basis: calc(100% - 64px - 1rem);
flex-grow: 0;
flex-shrink: 1;
max-width: 96ch;
word-break: break-word;
}
@ -38,15 +39,26 @@
margin-top: 0;
}
.mbox-header time,
.mbox-username {
.mbox-username,
.mbox-updated-contact,
.mbox mbox-recommend-server {
color: var(--color-accent);
}
.mbox-updated-contact .mbox-body,
.mbox-recommend-server .mbox-body {
display: block;
flex-basis: 100%;
font-size: var(--font-small);
overflow: scroll;
}
.mbox-updated-contact .mbox-header,
.mbox-recommend-server .mbox-header {
display: inline;
}
.mbox-updated-contact {
padding: 0 0 1rem 0;
margin: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

@ -1,4 +1,7 @@
form {
form,
.form {
display: flex;
flex-direction: column;
}
input,
@ -17,6 +20,7 @@ label {
margin-bottom: 0;
padding: 1.3rem 1.8rem;
text-indent: 0;
transition: background-color .25s;
}
label {
@ -25,11 +29,11 @@ label {
input[type="password"],
input[type="text"] {
background: var(--bgcolor-textinput);
border: .2rem solid #b7b7b7;
border-radius: .2rem;
display: block;
margin: 0;
width: 100%;
}
input[type="password"]:focus,
input[type="text"]:focus {
@ -78,6 +82,10 @@ button:focus {
font-size: 3.4rem;
}
.btn-danger {
background: var(--bgcolor-danger);
}
button:disabled {
background-color: var(--bgcolor-inactive);
cursor: default;
@ -95,6 +103,7 @@ button:disabled {
.form-inline {
display: flex;
flex-direction: row;
flex-grow: 1;
gap: 1rem;
}

@ -2,30 +2,30 @@
<html>
<head>
<meta charset="utf-8">
<title>nostr sandbox</title>
<title>nostr</title>
<link rel="stylesheet" href="main.css" type="text/css">
</head>
<body>
<main class="tabbed">
<input type="radio" name="maintabs" id="feed" class="tab" checked>
<label for="feed">feed</label>
<input type="radio" name="maintabs" id="trending" class="tab">
<!-- <input type="radio" name="maintabs" id="trending" class="tab">
<label for="trending">trending</label>
<input type="radio" name="maintabs" id="direct" class="tab">
<label for="direct">direct</label>
<input type="radio" name="maintabs" id="chat" class="tab">
<label for="chat">chat</label>
<label for="chat">chat</label> -->
<input type="radio" name="maintabs" id="settings" class="tab">
<label for="settings">settings</label>
<label for="settings">profile</label>
<div class="tabs">
<div class="tab-content">
<article class="mbox">
<img class="mbox-img" src="assets/comment.svg" alt="">
<img class="mbox-img" id="bubble" src="assets/comment.svg" alt="">
<div class="mbox-body" id="newMessage">
<form class="form-inline" id="writeForm">
<form action="#" class="form-inline" id="writeForm">
<input type="text" name="message">
<button type="button" id="publish" disabled>send</button>
<button type="submit" id="publish" disabled>send</button>
</form>
<small id="sendstatus" class="form-status"></small>
</div>
@ -46,6 +46,10 @@
</div>
<div class="tab-content">
<!-- <div class="form form-inline">
<input type="text" name="username" id="username" placeholder="username">
<button type="button" name="publish-username" tabindex="0">publish</button>
</div> -->
<form action="#" name="settings">
<label for="pubkey">public-key</label>
<input type="text" id="pubkey">

@ -24,23 +24,25 @@
@media (prefers-color-scheme: light) {
html {
--bgcolor: #fff;
--bgcolor-accent: #ff731d;
--bgcolor: #fdfefa;
--bgcolor-accent: #37ff1d;
--bgcolor-inactive: #bababa;
--bgcolor-textinput: #fff;
--color: rgb(68 68 68);
--color-accent: #ff731d;
--bgcolor-danger: rgb(255 0 0);
--bgcolor-danger: rgb(255, 80, 80);
}
}
@media (prefers-color-scheme: dark) {
html {
--bgcolor: #191919;
--bgcolor-accent: #2d4263;
--bgcolor-inactive: #535353;
--color: #c84b31;
--color-accent: #ecdbba;
--bgcolor-danger: rgb(255 0 0);
--bgcolor-accent: #1e437d;
--bgcolor-inactive: #333333;
--bgcolor-textinput: #0e0e0e;
--color: #fff;
--color-accent: #bbb;;
--bgcolor-danger: rgb(169, 0, 0);
}
img {
@ -81,14 +83,27 @@ time {
background-color: var(--bgcolor-danger);
}
a {
color: var(--color-accent);
}
a:focus {
border-radius: var(--focus-border-radius);
outline: var(--focus-outline);
outline-offset: 0;
}
a:visited {
color: darkmagenta;
}
img[alt] {
font-size: .9rem;
text-align: center;
word-break: break-all;
}
pre {
margin: 0;
padding: .5rem 0;
}

@ -4,23 +4,14 @@ import {dateTime, formatTime} from './timeutil.js';
// curl -H 'accept: application/nostr+json' https://nostr.x1ddos.ch
const pool = relayPool();
pool.addRelay('wss://relay.nostr.info', {read: true, write: true});
// pool.addRelay('wss://relay.damus.io', {read: true, write: true});
pool.addRelay('wss://relay.damus.io', {read: true, write: true});
pool.addRelay('wss://nostr.x1ddos.ch', {read: true, write: true});
// pool.addRelay('wss://nostr.openchain.fr', {read: true, write: true});
// pool.addRelay('wss://nostr.bitcoiner.social/', {read: true, write: true});
// read only
// pool.addRelay('wss://nostr.rocks', {read: true, write: false});
let max = 0;
function onEvent(evt, relay) {
if (evt.id === '209eefe6c940377fa8730853a75d1b4bb31bd929d79') {
console.log(evt)
}
// if (max++ >= 223) {
// return subscription.unsub();
// }
switch (evt.kind) {
case 0:
handleMetadata(evt, relay);
@ -32,7 +23,7 @@ function onEvent(evt, relay) {
handleRecommendServer(evt, relay);
break;
case 3:
updateContactList(evt, relay);
// handleContactList(evt, relay);
break;
case 7:
handleReaction(evt, relay);
@ -41,7 +32,13 @@ function onEvent(evt, relay) {
}
}
let pubkey = localStorage.getItem('pub_key')
let pubkey = localStorage.getItem('pub_key') || (() => {
const privatekey = generatePrivateKey();
const pubkey = getPublicKey(privatekey);
localStorage.setItem('private_key', privatekey);
localStorage.setItem('pub_key', pubkey);
return pubkey;
})();
const subscription = pool.sub({
cb: onEvent,
@ -55,7 +52,7 @@ const subscription = pool.sub({
// '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55
// ],
// since: new Date(Date.now() - (24 * 60 * 60 * 1000)),
limit: 400,
limit: 500,
}
});
@ -112,9 +109,10 @@ function handleReaction(evt, relay) {
const button = article.querySelector('button[name="star"]');
const reactions = button.querySelector('[data-reactions]');
reactions.textContent = reactionMap[eventId].length;
console.log(evt.pubkey, pubkey)
if (evt.pubkey === pubkey) {
button.querySelector('img[src$="star.svg"]').setAttribute('src', 'assets/star-fill.svg');
const star = button.querySelector('img[src$="star.svg"]');
star.setAttribute('src', 'assets/star-fill.svg');
star.setAttribute('title', reactionMap[eventId])
}
}
}
@ -131,14 +129,8 @@ const sortByCreatedAt = (evt1, evt2) => {
return evt1.created_at > evt2.created_at ? -1 : 1;
};
// let debounceDebugMessageTimer;
function renderFeed() {
const sortedFeeds = textNoteList.sort(sortByCreatedAt).reverse();
// debug
// clearTimeout(debounceDebugMessageTimer);
// debounceDebugMessageTimer = setTimeout(() => {
// console.log(`${sortedFeeds.reverse().map(e => dateTime.format(e.created_at * 1000)).join('\n')}`)
// }, 2000);
sortedFeeds.forEach((textNoteEvent, i) => {
if (feedDomMap[textNoteEvent.id]) {
// TODO check eventRelayMap if event was published to different relays
@ -178,22 +170,26 @@ function createTextNote(evt, relay) {
elem('strong', {className: 'mbox-username'}, userName),
' ',
elem('time', {dateTime: time.toISOString()}, formatTime(time)),
` kind:${evt.kind} ${evt.id}`,
]),
]),
elem('div', {data: isLongContent ? {append: evt.content.slice(280)} : null}, content),
elem('button', {
className: 'btn-inline', name: 'reply', type: 'button',
data: {'eventId': evt.id, relay},
}, [elem('img', {height: 24, width: 24, src: 'assets/comment.svg'})]),
elem('button', {
className: 'btn-inline', name: 'star', type: 'button',
data: {'eventId': evt.id, relay},
}, [
elem('img', {alt: didReact ? '✭' : '✩', height: 24, width: 24, src: `assets/${didReact ? 'star-fill' : 'star'}.svg`}), // ♥
elem('img', {
alt: didReact ? '✭' : '✩', // ♥
height: 24, width: 24,
src: `assets/${didReact ? 'star-fill' : 'star'}.svg`,
title: reactionMap[evt.id]?.map(({content}) => content).join(' '),
}),
elem('small', {data: {reactions: evt.id}}, hasReactions ? reactionMap[evt.id].length : ''),
]),
replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed) : '',
elem('button', {
className: 'btn-inline', name: 'reply', type: 'button',
data: {'eventId': evt.id, relay},
}, [elem('img', {height: 24, width: 24, src: 'assets/comment.svg'})]),
replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed.reverse()) : '',
]);
return rendernArticle([img, body]);
}
@ -210,8 +206,7 @@ function handleReply(evt, relay) {
function renderReply(evt, relay) {
const eventId = evt.tags[0][1]; // TODO: double check
const article = feedDomMap[eventId] || replyDomMap[eventId];
if (!article) {
// root article has not been rendered
if (!article) { // root article has not been rendered
return;
}
let replyContainer = article.querySelector('.mobx-replies');
@ -245,6 +240,45 @@ function handleRecommendServer(evt, relay) {
feedDomMap[evt.id] = art;
}
function handleContactList(evt, relay) {
if (feedDomMap[evt.id]) {
return;
}
const art = renderUpdateContact(evt, relay);
if (textNoteList.length < 2) {
feedContainer.append(art);
return;
}
const closestTextNotes = textNoteList.sort(sortEventCreatedAt(evt.created_at));
feedDomMap[closestTextNotes[0].id].after(art);
feedDomMap[evt.id] = art;
// const user = userList.find(u => u.pupkey === evt.pubkey);
// if (user) {
// console.log(`TODO: add contact list for ${evt.pubkey.slice(0, 8)} on ${relay}`, evt.tags);
// } else {
// tempContactList[relay] = tempContactList[relay]
// ? [...tempContactList[relay], evt]
// : [evt];
// }
}
function renderUpdateContact(evt, relay) {
const {img, time, userName} = getMetadata(evt, relay);
const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [
elem('header', {className: 'mbox-header'}, [
elem('small', {}, [
]),
]),
elem('pre', {title: JSON.stringify(evt.content)}, [
elem('strong', {}, userName),
' updated contacts: ',
JSON.stringify(evt.tags),
]),
]);
return rendernArticle([img, body], {className: 'mbox-updated-contact'});
}
function renderRecommendServer(evt, relay) {
const {img, time, userName} = getMetadata(evt, relay);
const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [
@ -264,7 +298,7 @@ function rendernArticle(content, props = {}) {
}
const userList = [];
const tempContactList = {};
// const tempContactList = {};
function handleMetadata(evt, relay) {
try {
@ -280,9 +314,7 @@ function setMetadata(evt, relay, content) {
const user = userList.find(u => u.pubkey === evt.pubkey);
if (!user) {
userList.push({
metadata: {
[relay]: content
},
metadata: {[relay]: content},
pubkey: evt.pubkey,
});
} else {
@ -292,12 +324,12 @@ function setMetadata(evt, relay, content) {
...content,
};
}
if (tempContactList[relay]) {
const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey);
if (updates) {
// if (tempContactList[relay]) {
// const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey);
// if (updates) {
// console.log('TODO: add contact list (kind 3)', updates);
}
}
// }
// }
}
const getHost = (url) => {
@ -326,19 +358,6 @@ function getMetadata(evt, relay) {
return {host, img, isReply, replies, time, userName};
}
function updateContactList(evt, relay) {
const user = userList.find(u => u.pupkey === evt.pubkey);
if (user) {
console.log(`TODO: add contact list for ${evt.pubkey.slice(0, 8)} on ${relay}`, evt.tags);
} else {
tempContactList[relay] = tempContactList[relay]
? [...tempContactList[relay], evt]
: [evt];
}
}
// check pool.status
// reply
const writeForm = document.querySelector('#writeForm');
const input = document.querySelector('input[name="message"]');
@ -351,8 +370,9 @@ feedContainer.addEventListener('click', (e) => {
lastReplyBtn.hidden = false;
}
lastReplyBtn = button;
button.hidden = true;
// button.hidden = true;
button.after(writeForm);
button.after(sendStatus);
writeForm.hidden = false;
replyTo = ['e', button.dataset.eventId, button.dataset.relay];
input.focus();
@ -364,6 +384,14 @@ feedContainer.addEventListener('click', (e) => {
}
});
const newMessageDiv = document.querySelector('#newMessage');
document.querySelector('#bubble').addEventListener('click', (e) => {
replyTo = null;
newMessageDiv.prepend(writeForm);
newMessageDiv.append(sendStatus);
input.focus();
});
async function upvote(eventId, relay) {
const privatekey = localStorage.getItem('private_key');
const newReaction = {
@ -393,7 +421,8 @@ const onSendError = err => {
sendStatus.hidden = false;
};
const publish = document.querySelector('#publish');
publish.addEventListener('click', async () => {
writeForm.addEventListener('submit', async (e) => {
e.preventDefault();
// const pubkey = localStorage.getItem('pub_key');
const privatekey = localStorage.getItem('private_key');
if (!pubkey || !privatekey) {
@ -424,7 +453,8 @@ publish.addEventListener('click', async () => {
lastReplyBtn.hidden = false;
lastReplyBtn = null;
replyTo = null;
document.querySelector('#newMessage').append(writeForm);
newMessageDiv.append(writeForm);
newMessageDiv.append(sendStatus);
}
// console.info(`event published by ${url}`, ev);
}
@ -433,6 +463,7 @@ publish.addEventListener('click', async () => {
});
input.addEventListener('input', () => publish.disabled = !input.value);
input.addEventListener('blur', () => sendStatus.textContent = '');
// settings
const form = document.querySelector('form[name="settings"]');

@ -1,4 +1,4 @@
.tabs { margin-top: 4rem; }
.tabs .tab-content { display: none; }
#feed:checked ~ .tabs .tab-content:nth-child(1),
#trending:checked ~ .tabs .tab-content:nth-child(2),
@ -43,9 +43,6 @@ input[type="radio"]:checked + label {
/*
.tabs {
position: relative;
}
.tab {
float: left;

Loading…
Cancel
Save