From 44c1af23810b0bb2f2aa59e8421f195ad8978a25 Mon Sep 17 00:00:00 2001 From: OFF0 Date: Tue, 13 Dec 2022 20:48:44 +0100 Subject: [PATCH 1/3] profile: update kind 0 name and picture Notes often miss name or picture of authors if the note was rendered before any metadata (kind 0) has been recieved. Update name and picture of existing notes. Pictures are only updated if a valid url. --- src/main.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main.js b/src/main.js index 5d5baf9..4381ccd 100644 --- a/src/main.js +++ b/src/main.js @@ -244,7 +244,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' : ''}`, data: {pubkey: evt.pubkey.slice(0, 12)}}, name || userName), ' ', elem('time', {dateTime: time.toISOString()}, formatTime(time)), ]), @@ -400,23 +400,40 @@ function handleMetadata(evt, relay) { } 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) { + feedContainer + .querySelectorAll(`canvas[data-pubkey="${evt.pubkey.slice(0, 12)}"]`) + .forEach(canvas => (canvas.parentNode.replaceChild(elem('img', {src: user.picture}), canvas))); + } + if (user.metadata[relay].name) { + feedContainer + .querySelectorAll(`.mbox-username[data-pubkey="${evt.pubkey.slice(0, 12)}"]`) + .forEach(username => { + username.textContent = user.metadata[relay].name; + username.classList.add('mbox-kind0-name'); + username.removeAttribute('data-pubkey'); + }); } // if (tempContactList[relay]) { // const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey); @@ -443,7 +460,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.slice(0, 12)}}); const context = canvas.getContext('2d'); const color = `#${text.slice(0, 6)}`; context.fillStyle = color; -- 2.46.2 From 23619bbaaa3a9d36596fe101ae4d99697537c35d Mon Sep 17 00:00:00 2001 From: OFF0 Date: Fri, 16 Dec 2022 12:44:05 +0100 Subject: [PATCH 2/3] noxy: log more useful response error --- src/main.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 4381ccd..7b423a1 100644 --- a/src/main.js +++ b/src/main.js @@ -210,7 +210,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; }; -- 2.46.2 From cd99b5e5c185ab2c8efa1bf92a6be4c1efa2af73 Mon Sep 17 00:00:00 2001 From: OFF0 Date: Tue, 13 Dec 2022 20:51:34 +0100 Subject: [PATCH 3/3] feed: add deeplinking and browser history Added deeplinks with browser history support. Each note and author have now a detail view under nostr.ch/ in a future commit also /e/ and /p/ could be supported. User can now navigate with browser back (with the expection of the settings overlay). Not everything is supported in the detail view (yet) i.e reply and stars are partially working (dont update visually), leaving this as open bug. This should fix itself once only 1 render container is used instead of different divs in the html for each view. Ideally the detail view should also query for related events, something to add in a future commit --- src/cards.css | 5 +- src/form.css | 6 +- src/index.html | 11 ++ src/main.css | 2 + src/main.js | 297 ++++++++++++++++++++++++++++++++++++++----------- src/tabs.css | 3 - 6 files changed, 252 insertions(+), 72 deletions(-) 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 @@
+
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 7b423a1..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 @@ -245,7 +437,7 @@ function createTextNote(evt, relay) { ${evt.content}` }, [ elem('small', {}, [ - elem('strong', {className: `mbox-username${name ? ' mbox-kind0-name' : ''}`, data: {pubkey: evt.pubkey.slice(0, 12)}}, name || userName), + elem('strong', {className: `mbox-username${name ? ' mbox-kind0-name' : ''}`}, name || userName), ' ', elem('time', {dateTime: time.toISOString()}, formatTime(time)), ]), @@ -276,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) { @@ -364,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) { @@ -377,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); } @@ -390,16 +582,22 @@ 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) { let user = userList.find(u => u.pubkey === evt.pubkey); const picture = getNoxyUrl('data', content.picture, evt.id, relay).href; @@ -423,17 +621,16 @@ function setMetadata(evt, relay, content) { } // update profile images if (user.picture) { - feedContainer - .querySelectorAll(`canvas[data-pubkey="${evt.pubkey.slice(0, 12)}"]`) + document.body + .querySelectorAll(`canvas[data-pubkey="${evt.pubkey}"]`) .forEach(canvas => (canvas.parentNode.replaceChild(elem('img', {src: user.picture}), canvas))); } if (user.metadata[relay].name) { - feedContainer - .querySelectorAll(`.mbox-username[data-pubkey="${evt.pubkey.slice(0, 12)}"]`) + 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'); - username.removeAttribute('data-pubkey'); }); } // if (tempContactList[relay]) { @@ -461,7 +658,7 @@ const getHost = (url) => { } const elemCanvas = (text) => { - const canvas = elem('canvas', {height: 80, width: 80, data: {pubkey: text.slice(0, 12)}}); + 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; @@ -472,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; } @@ -495,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"]'); @@ -730,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 -- 2.46.2