add deeplinks and support browser history #45

Merged
offbyn merged 3 commits from deeplink-notes-and-profile into master 2 years ago

@ -49,11 +49,13 @@
} }
.mbox-body { .mbox-body {
flex-basis: calc(100% - 64px - 1rem);
flex-grow: 0; flex-grow: 0;
flex-shrink: 1; flex-shrink: 1;
word-break: break-word; word-break: break-word;
} }
.mbox-img + .mbox-body {
flex-basis: calc(100% - 64px - 1rem);
}
.mbox-header { .mbox-header {
flex-basis: calc(100% - 64px - 1rem); flex-basis: calc(100% - 64px - 1rem);
@ -64,6 +66,7 @@
.mbox-header time, .mbox-header time,
.mbox-username { .mbox-username {
color: var(--color-accent); color: var(--color-accent);
cursor: pointer;
} }
.mbox-kind0-name { .mbox-kind0-name {

@ -168,9 +168,9 @@ button:disabled {
.form-inline * + * { .form-inline * + * {
margin-left: var(--gap); margin-left: var(--gap);
} }
.cards .form-inline button, .form-inline button,
.cards .form-inline input[type="text"], .form-inline input[type="text"],
.cards .form-inline textarea { .form-inline textarea {
margin: .4rem 0; margin: .4rem 0;
} }

@ -41,6 +41,17 @@
</div> </div>
</artcile> </artcile>
<div class="cards" id="homefeed"></div> <div class="cards" id="homefeed"></div>
<div id="detail" hidden>
<article class="mbox" id="profile" data-pubkey>
<div class="mbox-body">
<img class="profile-image">
<h2 class="profile-name mbox-username"></h2>
<p class="profile-about"></p>
<dl><dt class="profile-pubkey-label" hidden>pubkey</dt><dd class="profile-pubkey"></dd></dl>
</div>
</article>
<section id="textnote"></section>
</div>
</div> </div>
<div class="tab-content"> <div class="tab-content">

@ -70,6 +70,8 @@ body {
margin: 0; margin: 0;
} }
h1, h2, h3, h4, h5 { font-weight: normal; }
body, body,
button, button,
input, input,

@ -2,15 +2,11 @@ import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tool
import {elem, parseTextContent} from './domutil.js'; import {elem, parseTextContent} from './domutil.js';
import {dateTime, formatTime} from './timeutil.js'; import {dateTime, formatTime} from './timeutil.js';
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ // curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
const pool = relayPool(); const pool = relayPool();
pool.addRelay('wss://relay.nostr.info', {read: true, write: true}); 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://relay.nostr.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) { function onEvent(evt, relay) {
switch (evt.kind) { switch (evt.kind) {
@ -41,22 +37,218 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
return pubkey; return pubkey;
})(); })();
const subscription = pool.sub({ 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, cb: onEvent,
filter: { filter: {
// authors: [ kinds: [0, 1, 2, 7],
// '52155da703585f25053830ac39863c80ea6adc57b360472c16c566a412d2bc38', // quark // until: Math.floor(Date.now() * 0.001),
// 'a6057742e73ff93b89587c27a74edf2cdab86904291416e90dc98af1c5f70cfa', // mosc since: Math.floor((Date.now() * 0.001) - (24 * 60 * 60)),
// '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d', // fiatjaf limit: 100,
// '52155da703585f25053830ac39863c80ea6adc57b360472c16c566a412d2bc38', // x1ddos }
// // pubkey, // me }));
// '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55 }
// ],
// since: new Date(Date.now() - (24 * 60 * 60 * 1000)), function subNoteAndProfile(id) {
limit: 250, 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 const textNoteList = []; // could use indexDB
const eventRelayMap = {}; // eventId: [relay1, relay2] const eventRelayMap = {}; // eventId: [relay1, relay2]
const hasEventTag = tag => tag[0] === 'e'; const hasEventTag = tag => tag[0] === 'e';
@ -210,7 +402,8 @@ const fetchNext = (href, id, relay) => {
return 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; return previewId;
}; };
@ -244,7 +437,7 @@ function createTextNote(evt, relay) {
${evt.content}` ${evt.content}`
}, [ }, [
elem('small', {}, [ 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)), elem('time', {dateTime: time.toISOString()}, formatTime(time)),
]), ]),
@ -275,10 +468,10 @@ function createTextNote(evt, relay) {
appendReplyForm(body.querySelector('button[name="reply"]')); appendReplyForm(body.querySelector('button[name="reply"]'));
requestAnimationFrame(() => updateElemHeight(writeInput)); requestAnimationFrame(() => updateElemHeight(writeInput));
} }
return rendernArticle([ return renderArticle([
elem('div', {className: 'mbox-img'}, [img]), body, elem('div', {className: 'mbox-img'}, [img]), body,
replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed.reverse()) : '', replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed.reverse()) : '',
]); ], {data: {id: evt.id, pubkey: evt.pubkey, relay}});
} }
function handleReply(evt, relay) { function handleReply(evt, relay) {
@ -363,7 +556,7 @@ function renderUpdateContact(evt, relay) {
JSON.stringify(evt.tags), 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) { function renderRecommendServer(evt, relay) {
@ -376,12 +569,12 @@ function renderRecommendServer(evt, relay) {
]), ]),
` recommends server: ${evt.content}`, ` recommends server: ${evt.content}`,
]); ]);
return rendernArticle([ return renderArticle([
elem('div', {className: 'mbox-img'}, [img]), body 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'; const className = props.className ? ['mbox', props?.className].join(' ') : 'mbox';
return elem('article', {...props, className}, content); return elem('article', {...props, className}, content);
} }
@ -389,34 +582,56 @@ function rendernArticle(content, props = {}) {
const userList = []; const userList = [];
// const tempContactList = {}; // const tempContactList = {};
function handleMetadata(evt, relay) { function parseContent(content) {
try { try {
const content = JSON.parse(evt.content); return JSON.parse(content);
setMetadata(evt, relay, content);
} catch(err) { } catch(err) {
console.log(evt); console.log(evt);
console.error(err); console.error(err);
} }
} }
function handleMetadata(evt, relay) {
const content = parseContent(evt.content);
if (content) {
setMetadata(evt, relay, content);
}
}
function 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; const picture = getNoxyUrl('data', content.picture, evt.id, relay).href;
if (!user) { if (!user) {
userList.push({ user = {
metadata: {[relay]: content}, metadata: {[relay]: content},
...(content.picture && {picture}), ...(content.picture && {picture}),
pubkey: evt.pubkey, pubkey: evt.pubkey,
}); };
userList.push(user);
} else { } else {
user.metadata[relay] = { user.metadata[relay] = {
...user.metadata[relay], ...user.metadata[relay],
timestamp: evt.created_at, timestamp: evt.created_at,
...content, ...content,
}; };
// use only the first profile pic (for now), different pics on each releay are not supported yet
if (!user.picture) { if (!user.picture) {
user.picture = 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]) { // if (tempContactList[relay]) {
// const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey); // const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey);
@ -443,7 +658,7 @@ const getHost = (url) => {
} }
const elemCanvas = (text) => { 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 context = canvas.getContext('2d');
const color = `#${text.slice(0, 6)}`; const color = `#${text.slice(0, 6)}`;
context.fillStyle = color; context.fillStyle = color;
@ -454,7 +669,7 @@ const elemCanvas = (text) => {
if (color === '#000000') { if (color === '#000000') {
context.fillStyle = '#fff'; context.fillStyle = '#fff';
} }
context.fillText(text, 2, 46); context.fillText(text.slice(0, 8), 2, 46);
return canvas; return canvas;
} }
@ -477,23 +692,6 @@ function getMetadata(evt, relay) {
return {host, img, isReply, name, replies, time, userName}; 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 writeForm = document.querySelector('#writeForm');
const writeInput = document.querySelector('textarea[name="message"]'); const writeInput = document.querySelector('textarea[name="message"]');
@ -712,19 +910,6 @@ privateTgl.addEventListener('click', () => {
privateKeyInput.value = localStorage.getItem('private_key'); privateKeyInput.value = localStorage.getItem('private_key');
pubKeyInput.value = localStorage.getItem('pub_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 // profile
const profileForm = document.querySelector('form[name="profile"]'); const profileForm = document.querySelector('form[name="profile"]');
const profileSubmit = profileForm.querySelector('button[type="submit"]'); const profileSubmit = profileForm.querySelector('button[type="submit"]');

@ -73,7 +73,4 @@ input[type="radio"]:checked + label {
margin-left: var(--gap); margin-left: var(--gap);
order: 2; order: 2;
} }
.cards {
}
} }
Loading…
Cancel
Save