|
|
|
@ -14,27 +14,6 @@ import {getMetadata, handleMetadata} from './profiles';
|
|
|
|
|
|
|
|
|
|
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
|
|
|
|
|
|
|
|
|
|
function onEvent(evt: Event, relay: string) {
|
|
|
|
|
switch (evt.kind) {
|
|
|
|
|
case 0:
|
|
|
|
|
handleMetadata(evt, relay);
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
handleTextNote(evt, relay);
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
handleRecommendServer(evt, relay);
|
|
|
|
|
break;
|
|
|
|
|
case 3:
|
|
|
|
|
// handleContactList(evt, relay);
|
|
|
|
|
break;
|
|
|
|
|
case 7:
|
|
|
|
|
handleReaction(evt, relay);
|
|
|
|
|
default:
|
|
|
|
|
// console.log(`TODO: add support for event kind ${evt.kind}`/*, evt*/)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EventWithNip19 = Event & {
|
|
|
|
|
nip19: {
|
|
|
|
|
note: string;
|
|
|
|
@ -48,6 +27,62 @@ type EventRelayMap = {
|
|
|
|
|
};
|
|
|
|
|
const eventRelayMap: EventRelayMap = {}; // eventId: [relay1, relay2]
|
|
|
|
|
|
|
|
|
|
type EventWithNip19AndReplyTo = EventWithNip19 & {
|
|
|
|
|
replyTo: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const replyList: Array<EventWithNip19AndReplyTo> = [];
|
|
|
|
|
|
|
|
|
|
const createTextNote = (evt: EventWithNip19, relay: string) => {
|
|
|
|
|
const {host, img, name, time, userName} = getMetadata(evt, relay);
|
|
|
|
|
const replies = replyList.filter(({replyTo}) => replyTo === evt.id);
|
|
|
|
|
// const isLongContent = evt.content.trimRight().length > 280;
|
|
|
|
|
// const content = isLongContent ? evt.content.slice(0, 280) : evt.content;
|
|
|
|
|
const reactions = getReactions(evt.id);
|
|
|
|
|
const didReact = reactions.length && !!reactions.find(reaction => reaction.pubkey === config.pubkey);
|
|
|
|
|
const replyFeed: Array<HTMLElement> = replies[0] ? replies.sort(sortByCreatedAt).map(e => setViewElem(e.id, createTextNote(e, relay))) : [];
|
|
|
|
|
const [content, {firstLink}] = parseTextContent(evt.content);
|
|
|
|
|
const buttons = elem('div', {className: 'buttons'}, [
|
|
|
|
|
elem('button', {name: 'reply', type: 'button'}, [
|
|
|
|
|
elem('img', {height: 24, width: 24, src: '/assets/comment.svg'})
|
|
|
|
|
]),
|
|
|
|
|
elem('button', {name: 'star', type: 'button'}, [
|
|
|
|
|
elem('img', {
|
|
|
|
|
alt: didReact ? '✭' : '✩', // ♥
|
|
|
|
|
height: 24, width: 24,
|
|
|
|
|
src: `/assets/${didReact ? 'star-fill' : 'star'}.svg`,
|
|
|
|
|
title: getReactionContents(evt.id).join(' '),
|
|
|
|
|
}),
|
|
|
|
|
elem('small', {data: {reactions: ''}}, reactions.length || ''),
|
|
|
|
|
]),
|
|
|
|
|
]);
|
|
|
|
|
const body = elem('div', {className: 'mbox-body'}, [
|
|
|
|
|
elem('header', {
|
|
|
|
|
className: 'mbox-header',
|
|
|
|
|
title: `User: ${userName}\n${time}\n\nUser pubkey: ${evt.pubkey}\n\nRelay: ${host}\n\nEvent-id: ${evt.id}
|
|
|
|
|
${evt.tags.length ? `\nTags ${JSON.stringify(evt.tags)}\n` : ''}
|
|
|
|
|
${evt.content}`
|
|
|
|
|
}, [
|
|
|
|
|
elem('a', {className: `mbox-username${name ? ' mbox-kind0-name' : ''}`, href: `/${evt.nip19.npub}`}, name || userName),
|
|
|
|
|
' ',
|
|
|
|
|
elem('a', {href: `/${evt.nip19.note}`}, elem('time', {dateTime: time.toISOString()}, formatTime(time))),
|
|
|
|
|
]),
|
|
|
|
|
elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, [
|
|
|
|
|
...content,
|
|
|
|
|
(firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : null,
|
|
|
|
|
]),
|
|
|
|
|
buttons,
|
|
|
|
|
]);
|
|
|
|
|
if (localStorage.getItem('reply_to') === evt.id) {
|
|
|
|
|
openWriteInput(buttons, evt.id);
|
|
|
|
|
}
|
|
|
|
|
return elemArticle([
|
|
|
|
|
elem('div', {className: 'mbox-img'}, img),
|
|
|
|
|
body,
|
|
|
|
|
...(replies[0] ? [elem('div', {className: 'mobx-replies'}, replyFeed.reverse())] : []),
|
|
|
|
|
], {data: {id: evt.id, pubkey: evt.pubkey, relay}});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderNote = (
|
|
|
|
|
evt: EventWithNip19,
|
|
|
|
|
i: number,
|
|
|
|
@ -84,36 +119,22 @@ const renderFeed = bounce(() => {
|
|
|
|
|
.forEach(renderNote);
|
|
|
|
|
}, 17); // (16.666 rounded, an arbitrary value to limit updates to max 60x per s)
|
|
|
|
|
|
|
|
|
|
function handleTextNote(evt: Event, relay: string) {
|
|
|
|
|
if (eventRelayMap[evt.id]) {
|
|
|
|
|
eventRelayMap[evt.id] = [...(eventRelayMap[evt.id]), relay]; // TODO: just push?
|
|
|
|
|
} else {
|
|
|
|
|
eventRelayMap[evt.id] = [relay];
|
|
|
|
|
const evtWithNip19 = {
|
|
|
|
|
nip19: {
|
|
|
|
|
note: nip19.noteEncode(evt.id),
|
|
|
|
|
npub: nip19.npubEncode(evt.pubkey),
|
|
|
|
|
},
|
|
|
|
|
...evt
|
|
|
|
|
};
|
|
|
|
|
if (evt.tags.some(hasEventTag)) {
|
|
|
|
|
handleReply(evtWithNip19, relay);
|
|
|
|
|
} else {
|
|
|
|
|
textNoteList.push(evtWithNip19);
|
|
|
|
|
}
|
|
|
|
|
const renderReply = (evt: EventWithNip19AndReplyTo, relay: string) => {
|
|
|
|
|
const parent = getViewElem(evt.replyTo);
|
|
|
|
|
if (!parent) { // root article has not been rendered
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!getViewElem(evt.id)) {
|
|
|
|
|
renderFeed();
|
|
|
|
|
let replyContainer = parent.querySelector('.mobx-replies');
|
|
|
|
|
if (!replyContainer) {
|
|
|
|
|
replyContainer = elem('div', {className: 'mobx-replies'});
|
|
|
|
|
parent.append(replyContainer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EventWithNip19AndReplyTo = EventWithNip19 & {
|
|
|
|
|
replyTo: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const replyList: Array<EventWithNip19AndReplyTo> = [];
|
|
|
|
|
const reply = createTextNote(evt, relay);
|
|
|
|
|
replyContainer.append(reply);
|
|
|
|
|
setViewElem(evt.id, reply);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function handleReply(evt: EventWithNip19, relay: string) {
|
|
|
|
|
const handleReply = (evt: EventWithNip19, relay: string) => {
|
|
|
|
|
if (
|
|
|
|
|
getViewElem(evt.id) // already rendered probably received from another relay
|
|
|
|
|
|| evt.tags.some(isMention) // ignore mentions for now
|
|
|
|
@ -128,22 +149,30 @@ function handleReply(evt: EventWithNip19, relay: string) {
|
|
|
|
|
const evtWithReplyTo = {replyTo, ...evt};
|
|
|
|
|
replyList.push(evtWithReplyTo);
|
|
|
|
|
renderReply(evtWithReplyTo, relay);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function renderReply(evt: EventWithNip19AndReplyTo, relay: string) {
|
|
|
|
|
const parent = getViewElem(evt.replyTo);
|
|
|
|
|
if (!parent) { // root article has not been rendered
|
|
|
|
|
return;
|
|
|
|
|
const handleTextNote = (evt: Event, relay: string) => {
|
|
|
|
|
if (eventRelayMap[evt.id]) {
|
|
|
|
|
eventRelayMap[evt.id] = [...(eventRelayMap[evt.id]), relay]; // TODO: just push?
|
|
|
|
|
} else {
|
|
|
|
|
eventRelayMap[evt.id] = [relay];
|
|
|
|
|
const evtWithNip19 = {
|
|
|
|
|
nip19: {
|
|
|
|
|
note: nip19.noteEncode(evt.id),
|
|
|
|
|
npub: nip19.npubEncode(evt.pubkey),
|
|
|
|
|
},
|
|
|
|
|
...evt,
|
|
|
|
|
};
|
|
|
|
|
if (evt.tags.some(hasEventTag)) {
|
|
|
|
|
handleReply(evtWithNip19, relay);
|
|
|
|
|
} else {
|
|
|
|
|
textNoteList.push(evtWithNip19);
|
|
|
|
|
}
|
|
|
|
|
let replyContainer = parent.querySelector('.mobx-replies');
|
|
|
|
|
if (!replyContainer) {
|
|
|
|
|
replyContainer = elem('div', {className: 'mobx-replies'});
|
|
|
|
|
parent.append(replyContainer);
|
|
|
|
|
}
|
|
|
|
|
const reply = createTextNote(evt, relay);
|
|
|
|
|
replyContainer.append(reply);
|
|
|
|
|
setViewElem(evt.id, reply);
|
|
|
|
|
}
|
|
|
|
|
if (!getViewElem(evt.id)) {
|
|
|
|
|
renderFeed();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
config.rerenderFeed = () => {
|
|
|
|
|
clearView();
|
|
|
|
@ -156,58 +185,22 @@ setInterval(() => {
|
|
|
|
|
});
|
|
|
|
|
}, 10000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createTextNote(evt: EventWithNip19, relay: string) {
|
|
|
|
|
const {host, img, name, time, userName} = getMetadata(evt, relay);
|
|
|
|
|
const replies = replyList.filter(({replyTo}) => replyTo === evt.id);
|
|
|
|
|
// const isLongContent = evt.content.trimRight().length > 280;
|
|
|
|
|
// const content = isLongContent ? evt.content.slice(0, 280) : evt.content;
|
|
|
|
|
const reactions = getReactions(evt.id);
|
|
|
|
|
const didReact = reactions.length && !!reactions.find(reaction => reaction.pubkey === config.pubkey);
|
|
|
|
|
const replyFeed: Array<HTMLElement> = replies[0] ? replies.sort(sortByCreatedAt).map(e => setViewElem(e.id, createTextNote(e, relay))) : [];
|
|
|
|
|
const [content, {firstLink}] = parseTextContent(evt.content);
|
|
|
|
|
const buttons = elem('div', {className: 'buttons'}, [
|
|
|
|
|
elem('button', {name: 'reply', type: 'button'}, [
|
|
|
|
|
elem('img', {height: 24, width: 24, src: '/assets/comment.svg'})
|
|
|
|
|
]),
|
|
|
|
|
elem('button', {name: 'star', type: 'button'}, [
|
|
|
|
|
elem('img', {
|
|
|
|
|
alt: didReact ? '✭' : '✩', // ♥
|
|
|
|
|
height: 24, width: 24,
|
|
|
|
|
src: `/assets/${didReact ? 'star-fill' : 'star'}.svg`,
|
|
|
|
|
title: getReactionContents(evt.id).join(' '),
|
|
|
|
|
}),
|
|
|
|
|
elem('small', {data: {reactions: ''}}, reactions.length || ''),
|
|
|
|
|
]),
|
|
|
|
|
]);
|
|
|
|
|
const body = elem('div', {className: 'mbox-body'}, [
|
|
|
|
|
elem('header', {
|
|
|
|
|
className: 'mbox-header',
|
|
|
|
|
title: `User: ${userName}\n${time}\n\nUser pubkey: ${evt.pubkey}\n\nRelay: ${host}\n\nEvent-id: ${evt.id}
|
|
|
|
|
${evt.tags.length ? `\nTags ${JSON.stringify(evt.tags)}\n` : ''}
|
|
|
|
|
${evt.content}`
|
|
|
|
|
}, [
|
|
|
|
|
elem('a', {className: `mbox-username${name ? ' mbox-kind0-name' : ''}`, href: `/${evt.nip19.npub}`}, name || userName),
|
|
|
|
|
' ',
|
|
|
|
|
elem('a', {href: `/${evt.nip19.note}`}, elem('time', {dateTime: time.toISOString()}, formatTime(time))),
|
|
|
|
|
const renderRecommendServer = (evt: Event, relay: string) => {
|
|
|
|
|
const {img, name, time, userName} = getMetadata(evt, relay);
|
|
|
|
|
const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [
|
|
|
|
|
elem('header', {className: 'mbox-header'}, [
|
|
|
|
|
elem('small', {}, [
|
|
|
|
|
elem('strong', {}, userName)
|
|
|
|
|
]),
|
|
|
|
|
elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, [
|
|
|
|
|
...content,
|
|
|
|
|
(firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : null,
|
|
|
|
|
]),
|
|
|
|
|
buttons,
|
|
|
|
|
` recommends server: ${evt.content}`,
|
|
|
|
|
]);
|
|
|
|
|
if (localStorage.getItem('reply_to') === evt.id) {
|
|
|
|
|
openWriteInput(buttons, evt.id);
|
|
|
|
|
}
|
|
|
|
|
return elemArticle([
|
|
|
|
|
elem('div', {className: 'mbox-img'}, img),
|
|
|
|
|
body,
|
|
|
|
|
...(replies[0] ? [elem('div', {className: 'mobx-replies'}, replyFeed.reverse())] : []),
|
|
|
|
|
], {data: {id: evt.id, pubkey: evt.pubkey, relay}});
|
|
|
|
|
}
|
|
|
|
|
elem('div', {className: 'mbox-img'}, [img]), body
|
|
|
|
|
], {className: 'mbox-recommend-server', data: {id: evt.id, pubkey: evt.pubkey}});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function handleRecommendServer(evt: Event, relay: string) {
|
|
|
|
|
const handleRecommendServer = (evt: Event, relay: string) => {
|
|
|
|
|
if (getViewElem(evt.id) || !isWssUrl(evt.content)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@ -222,25 +215,31 @@ function handleRecommendServer(evt: Event, relay: string) {
|
|
|
|
|
getViewElem(closestTextNotes[0].id)?.after(art); // TODO: note might not be in the dom yet, recommendedServers could be controlled by renderFeed
|
|
|
|
|
}
|
|
|
|
|
setViewElem(evt.id, art);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function renderRecommendServer(evt: Event, relay: string) {
|
|
|
|
|
const {img, name, time, userName} = getMetadata(evt, relay);
|
|
|
|
|
const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [
|
|
|
|
|
elem('header', {className: 'mbox-header'}, [
|
|
|
|
|
elem('small', {}, [
|
|
|
|
|
elem('strong', {}, userName)
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
` recommends server: ${evt.content}`,
|
|
|
|
|
]);
|
|
|
|
|
return elemArticle([
|
|
|
|
|
elem('div', {className: 'mbox-img'}, [img]), body
|
|
|
|
|
], {className: 'mbox-recommend-server', data: {id: evt.id, pubkey: evt.pubkey}});
|
|
|
|
|
}
|
|
|
|
|
const onEvent = (evt: Event, relay: string) => {
|
|
|
|
|
switch (evt.kind) {
|
|
|
|
|
case 0:
|
|
|
|
|
handleMetadata(evt, relay);
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
handleTextNote(evt, relay);
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
handleRecommendServer(evt, relay);
|
|
|
|
|
break;
|
|
|
|
|
case 3:
|
|
|
|
|
// handleContactList(evt, relay);
|
|
|
|
|
break;
|
|
|
|
|
case 7:
|
|
|
|
|
handleReaction(evt, relay);
|
|
|
|
|
default:
|
|
|
|
|
// console.log(`TODO: add support for event kind ${evt.kind}`/*, evt*/)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// subscribe and change view
|
|
|
|
|
function route(path: string) {
|
|
|
|
|
const route = (path: string) => {
|
|
|
|
|
if (path === '/') {
|
|
|
|
|
sub24hFeed(onEvent);
|
|
|
|
|
view('/');
|
|
|
|
@ -263,7 +262,7 @@ function route(path: string) {
|
|
|
|
|
console.warn(`type ${type} not yet supported`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// onload
|
|
|
|
|
route(location.pathname);
|
|
|
|
|