|
|
|
@ -4,15 +4,15 @@ import {elem} from './utils/dom';
|
|
|
|
|
import {bounce} from './utils/time';
|
|
|
|
|
import {isWssUrl} from './utils/url';
|
|
|
|
|
import {closeSettingsView, config, toggleSettingsView} from './settings';
|
|
|
|
|
import {subGlobalFeed, subEventID, subNote, subProfile, subPubkeys, subOwnContacts, subContactList} from './subscriptions'
|
|
|
|
|
import {getReplyTo, hasEventTag, isEvent, isMention, sortByCreatedAt, sortEventCreatedAt} from './events';
|
|
|
|
|
import {sub24hFeed, subEventID, subNote, subProfile} from './subscriptions'
|
|
|
|
|
import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt} from './events';
|
|
|
|
|
import {clearView, getViewContent, getViewElem, getViewOptions, setViewElem, view} from './view';
|
|
|
|
|
import {handleReaction, handleUpvote} from './reactions';
|
|
|
|
|
import {closePublishView, openWriteInput, togglePublishView} from './write';
|
|
|
|
|
import {handleMetadata, renderProfile} from './profiles';
|
|
|
|
|
import {followContact, getContactUpdateMessage, getContacts, getOwnContacts, refreshFollowing, resetContactList, setContactList, updateContactList, updateFollowBtn} from './contacts';
|
|
|
|
|
import {getContactUpdateMessage, setContactList, updateContactList} from './contacts';
|
|
|
|
|
import {EventWithNip19, EventWithNip19AndReplyTo, textNoteList, replyList} from './notes';
|
|
|
|
|
import {createContact, createTextNote, renderEventDetails, renderRecommendServer, renderUpdateContact} from './ui';
|
|
|
|
|
import {createTextNote, renderEventDetails, renderRecommendServer, renderUpdateContact} from './ui';
|
|
|
|
|
|
|
|
|
|
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
|
|
|
|
|
|
|
|
|
@ -38,18 +38,6 @@ const renderNote = (
|
|
|
|
|
setViewElem(evt.id, article);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderContact = (pubkey: string) => {
|
|
|
|
|
if (getViewElem(`contact-${pubkey}`)) { // contact already in view
|
|
|
|
|
updateFollowBtn(pubkey);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const contact = createContact(pubkey);
|
|
|
|
|
if (contact) {
|
|
|
|
|
getViewContent().append(contact);
|
|
|
|
|
setViewElem(`contact-${pubkey}`, contact);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hasEnoughPOW = (
|
|
|
|
|
[tag, , commitment]: string[],
|
|
|
|
|
eventId: string
|
|
|
|
@ -67,6 +55,7 @@ const renderFeed = bounce(() => {
|
|
|
|
|
.forEach(renderNote);
|
|
|
|
|
break;
|
|
|
|
|
case 'profile':
|
|
|
|
|
const isEvent = <T>(evt?: T): evt is T => evt !== undefined;
|
|
|
|
|
[
|
|
|
|
|
...textNoteList // get notes
|
|
|
|
|
.filter(note => note.pubkey === view.id),
|
|
|
|
@ -76,24 +65,9 @@ const renderFeed = bounce(() => {
|
|
|
|
|
]
|
|
|
|
|
.sort(sortByCreatedAt)
|
|
|
|
|
.reverse()
|
|
|
|
|
.forEach(renderNote);
|
|
|
|
|
.forEach(renderNote); // render in-reply-to
|
|
|
|
|
|
|
|
|
|
renderProfile(view.id);
|
|
|
|
|
refreshFollowing(view.id);
|
|
|
|
|
break;
|
|
|
|
|
case 'home':
|
|
|
|
|
const ids = getOwnContacts();
|
|
|
|
|
[
|
|
|
|
|
...textNoteList
|
|
|
|
|
.filter(note => ids.includes(note.pubkey)),
|
|
|
|
|
...replyList // search id in notes and replies
|
|
|
|
|
.filter(reply => ids.includes(reply.pubkey))
|
|
|
|
|
.map(reply => textNoteList.find(note => note.id === reply.replyTo))
|
|
|
|
|
.filter(isEvent),
|
|
|
|
|
]
|
|
|
|
|
.sort(sortByCreatedAt)
|
|
|
|
|
.reverse()
|
|
|
|
|
.forEach(renderNote);
|
|
|
|
|
break;
|
|
|
|
|
case 'feed':
|
|
|
|
|
const now = Math.floor(Date.now() * 0.001);
|
|
|
|
@ -108,16 +82,12 @@ const renderFeed = bounce(() => {
|
|
|
|
|
.reverse()
|
|
|
|
|
.forEach(renderNote);
|
|
|
|
|
break;
|
|
|
|
|
case 'contacts':
|
|
|
|
|
getContacts(view.id)
|
|
|
|
|
.forEach(renderContact);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}, 17); // (16.666 rounded, an arbitrary value to limit updates to max 60x per s)
|
|
|
|
|
|
|
|
|
|
const renderReply = (evt: EventWithNip19AndReplyTo) => {
|
|
|
|
|
const parent = getViewElem(evt.replyTo);
|
|
|
|
|
if (!parent || getViewElem(evt.id)) {
|
|
|
|
|
if (!parent) { // root article has not been rendered
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let replyContainer = parent.querySelector('.mbox-replies');
|
|
|
|
@ -140,6 +110,7 @@ const handleReply = (evt: EventWithNip19, relay: string) => {
|
|
|
|
|
}
|
|
|
|
|
const replyTo = getReplyTo(evt);
|
|
|
|
|
if (!replyTo) {
|
|
|
|
|
console.warn('expected to find reply-to-event-id', evt);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const evtWithReplyTo = {replyTo, ...evt};
|
|
|
|
@ -153,7 +124,7 @@ const handleTextNote = (evt: Event, relay: string) => {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (eventRelayMap[evt.id]) {
|
|
|
|
|
eventRelayMap[evt.id] = [...(eventRelayMap[evt.id]), relay]; // TODO: remove eventRelayMap and just check for getViewElem?
|
|
|
|
|
eventRelayMap[evt.id] = [...(eventRelayMap[evt.id]), relay]; // TODO: just push?
|
|
|
|
|
} else {
|
|
|
|
|
eventRelayMap[evt.id] = [relay];
|
|
|
|
|
const evtWithNip19 = {
|
|
|
|
@ -174,45 +145,38 @@ const handleTextNote = (evt: Event, relay: string) => {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const rerenderFeed = () => {
|
|
|
|
|
config.rerenderFeed = () => {
|
|
|
|
|
clearView();
|
|
|
|
|
renderFeed();
|
|
|
|
|
};
|
|
|
|
|
config.rerenderFeed = rerenderFeed;
|
|
|
|
|
|
|
|
|
|
const handleContactList = (evt: Event, relay: string) => {
|
|
|
|
|
// TODO: if newer and view.type === 'home' rerenderFeed()
|
|
|
|
|
setContactList(evt);
|
|
|
|
|
const view = getViewOptions();
|
|
|
|
|
if (getViewElem(evt.id)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
view.type === 'contacts'
|
|
|
|
|
&& [view.id, config.pubkey].includes(evt.pubkey) // render if contact-list is from current users or current view
|
|
|
|
|
getViewElem(evt.id)
|
|
|
|
|
|| view.type !== 'profile'
|
|
|
|
|
|| view.id !== evt.pubkey
|
|
|
|
|
) {
|
|
|
|
|
renderFeed();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (view.type === 'profile' && view.id === evt.pubkey) {
|
|
|
|
|
// use find instead of sort?
|
|
|
|
|
const closestTextNotes = textNoteList.sort(sortEventCreatedAt(evt.created_at));
|
|
|
|
|
const closestNote = getViewElem(closestTextNotes[0].id);
|
|
|
|
|
if (!closestNote) {
|
|
|
|
|
// no close note, try later
|
|
|
|
|
setTimeout(() => handleContactList(evt, relay), 1500);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
const [addedContacts, removedContacts] = updateContactList(evt);
|
|
|
|
|
const content = getContactUpdateMessage(addedContacts, removedContacts);
|
|
|
|
|
if (!content.length) {
|
|
|
|
|
// P same as before, maybe only evt.content or 'a' tags changed?
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const art = renderUpdateContact({...evt, content}, relay);
|
|
|
|
|
closestNote.after(art);
|
|
|
|
|
setViewElem(evt.id, art);
|
|
|
|
|
// use find instead of sort?
|
|
|
|
|
const closestTextNotes = textNoteList.sort(sortEventCreatedAt(evt.created_at));
|
|
|
|
|
const closestNote = getViewElem(closestTextNotes[0].id);
|
|
|
|
|
if (!closestNote) {
|
|
|
|
|
// no close note, try later
|
|
|
|
|
setTimeout(() => handleContactList(evt, relay), 1500);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
const [addedContacts, removedContacts] = updateContactList(evt);
|
|
|
|
|
const content = getContactUpdateMessage(addedContacts, removedContacts);
|
|
|
|
|
if (!content.length) {
|
|
|
|
|
// P same as before, maybe only evt.content or 'a' tags changed?
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const art = renderUpdateContact({...evt, content}, relay);
|
|
|
|
|
closestNote.after(art);
|
|
|
|
|
setViewElem(evt.id, art);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRecommendServer = (evt: Event, relay: string) => {
|
|
|
|
@ -234,12 +198,12 @@ const handleRecommendServer = (evt: Event, relay: string) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onEventDetails = (evt: Event, relay: string) => {
|
|
|
|
|
if (getViewElem(evt.id)) {
|
|
|
|
|
if (getViewElem(`detail-${evt.id}`)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const article = renderEventDetails(evt, relay);
|
|
|
|
|
getViewContent().append(article);
|
|
|
|
|
setViewElem(evt.id, article);
|
|
|
|
|
const art = renderEventDetails(evt, relay);
|
|
|
|
|
getViewContent().append(art);
|
|
|
|
|
setViewElem(`detail-${evt.id}`, art);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onEvent = (evt: Event, relay: string) => {
|
|
|
|
@ -266,19 +230,8 @@ const onEvent = (evt: Event, relay: string) => {
|
|
|
|
|
// subscribe and change view
|
|
|
|
|
const route = (path: string) => {
|
|
|
|
|
if (path === '/') {
|
|
|
|
|
const contactList = getOwnContacts();
|
|
|
|
|
if (contactList.length) {
|
|
|
|
|
subPubkeys(contactList, onEvent);
|
|
|
|
|
view(`/`, {type: 'home'});
|
|
|
|
|
} else {
|
|
|
|
|
subGlobalFeed(onEvent);
|
|
|
|
|
view('/feed', {type: 'feed'});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (path === '/feed') {
|
|
|
|
|
subGlobalFeed(onEvent);
|
|
|
|
|
view('/feed', {type: 'feed'});
|
|
|
|
|
sub24hFeed(onEvent);
|
|
|
|
|
view('/', {type: 'feed'});
|
|
|
|
|
} else if (path.length === 64 && path.match(/^\/[0-9a-z]+$/)) {
|
|
|
|
|
const {type, data} = nip19.decode(path.slice(1));
|
|
|
|
|
if (typeof data !== 'string') {
|
|
|
|
@ -293,31 +246,22 @@ const route = (path: string) => {
|
|
|
|
|
case 'npub':
|
|
|
|
|
subProfile(data, onEvent);
|
|
|
|
|
view(path, {type: 'profile', id: data});
|
|
|
|
|
updateFollowBtn(data);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.warn(`type ${type} not yet supported`);
|
|
|
|
|
}
|
|
|
|
|
renderFeed();
|
|
|
|
|
} else if (path.length === 73 && path.match(/^\/contacts\/npub[0-9a-z]+$/)) {
|
|
|
|
|
const contactNpub = path.slice(10);
|
|
|
|
|
const {type: contactType, data: contactPubkey} = nip19.decode(contactNpub);
|
|
|
|
|
if (contactType === 'npub') {
|
|
|
|
|
subContactList(contactPubkey, onEvent);
|
|
|
|
|
view(path, {type: 'contacts', id: contactPubkey});
|
|
|
|
|
}
|
|
|
|
|
} else if (path.length === 65) {
|
|
|
|
|
const eventID = path.slice(1);
|
|
|
|
|
subEventID(eventID, onEventDetails);
|
|
|
|
|
view(path, {type: 'event', id: eventID});
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('no support for ', path);
|
|
|
|
|
console.warn('no support for ', path)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// onload
|
|
|
|
|
route(location.pathname);
|
|
|
|
|
subOwnContacts(onEvent); // subscribe after route as routing unsubscribes current subs
|
|
|
|
|
|
|
|
|
|
// only push a new entry if there is no history onload
|
|
|
|
|
if (!history.length) {
|
|
|
|
@ -342,11 +286,8 @@ const handleLink = (a: HTMLAnchorElement, e: MouseEvent) => {
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
href === '/'
|
|
|
|
|
|| href.startsWith('/feed')
|
|
|
|
|
|| href.startsWith('/note')
|
|
|
|
|
|| href.startsWith('/npub')
|
|
|
|
|
|| href.startsWith('/contacts/npub')
|
|
|
|
|
|| (href.startsWith('/') && href.length === 65)
|
|
|
|
|
) {
|
|
|
|
|
route(href);
|
|
|
|
|
history.pushState({}, '', href);
|
|
|
|
@ -365,26 +306,17 @@ const handleButton = (button: HTMLButtonElement) => {
|
|
|
|
|
case 'back':
|
|
|
|
|
closePublishView();
|
|
|
|
|
return;
|
|
|
|
|
case 'import':
|
|
|
|
|
resetContactList(config.pubkey);
|
|
|
|
|
rerenderFeed();
|
|
|
|
|
subOwnContacts(onEvent);
|
|
|
|
|
subGlobalFeed(onEvent);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const id = button.dataset.id || (button.closest('[data-id]') as HTMLElement)?.dataset.id;
|
|
|
|
|
const id = (button.closest('[data-id]') as HTMLElement)?.dataset.id;
|
|
|
|
|
if (id) {
|
|
|
|
|
switch(button.name) {
|
|
|
|
|
case 'reply':
|
|
|
|
|
openWriteInput(button, id);
|
|
|
|
|
return;
|
|
|
|
|
break;
|
|
|
|
|
case 'star':
|
|
|
|
|
const note = replyList.find(r => r.id === id) || textNoteList.find(n => n.id === (id));
|
|
|
|
|
note && handleUpvote(note);
|
|
|
|
|
return;
|
|
|
|
|
case 'follow':
|
|
|
|
|
followContact(id);
|
|
|
|
|
return;
|
|
|
|
|
const note = replyList.find(r => r.id === id) || textNoteList.find(n => n.id === (id));
|
|
|
|
|
note && handleUpvote(note);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// const container = e.target.closest('[data-append]');
|
|
|
|
|