diff --git a/src/cards.css b/src/cards.css
index 8e2dc4e..2f99bf4 100644
--- a/src/cards.css
+++ b/src/cards.css
@@ -49,11 +49,13 @@
}
.mbox-body {
- flex-basis: calc(100% - 64px - 1rem);
flex-grow: 0;
flex-shrink: 1;
word-break: break-word;
}
+.mbox-img + .mbox-body {
+ flex-basis: calc(100% - 64px - 1rem);
+}
.mbox-header {
flex-basis: calc(100% - 64px - 1rem);
@@ -64,6 +66,7 @@
.mbox-header time,
.mbox-username {
color: var(--color-accent);
+ cursor: pointer;
}
.mbox-kind0-name {
diff --git a/src/form.css b/src/form.css
index 4863d74..5a0151b 100644
--- a/src/form.css
+++ b/src/form.css
@@ -168,9 +168,9 @@ button:disabled {
.form-inline * + * {
margin-left: var(--gap);
}
-.cards .form-inline button,
-.cards .form-inline input[type="text"],
-.cards .form-inline textarea {
+.form-inline button,
+.form-inline input[type="text"],
+.form-inline textarea {
margin: .4rem 0;
}
diff --git a/src/index.html b/src/index.html
index ca800a5..23f0f3e 100644
--- a/src/index.html
+++ b/src/index.html
@@ -41,6 +41,17 @@
+
+
+
+
+
+
+
- pubkey
+
+
+
+
diff --git a/src/main.css b/src/main.css
index f10f73c..a60d2f1 100644
--- a/src/main.css
+++ b/src/main.css
@@ -70,6 +70,8 @@ body {
margin: 0;
}
+h1, h2, h3, h4, h5 { font-weight: normal; }
+
body,
button,
input,
diff --git a/src/main.js b/src/main.js
index 5d5baf9..fab7095 100644
--- a/src/main.js
+++ b/src/main.js
@@ -2,15 +2,11 @@ import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tool
import {elem, parseTextContent} from './domutil.js';
import {dateTime, formatTime} from './timeutil.js';
// curl -H 'accept: application/nostr+json' https://relay.nostr.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://nostr.x1ddos.ch', {read: true, write: true});
pool.addRelay('wss://relay.nostr.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});
function onEvent(evt, relay) {
switch (evt.kind) {
@@ -41,20 +37,216 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
return pubkey;
})();
-const subscription = pool.sub({
- cb: onEvent,
- filter: {
- // authors: [
- // '52155da703585f25053830ac39863c80ea6adc57b360472c16c566a412d2bc38', // quark
- // 'a6057742e73ff93b89587c27a74edf2cdab86904291416e90dc98af1c5f70cfa', // mosc
- // '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d', // fiatjaf
- // '52155da703585f25053830ac39863c80ea6adc57b360472c16c566a412d2bc38', // x1ddos
- // // pubkey, // me
- // '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55
- // ],
- // since: new Date(Date.now() - (24 * 60 * 60 * 1000)),
- limit: 250,
+const subList = [];
+const unSubAll = () => {
+ subList.forEach(sub => sub.unsub());
+ subList.length = 0;
+};
+
+window.addEventListener('popstate', (event) => {
+ // console.log(`popstate path: ${location.pathname}, state: ${JSON.stringify(event.state)}`);
+ unSubAll();
+ if (event.state?.author) {
+ subProfile(event.state.author);
+ return;
+ }
+ if (event.state?.pubOrEvt) {
+ subNoteAndProfile(event.state.pubOrEvt);
+ return;
+ }
+ if (event.state?.eventId) {
+ subTextNote(event.state.eventId);
+ return;
+ }
+ sub24hFeed();
+ showFeed();
+});
+
+switch(location.pathname) {
+ case '/':
+ history.pushState({}, '', '/');
+ sub24hFeed();
+ break;
+ default:
+ const pubOrEvt = location.pathname.slice(1);
+ if (pubOrEvt.length === 64 && pubOrEvt.match(/^[0-9a-f]+$/)) {
+ history.pushState({pubOrEvt}, '', `/${pubOrEvt}`);
+ subNoteAndProfile(pubOrEvt);
+ }
+ break;
+}
+
+function sub24hFeed() {
+ subList.push(pool.sub({
+ cb: onEvent,
+ filter: {
+ kinds: [0, 1, 2, 7],
+ // until: Math.floor(Date.now() * 0.001),
+ since: Math.floor((Date.now() * 0.001) - (24 * 60 * 60)),
+ limit: 100,
+ }
+ }));
+}
+
+function subNoteAndProfile(id) {
+ subProfile(id);
+ subTextNote(id);
+}
+
+function subTextNote(eventId) {
+ subList.push(pool.sub({
+ cb: (evt, relay) => {
+ clearTextNoteDetail();
+ showTextNoteDetail(evt, relay);
+ },
+ filter: {
+ ids: [eventId],
+ kinds: [1],
+ limit: 1,
+ }
+ }));
+}
+
+function subProfile(pubkey) {
+ subList.push(pool.sub({
+ cb: (evt, relay) => {
+ renderProfile(evt, relay);
+ showProfileDetail();
+ },
+ filter: {
+ authors: [pubkey],
+ kinds: [0],
+ limit: 1,
+ }
+ }));
+ // get notes for profile
+ subList.push(pool.sub({
+ cb: (evt, relay) => {
+ showTextNoteDetail(evt, relay);
+ showProfileDetail();
+ },
+ filter: {
+ authors: [pubkey],
+ kinds: [1],
+ limit: 150,
+ }
+ }));
+}
+
+const detailContainer = document.querySelector('#detail');
+const profileContainer = document.querySelector('#profile');
+const profileAbout = profileContainer.querySelector('.profile-about');
+const profileName = profileContainer.querySelector('.profile-name');
+const profilePubkey = profileContainer.querySelector('.profile-pubkey');
+const profilePubkeyLabel = profileContainer.querySelector('.profile-pubkey-label');
+const profileImage = profileContainer.querySelector('.profile-image');
+const textNoteContainer = document.querySelector('#textnote');
+
+function clearProfile() {
+ profileAbout.textContent = '';
+ profileName.textContent = '';
+ profilePubkey.textContent = '';
+ profilePubkeyLabel.hidden = true;
+}
+function renderProfile(evt, relay) {
+ profileContainer.dataset.pubkey = evt.pubkey;
+ profilePubkey.textContent = evt.pubkey;
+ profilePubkeyLabel.hidden = false;
+ const content = parseContent(evt.content);
+ if (content) {
+ profileAbout.textContent = content.about;
+ profileName.textContent = content.name;
+ const noxyImg = getNoxyUrl('data', content.picture, evt.id, relay);
+ if (noxyImg) {
+ profileImage.setAttribute('src', getNoxyUrl('data', noxyImg, evt.id, relay))
+ }
+ }
+}
+
+function showProfileDetail() {
+ profileContainer.hidden = false;
+ textNoteContainer.hidden = false;
+ showDetail();
+}
+
+function clearTextNoteDetail() {
+ textNoteContainer.replaceChildren([]);
+}
+
+function showTextNoteDetail(evt, relay) {
+ if (!textNoteContainer.querySelector(`[data-id="${evt.id}"]`)) {
+ textNoteContainer.append(createTextNote(evt, relay));
+ }
+ textNoteContainer.hidden = false;
+ profileContainer.hidden = true;
+ showDetail();
+}
+
+function showDetail() {
+ feedContainer.hidden = true;
+ detailContainer.hidden = false;
+}
+
+function showFeed() {
+ feedContainer.hidden = false;
+ detailContainer.hidden = true;
+}
+
+document.querySelector('label[for="feed"]').addEventListener('click', () => {
+ if (location.pathname !== '/') {
+ showFeed();
+ history.pushState({}, '', '/');
+ unSubAll();
+ sub24hFeed();
+ }
+});
+
+document.body.addEventListener('click', (e) => {
+ const button = e.target.closest('button');
+ const pubkey = e.target.closest('[data-pubkey]')?.dataset.pubkey;
+ const id = e.target.closest('[data-id]')?.dataset.id;
+ const relay = e.target.closest('[data-relay]')?.dataset.relay;
+ if (button && button.name === 'reply') {
+ if (localStorage.getItem('reply_to') === id) {
+ writeInput.blur();
+ return;
+ }
+ appendReplyForm(button);
+ localStorage.setItem('reply_to', id);
+ return;
}
+ if (button && button.name === 'star') {
+ upvote(id, relay)
+ return;
+ }
+ if (button && button.name === 'back') {
+ hideNewMessage(true);
+ return;
+ }
+ const username = e.target.closest('.mbox-username')
+ if (username) {
+ history.pushState({author: pubkey}, '', `/${pubkey}`);
+ unSubAll();
+ clearProfile();
+ clearTextNoteDetail();
+ subProfile(pubkey);
+ showProfileDetail();
+ return;
+ }
+ const eventTime = e.target.closest('.mbox-header time');
+ if (eventTime) {
+ history.pushState({eventId: id, relay}, '', `/${id}`);
+ unSubAll();
+ clearTextNoteDetail();
+ subTextNote(id);
+ return;
+ }
+ // const container = e.target.closest('[data-append]');
+ // if (container) {
+ // container.append(...parseTextContent(container.dataset.append));
+ // delete container.dataset.append;
+ // return;
+ // }
});
const textNoteList = []; // could use indexDB
@@ -210,7 +402,8 @@ const fetchNext = (href, id, relay) => {
return fetchNext(href, id, relay);
}
})
- .catch(console.warn);
+ .catch(err => err.text && err.text())
+ .then(errMsg => errMsg && console.warn(errMsg));
return previewId;
};
@@ -244,7 +437,7 @@ function createTextNote(evt, relay) {
${evt.content}`
}, [
elem('small', {}, [
- elem('strong', {className: name ? 'mbox-kind0-name' : 'mbox-username'}, name || userName),
+ elem('strong', {className: `mbox-username${name ? ' mbox-kind0-name' : ''}`}, name || userName),
' ',
elem('time', {dateTime: time.toISOString()}, formatTime(time)),
]),
@@ -275,10 +468,10 @@ function createTextNote(evt, relay) {
appendReplyForm(body.querySelector('button[name="reply"]'));
requestAnimationFrame(() => updateElemHeight(writeInput));
}
- return rendernArticle([
+ return renderArticle([
elem('div', {className: 'mbox-img'}, [img]), body,
replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed.reverse()) : '',
- ]);
+ ], {data: {id: evt.id, pubkey: evt.pubkey, relay}});
}
function handleReply(evt, relay) {
@@ -363,7 +556,7 @@ function renderUpdateContact(evt, relay) {
JSON.stringify(evt.tags),
]),
]);
- return rendernArticle([img, body], {className: 'mbox-updated-contact'});
+ return renderArticle([img, body], {className: 'mbox-updated-contact', data: {id: evt.id, pubkey: evt.pubkey, relay}});
}
function renderRecommendServer(evt, relay) {
@@ -376,12 +569,12 @@ function renderRecommendServer(evt, relay) {
]),
` recommends server: ${evt.content}`,
]);
- return rendernArticle([
+ return renderArticle([
elem('div', {className: 'mbox-img'}, [img]), body
- ], {className: 'mbox-recommend-server', data: {relay: evt.content}});
+ ], {className: 'mbox-recommend-server', data: {id: evt.id, pubkey: evt.pubkey}});
}
-function rendernArticle(content, props = {}) {
+function renderArticle(content, props = {}) {
const className = props.className ? ['mbox', props?.className].join(' ') : 'mbox';
return elem('article', {...props, className}, content);
}
@@ -389,34 +582,56 @@ function rendernArticle(content, props = {}) {
const userList = [];
// const tempContactList = {};
-function handleMetadata(evt, relay) {
+function parseContent(content) {
try {
- const content = JSON.parse(evt.content);
- setMetadata(evt, relay, content);
+ return JSON.parse(content);
} catch(err) {
console.log(evt);
console.error(err);
}
}
+function handleMetadata(evt, relay) {
+ const content = parseContent(evt.content);
+ if (content) {
+ setMetadata(evt, relay, content);
+ }
+}
+
function setMetadata(evt, relay, content) {
- const user = userList.find(u => u.pubkey === evt.pubkey);
+ let user = userList.find(u => u.pubkey === evt.pubkey);
const picture = getNoxyUrl('data', content.picture, evt.id, relay).href;
if (!user) {
- userList.push({
+ user = {
metadata: {[relay]: content},
...(content.picture && {picture}),
pubkey: evt.pubkey,
- });
+ };
+ userList.push(user);
} else {
user.metadata[relay] = {
...user.metadata[relay],
timestamp: evt.created_at,
...content,
};
+ // use only the first profile pic (for now), different pics on each releay are not supported yet
if (!user.picture) {
user.picture = picture;
- } // no support (yet) for other picture from same pubkey on different relays
+ }
+ }
+ // update profile images
+ if (user.picture) {
+ document.body
+ .querySelectorAll(`canvas[data-pubkey="${evt.pubkey}"]`)
+ .forEach(canvas => (canvas.parentNode.replaceChild(elem('img', {src: user.picture}), canvas)));
+ }
+ if (user.metadata[relay].name) {
+ document.body
+ .querySelectorAll(`[data-id="${evt.pubkey}"] .mbox-username:not(.mbox-kind0-name)`)
+ .forEach(username => {
+ username.textContent = user.metadata[relay].name;
+ username.classList.add('mbox-kind0-name');
+ });
}
// if (tempContactList[relay]) {
// const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey);
@@ -443,7 +658,7 @@ const getHost = (url) => {
}
const elemCanvas = (text) => {
- const canvas = elem('canvas', {height: 80, width: 80});
+ const canvas = elem('canvas', {height: 80, width: 80, data: {pubkey: text}});
const context = canvas.getContext('2d');
const color = `#${text.slice(0, 6)}`;
context.fillStyle = color;
@@ -454,7 +669,7 @@ const elemCanvas = (text) => {
if (color === '#000000') {
context.fillStyle = '#fff';
}
- context.fillText(text, 2, 46);
+ context.fillText(text.slice(0, 8), 2, 46);
return canvas;
}
@@ -477,23 +692,6 @@ function getMetadata(evt, relay) {
return {host, img, isReply, name, replies, time, userName};
}
-feedContainer.addEventListener('click', (e) => {
- const button = e.target.closest('button');
- if (button && button.name === 'reply') {
- if (localStorage.getItem('reply_to') === button.dataset.eventId) {
- writeInput.blur();
- return;
- }
- appendReplyForm(button);
- localStorage.setItem('reply_to', button.dataset.eventId);
- return;
- }
- if (button && button.name === 'star') {
- upvote(button.dataset.eventId, button.dataset.relay)
- return;
- }
-});
-
const writeForm = document.querySelector('#writeForm');
const writeInput = document.querySelector('textarea[name="message"]');
@@ -712,19 +910,6 @@ privateTgl.addEventListener('click', () => {
privateKeyInput.value = localStorage.getItem('private_key');
pubKeyInput.value = localStorage.getItem('pub_key');
-document.body.addEventListener('click', (e) => {
- // const container = e.target.closest('[data-append]');
- // if (container) {
- // container.append(...parseTextContent(container.dataset.append));
- // delete container.dataset.append;
- // return;
- // }
- const back = e.target.closest('[name="back"]')
- if (back) {
- hideNewMessage(true);
- }
-});
-
// profile
const profileForm = document.querySelector('form[name="profile"]');
const profileSubmit = profileForm.querySelector('button[type="submit"]');
diff --git a/src/tabs.css b/src/tabs.css
index da7bbec..57a0c07 100644
--- a/src/tabs.css
+++ b/src/tabs.css
@@ -73,7 +73,4 @@ input[type="radio"]:checked + label {
margin-left: var(--gap);
order: 2;
}
- .cards {
-
- }
}
\ No newline at end of file