route: add contact list view
added new route /contacts/npub... to show contact lists of users. each user has about text, follow/unfollow buttons. fixed CSS and JavaScript links in index.html to support deeper path i.e. /contacts/npub... uri's.pull/84/head
parent
25d3283a80
commit
fea8c0bd21
114
src/contacts.ts
114
src/contacts.ts
|
@ -13,22 +13,28 @@ const contactHistoryMap: {
|
|||
[pubkey: string]: Event[];
|
||||
} = {};
|
||||
|
||||
const hasOwnContactList = () => {
|
||||
return !!contactHistoryMap[config.pubkey];
|
||||
};
|
||||
|
||||
/**
|
||||
* returns true if user is following pubkey
|
||||
*/
|
||||
export const isFollowing = (id: string) => {
|
||||
export const isFollowing = (pubkey: string) => {
|
||||
const following = contactHistoryMap[config.pubkey]?.at(0);
|
||||
if (!following) {
|
||||
return false;
|
||||
}
|
||||
return following.tags.some(([tag, value]) => tag === 'p' && value === id);
|
||||
return following.tags.some(([tag, value]) => tag === 'p' && value === pubkey);
|
||||
};
|
||||
|
||||
export const updateFollowBtn = (pubkey: string) => {
|
||||
const followBtn = getViewElem('followBtn');
|
||||
if (followBtn) {
|
||||
const followBtn = getViewElem(`followBtn-${pubkey}`);
|
||||
const view = getViewOptions();
|
||||
if (followBtn && (view.type === 'contacts' || view.type === 'profile')) {
|
||||
const hasContact = isFollowing(pubkey);
|
||||
followBtn.textContent = hasContact ? 'unfollow' : 'follow';
|
||||
const isMe = config.pubkey === pubkey;
|
||||
followBtn.textContent = isMe ? 'following' : hasContact ? 'unfollow' : 'follow';
|
||||
followBtn.classList.remove('primary', 'secondary');
|
||||
followBtn.classList.add(hasContact ? 'secondary' : 'primary');
|
||||
followBtn.hidden = false;
|
||||
|
@ -40,25 +46,49 @@ const updateFollowing = (evt: Event) => {
|
|||
if (evt.pubkey === config.pubkey) {
|
||||
localStorage.setItem('follwing', JSON.stringify(evt));
|
||||
}
|
||||
if (view.type === 'profile') {
|
||||
updateFollowBtn(view.id);
|
||||
if (view.id === evt.pubkey) {
|
||||
// update following link
|
||||
const following = getViewElem('following') as HTMLElement;
|
||||
if (following) {
|
||||
const count = evt.tags.filter(isPTag).length;
|
||||
const anchor = elem('a', {
|
||||
data: {following: evt.pubkey},
|
||||
href: `/${evt.id}`,
|
||||
title: dateTime.format(evt.created_at * 1000),
|
||||
}, [
|
||||
'following ',
|
||||
elem('span', {className: 'highlight'}, count),
|
||||
]);
|
||||
following.replaceWith(anchor);
|
||||
setViewElem('following', anchor);
|
||||
switch(view.type) {
|
||||
case 'contacts':
|
||||
if (hasOwnContactList()) {
|
||||
const lastContactList = contactHistoryMap[config.pubkey]?.at(1);
|
||||
if (lastContactList) {
|
||||
const [added, removed] = findChanges(evt, lastContactList);
|
||||
[
|
||||
...added.map(([, pubkey]) => pubkey),
|
||||
...removed.map(([, pubkey]) => pubkey),
|
||||
].forEach(updateFollowBtn);
|
||||
} else {
|
||||
evt.tags
|
||||
.filter(isPTag)
|
||||
.forEach(([, pubkey]) => updateFollowBtn(pubkey));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'profile':
|
||||
updateFollowBtn(view.id);
|
||||
if (view.id === evt.pubkey) {
|
||||
// update following link
|
||||
const following = getViewElem('following') as HTMLElement;
|
||||
if (following) {
|
||||
const count = evt.tags.filter(isPTag).length;
|
||||
const anchor = elem('a', {
|
||||
data: {following: evt.pubkey},
|
||||
href: `/contacts/${nip19.npubEncode(evt.pubkey)}`,
|
||||
title: dateTime.format(evt.created_at * 1000),
|
||||
}, [
|
||||
'following ',
|
||||
elem('span', {className: 'highlight'}, count),
|
||||
]);
|
||||
following.replaceWith(anchor);
|
||||
setViewElem('following', anchor);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export const refreshFollowing = (id: string) => {
|
||||
if (contactHistoryMap[id]?.at(0)) {
|
||||
updateFollowing(contactHistoryMap[id][0]);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -148,12 +178,29 @@ export const updateContactList = (evt: Event) => {
|
|||
return [evt.tags.filter(isPTag)];
|
||||
};
|
||||
|
||||
export const getContacts = () => {
|
||||
const following = contactHistoryMap[config.pubkey]?.at(0); // TODO: ensure newest contactlist
|
||||
if (following) {
|
||||
return following.tags
|
||||
.filter(isPTag)
|
||||
.map(([, pubkey]) => pubkey);
|
||||
/**
|
||||
* returns list of pubkeys the given pubkey is following
|
||||
* @param pubkey
|
||||
* @returns {String[]} pubkeys
|
||||
*/
|
||||
export const getContacts = (pubkey: string) => {
|
||||
const following = contactHistoryMap[pubkey]?.at(0);
|
||||
if (!following) {
|
||||
return [];
|
||||
}
|
||||
return following.tags
|
||||
.filter(isPTag)
|
||||
.map(([, pubkey]) => pubkey);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns list of pubkeys the user is following, if none found it will try from localstorage
|
||||
* @returns {String[]} pubkeys
|
||||
*/
|
||||
export const getOwnContacts = () => {
|
||||
const following = getContacts(config.pubkey);
|
||||
if (following.length) {
|
||||
return following;
|
||||
}
|
||||
const followingFromStorage = localStorage.getItem('follwing');
|
||||
if (followingFromStorage) {
|
||||
|
@ -186,9 +233,9 @@ const updateContactTags = (
|
|||
];
|
||||
};
|
||||
|
||||
export const followContact = async (id: string) => {
|
||||
const followBtn = getViewElem('followBtn') as HTMLButtonElement;
|
||||
const statusElem = getViewElem('followStatus') as HTMLElement;
|
||||
export const followContact = async (pubkey: string) => {
|
||||
const followBtn = getViewElem(`followBtn-${pubkey}`) as HTMLButtonElement;
|
||||
const statusElem = getViewElem(`followStatus-${pubkey}`) as HTMLElement;
|
||||
if (!followBtn || !statusElem) {
|
||||
return;
|
||||
}
|
||||
|
@ -197,7 +244,7 @@ export const followContact = async (id: string) => {
|
|||
kind: 3,
|
||||
pubkey: config.pubkey,
|
||||
content: '',
|
||||
tags: updateContactTags(id, following),
|
||||
tags: updateContactTags(pubkey, following),
|
||||
created_at: Math.floor(Date.now() * 0.001),
|
||||
};
|
||||
|
||||
|
@ -234,5 +281,4 @@ export const followContact = async (id: string) => {
|
|||
console.info(`event published by ${relay}`);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>nostr</title>
|
||||
<link rel="stylesheet" href="styles/main.css" type="text/css">
|
||||
<link rel="stylesheet" href="/styles/main.css" type="text/css">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -110,5 +110,5 @@
|
|||
</div>
|
||||
|
||||
</body>
|
||||
<script src="main.js"></script>
|
||||
<script src="/main.js"></script>
|
||||
</html>
|
||||
|
|
85
src/main.ts
85
src/main.ts
|
@ -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} from './subscriptions'
|
||||
import {subGlobalFeed, subEventID, subNote, subProfile, subPubkeys, subOwnContacts, subContactList} from './subscriptions'
|
||||
import {getReplyTo, hasEventTag, isEvent, 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, resetContactList, setContactList, updateContactList, updateFollowBtn} from './contacts';
|
||||
import {followContact, getContactUpdateMessage, getContacts, getOwnContacts, refreshFollowing, resetContactList, setContactList, updateContactList, updateFollowBtn} from './contacts';
|
||||
import {EventWithNip19, EventWithNip19AndReplyTo, textNoteList, replyList} from './notes';
|
||||
import {createTextNote, renderEventDetails, renderRecommendServer, renderUpdateContact} from './ui';
|
||||
import {createContact, createTextNote, renderEventDetails, renderRecommendServer, renderUpdateContact} from './ui';
|
||||
|
||||
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
|
||||
|
||||
|
@ -38,6 +38,18 @@ 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
|
||||
|
@ -64,12 +76,13 @@ const renderFeed = bounce(() => {
|
|||
]
|
||||
.sort(sortByCreatedAt)
|
||||
.reverse()
|
||||
.forEach(renderNote); // render in-reply-to
|
||||
.forEach(renderNote);
|
||||
|
||||
renderProfile(view.id);
|
||||
refreshFollowing(view.id);
|
||||
break;
|
||||
case 'home':
|
||||
const ids = getContacts();
|
||||
const ids = getOwnContacts();
|
||||
[
|
||||
...textNoteList
|
||||
.filter(note => ids.includes(note.pubkey)),
|
||||
|
@ -95,6 +108,10 @@ 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)
|
||||
|
||||
|
@ -167,30 +184,35 @@ 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 (
|
||||
getViewElem(evt.id)
|
||||
|| view.type !== 'profile'
|
||||
|| view.id !== evt.pubkey
|
||||
view.type === 'contacts'
|
||||
&& [view.id, config.pubkey].includes(evt.pubkey) // render if contact-list is from current users or current view
|
||||
) {
|
||||
renderFeed();
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
const art = renderUpdateContact({...evt, content}, relay);
|
||||
closestNote.after(art);
|
||||
setViewElem(evt.id, art);
|
||||
};
|
||||
|
||||
const handleRecommendServer = (evt: Event, relay: string) => {
|
||||
|
@ -243,10 +265,9 @@ const onEvent = (evt: Event, relay: string) => {
|
|||
|
||||
// subscribe and change view
|
||||
const route = (path: string) => {
|
||||
const contactList = getContacts();
|
||||
if (path === '/') {
|
||||
const contactList = getOwnContacts();
|
||||
if (contactList.length) {
|
||||
const {pubkey} = config;
|
||||
subPubkeys(contactList, onEvent);
|
||||
view(`/`, {type: 'home'});
|
||||
} else {
|
||||
|
@ -278,18 +299,25 @@ const route = (path: string) => {
|
|||
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
|
||||
subOwnContacts(onEvent);
|
||||
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) {
|
||||
|
@ -317,6 +345,7 @@ const handleLink = (a: HTMLAnchorElement, e: MouseEvent) => {
|
|||
|| href.startsWith('/feed')
|
||||
|| href.startsWith('/note')
|
||||
|| href.startsWith('/npub')
|
||||
|| href.startsWith('/contacts/npub')
|
||||
|| (href.startsWith('/') && href.length === 65)
|
||||
) {
|
||||
route(href);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Event} from 'nostr-tools';
|
||||
import {elem, elemCanvas, parseTextContent} from './utils/dom';
|
||||
import {getNoxyUrl} from './utils/url';
|
||||
import {getViewElem} from './view';
|
||||
import {getViewElem, getViewOptions} from './view';
|
||||
import {parseJSON} from './media';
|
||||
import {sortByCreatedAt} from './events';
|
||||
|
||||
|
@ -87,10 +87,22 @@ export const handleMetadata = (evt: Event, relay: string) => {
|
|||
username.classList.add('mbox-kind0-name');
|
||||
});
|
||||
}
|
||||
if (metadata.about) {
|
||||
const about = getViewElem(`about-${evt.pubkey}`);
|
||||
if (about) {
|
||||
const view = getViewOptions();
|
||||
about.replaceChildren(...parseTextContent(
|
||||
view.type === 'contacts'
|
||||
? metadata.about.split('\n')[0]
|
||||
: metadata.about
|
||||
)[0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getMetadata = (pubkey: string) => {
|
||||
const user = profileMap[pubkey];
|
||||
const about = user?.about;
|
||||
const name = user?.name;
|
||||
const userName = name || pubkey.slice(0, 8);
|
||||
// const userImg = user?.picture;
|
||||
|
@ -100,7 +112,7 @@ export const getMetadata = (pubkey: string) => {
|
|||
src: userImg,
|
||||
title: `${userName} on ${host} ${userAbout}`,
|
||||
}) : */ elemCanvas(pubkey);
|
||||
return {img, name, userName};
|
||||
return {about, img, name, userName};
|
||||
};
|
||||
|
||||
export const renderProfile = (pubkey: string) => {
|
||||
|
@ -118,12 +130,11 @@ export const renderProfile = (pubkey: string) => {
|
|||
header.append(elem('h1', {}, metadata.name));
|
||||
}
|
||||
}
|
||||
const detail = getViewElem('detail');
|
||||
if (metadata.about) {
|
||||
if (!detail.children.length) {
|
||||
const [content] = parseTextContent(metadata.about);
|
||||
detail?.append(...content);
|
||||
}
|
||||
console.log('render detail')
|
||||
const detail = getViewElem(`detail-${pubkey}`);
|
||||
if (metadata.about && !detail.children.length) {
|
||||
const [content] = parseTextContent(metadata.about);
|
||||
detail?.append(...content);
|
||||
}
|
||||
if (metadata.website) {
|
||||
const website = detail.querySelector('[data-website]');
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
background-color: var(--bgcolor-textinput);
|
||||
border-radius: var(--profileimg-size);
|
||||
flex-basis: var(--profileimg-size);
|
||||
flex-shrink: 0;
|
||||
height: var(--profileimg-size);
|
||||
margin-right: var(--gap-half);
|
||||
max-height: var(--profileimg-size);
|
||||
|
@ -57,9 +58,12 @@ a.mbox-img:focus {
|
|||
.mbox-replies .mbox-replies .mbox-img + .mbox-body {
|
||||
--max-width: calc(100% - var(--profileimg-size) + var(--gap-half));
|
||||
}
|
||||
.mbox-contact .mbox-img + .mbox-body {
|
||||
--max-width: calc(100% - var(--profileimg-size) - var(--gap-half) - 90px);
|
||||
}
|
||||
|
||||
.mbox-header {
|
||||
align-items: start;
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
gap: var(--gap-quarter);
|
||||
justify-content: space-between;
|
||||
|
@ -72,6 +76,7 @@ a.mbox-img:focus {
|
|||
text-decoration: none;
|
||||
}
|
||||
.mbox-header small {
|
||||
color: var(--color-accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -81,6 +86,35 @@ a.mbox-img:focus {
|
|||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.mbox-contact {
|
||||
align-items: start; /* TODO: maybe all .mbox element should have align-items start */
|
||||
flex-wrap: nowrap;
|
||||
padding: var(--gap-half);
|
||||
}
|
||||
.mbox-contact .mbox-header {
|
||||
justify-content: start;
|
||||
}
|
||||
.mbox-contact .mbox-body {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0; /* with this mbox-content displays text on one line cutting off text-overflo... */
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.mbox-contact .mbox-content {
|
||||
overflow: clip;
|
||||
padding-right: var(--gap-quarter);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mbox-cta {
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mbox-updated-contact,
|
||||
.mbox-recommend-server {
|
||||
padding-bottom: var(--gap-quarter);
|
||||
|
|
|
@ -146,6 +146,10 @@ button:active {
|
|||
.secondary {
|
||||
background-color: transparent;
|
||||
}
|
||||
.secondary:disabled {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
button:focus {
|
||||
}
|
||||
|
|
|
@ -159,7 +159,10 @@ nav a {
|
|||
}
|
||||
.hero-title h1 {
|
||||
flex-grow: 1;
|
||||
font-size: 2.1rem;
|
||||
line-height: 1.285714285714286;
|
||||
margin-bottom: 0;
|
||||
margin-top: 2rem;
|
||||
padding-left: var(--extra-space);
|
||||
}
|
||||
.hero-title button {
|
||||
|
@ -173,8 +176,9 @@ nav a {
|
|||
|
||||
.hero .hero-npub {
|
||||
color: var(--color-accent);
|
||||
font-size: 1.1rem;
|
||||
display: block;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.36363636;
|
||||
max-width: 100%;
|
||||
overflow: clip;
|
||||
text-align: center;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Event} from 'nostr-tools';
|
||||
import {getReplyTo, hasEventTag, isMention} from './events';
|
||||
import {getReplyTo, hasEventTag, isMention, isPTag} from './events';
|
||||
import {config} from './settings';
|
||||
import {sub, subOnce, unsubAll} from './relays';
|
||||
|
||||
|
@ -288,3 +288,41 @@ export const subOwnContacts = (onEvent: SubCallback) => {
|
|||
unsub: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const subContactList = (
|
||||
pubkey: string,
|
||||
onEvent: SubCallback,
|
||||
) => {
|
||||
unsubAll();
|
||||
const pubkeys = new Set<string>();
|
||||
let newestEvent = 0;
|
||||
sub({
|
||||
cb: (evt: Event, relay: string) => {
|
||||
if (evt.created_at <= newestEvent) {
|
||||
return;
|
||||
}
|
||||
newestEvent = evt.created_at;
|
||||
const newPubkeys = evt.tags
|
||||
.filter(isPTag)
|
||||
.filter(([, p]) => !pubkeys.has(p))
|
||||
.map(([, p]) => {
|
||||
pubkeys.add(p);
|
||||
return p
|
||||
});
|
||||
subOnce({
|
||||
cb: onEvent,
|
||||
filter: {
|
||||
authors: newPubkeys,
|
||||
kinds: [0],
|
||||
},
|
||||
relay,
|
||||
});
|
||||
onEvent(evt, relay);
|
||||
},
|
||||
filter: {
|
||||
authors: [pubkey],
|
||||
kinds: [3],
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -88,7 +88,7 @@ export const powEvent = (
|
|||
statusElem.replaceChildren('working…', cancelBtn);
|
||||
statusElem.hidden = false;
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('./worker.js');
|
||||
const worker = new Worker('/worker.js');
|
||||
|
||||
const onCancel = () => {
|
||||
worker.terminate();
|
||||
|
|
|
@ -16,6 +16,9 @@ export type ViewTemplateOptions = {
|
|||
} | {
|
||||
type: 'profile';
|
||||
id: string;
|
||||
} | {
|
||||
type: 'contacts';
|
||||
id: string;
|
||||
} | {
|
||||
type: 'event';
|
||||
id: string;
|
||||
|
@ -32,7 +35,8 @@ export const renderViewTemplate = (options: ViewTemplateOptions) => {
|
|||
case 'profile':
|
||||
const pubkey = options.id;
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const detail = elem('p');
|
||||
const about = elem('span');
|
||||
const detail = elem('p', {}, about);
|
||||
const followStatus = elem('small');
|
||||
const followBtn = elem('button', {
|
||||
className: 'primary',
|
||||
|
@ -51,15 +55,18 @@ export const renderViewTemplate = (options: ViewTemplateOptions) => {
|
|||
elem('footer', {}, following),
|
||||
]);
|
||||
dom.header = profileHeader;
|
||||
dom.detail = detail;
|
||||
dom[`about-${pubkey}`] = about;
|
||||
dom[`detail-${pubkey}`] = detail;
|
||||
dom.following = following;
|
||||
dom.followStatus = followStatus;
|
||||
dom.followBtn = followBtn;
|
||||
dom[`followStatus-${pubkey}`] = followStatus;
|
||||
dom[`followBtn-${pubkey}`] = followBtn;
|
||||
content.append(profileHeader);
|
||||
document.title = pubkey;
|
||||
break;
|
||||
case 'note':
|
||||
break;
|
||||
case 'contacts':
|
||||
break;
|
||||
case 'event':
|
||||
const id = options.id;
|
||||
content.append(
|
||||
|
|
48
src/ui.ts
48
src/ui.ts
|
@ -2,14 +2,15 @@ import {Event, nip19} from 'nostr-tools';
|
|||
import {Children, elem, elemArticle, parseTextContent} from './utils/dom';
|
||||
import {dateTime, formatTime} from './utils/time';
|
||||
import {/*validatePow,*/ sortByCreatedAt} from './events';
|
||||
import {setViewElem} from './view';
|
||||
import {getViewElem, getViewOptions, setViewElem} from './view';
|
||||
import {config} from './settings';
|
||||
import {getReactions, getReactionContents} from './reactions';
|
||||
import {openWriteInput} from './write';
|
||||
// import {linkPreview} from './media';
|
||||
import {parseJSON} from './media';
|
||||
import {getMetadata} from './profiles';
|
||||
import {EventWithNip19, replyList} from './notes';
|
||||
import {parseJSON} from './media';
|
||||
import {isFollowing} from './contacts';
|
||||
|
||||
setInterval(() => {
|
||||
document.querySelectorAll('time[datetime]').forEach((timeElem: HTMLTimeElement) => {
|
||||
|
@ -179,3 +180,46 @@ export const renderEventDetails = (evt: Event, relay: string) => {
|
|||
data: {kind: evt.kind, id: evt.id, pubkey: evt.pubkey}
|
||||
});
|
||||
};
|
||||
|
||||
export const createContact = (pubkey: string) => {
|
||||
const {about: aboutContent, img, name, userName} = getMetadata(pubkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const view = getViewOptions();
|
||||
if (view.type !== 'contacts') {
|
||||
return null;
|
||||
}
|
||||
const isMe = config.pubkey === pubkey;
|
||||
const isCurrentUser = view.id === pubkey;
|
||||
const hasContact = isFollowing(pubkey);
|
||||
const followStatus = elem('small');
|
||||
const followBtn = elem('button', {
|
||||
className: hasContact ? 'secondary' : 'primary',
|
||||
...(isMe && {disabled: true}),
|
||||
name: 'follow',
|
||||
data: {id: pubkey}
|
||||
}, hasContact ? (isMe ? 'following' : 'unfollow') : 'follow');
|
||||
const about = elem('div', {className: 'mbox-content'}, aboutContent);
|
||||
setViewElem(`about-${pubkey}`, about);
|
||||
setViewElem(`followStatus-${pubkey}`, followStatus);
|
||||
setViewElem(`followBtn-${pubkey}`, followBtn);
|
||||
return elemArticle([
|
||||
elem('a', {className: 'mbox-img', href: `/${npub}`, tabIndex: -1}, img),
|
||||
elem('div', {className: 'mbox-body'}, [
|
||||
elem('header', {className: 'mbox-header'}, [
|
||||
elem('a', {
|
||||
className: `mbox-username${name ? ' mbox-kind0-name' : ''}`,
|
||||
data: {profile: pubkey},
|
||||
href: `/${npub}`,
|
||||
}, name || userName),
|
||||
(isMe || isCurrentUser)
|
||||
? elem('small', {}, isMe ? '(your user)' : '(current user)')
|
||||
: null,
|
||||
]),
|
||||
about,
|
||||
]),
|
||||
elem('div', {className: 'mbox-cta'}, [followStatus, followBtn]),
|
||||
], {
|
||||
className: 'mbox-contact',
|
||||
data: {pubkey},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -60,7 +60,7 @@ type GetViewOptions = () => ViewTemplateOptions;
|
|||
|
||||
/**
|
||||
* get options for current view
|
||||
* @returns {id: 'home' | 'feed' | 'profile' | 'note' | 'event', id?: string}
|
||||
* @returns {id: 'home' | 'feed' | 'profile' | 'note' | 'contacts' | 'event', id?: string}
|
||||
*/
|
||||
export const getViewOptions: GetViewOptions = () => containers[activeContainerIndex]?.options || {type: 'feed'};
|
||||
|
||||
|
|
Loading…
Reference in New Issue