You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nostrweb/src/ui.ts

226 lines
8.0 KiB
TypeScript

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 {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 {isFollowing} from './contacts';
setInterval(() => {
document.querySelectorAll('time[datetime]').forEach((timeElem: HTMLTimeElement) => {
timeElem.textContent = formatTime(new Date(timeElem.dateTime));
});
}, 10000);
export const createTextNote = (
evt: EventWithNip19,
relay: string,
) => {
const {img, name, userName} = getMetadata(evt.pubkey);
const replies = replyList.filter(({replyTo}) => replyTo === evt.id)
.sort(sortByCreatedAt)
.reverse();
// 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 [content, {firstLink}] = parseTextContent(evt.content);
const time = new Date(evt.created_at * 1000);
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 || ''),
]),
]);
if (localStorage.getItem('reply_to') === evt.id) {
openWriteInput(buttons, evt.id);
}
const replyFeed: Array<HTMLElement> = replies[0] ? replies.map(e => setViewElem(e.id, createTextNote(e, relay))) : [];
return elemArticle([
elem('a', {className: 'mbox-img', href: `/${evt.nip19.npub}`, tabIndex: -1}, img),
elem('div', {className: 'mbox-body'}, [
elem('header', {
className: 'mbox-header',
title: `User: ${userName}\n${time}\n\nUser pubkey: ${evt.pubkey}\n\nRelay: ${relay}\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' : ''}`,
data: {profile: evt.pubkey},
href: `/${evt.nip19.npub}`,
}, name || userName),
' ',
elem('a', {href: `/${evt.nip19.note}`}, elem('time', {dateTime: time.toISOString()}, formatTime(time))),
]),
elem('div', {className: 'mbox-content'/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, content /*[
...content,
(firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : null,
]*/),
buttons,
]),
...(replies[0] ? [elem('div', {className: 'mbox-replies'}, replyFeed)] : []),
], {
className: replies.length ? 'mbox-has-replies' : '',
data: {kind: evt.kind, id: evt.id, pubkey: evt.pubkey, relay}
});
};
type EventWithContent = Omit<Event, 'content'> & {
content: Children
}
export const renderUpdateContact = (
evt: EventWithContent,
relay: string,
) => {
const {img, name, userName} = getMetadata(evt.pubkey);
const time = new Date(evt.created_at * 1000);
return elemArticle([
elem('div', {className: 'mbox-img'}, img),
elem('div', {className: 'mbox-body'}, [
elem('header', {className: 'mbox-header'}, [
elem('span', {
className: `mbox-username${name ? ' mbox-kind0-name' : ''}`,
data: {profile: evt.pubkey},
}, name || userName),
' ',
elem('span', {data: {contacts: evt.id}, title: time.toISOString()}, evt.content),
]),
]),
], {
className: 'mbox-updated-contact',
data: {kind: evt.kind, id: evt.id, pubkey: evt.pubkey, relay}
}
);
};
export const renderRecommendServer = (evt: Event, relay: string) => {
const {img, userName} = getMetadata(evt.pubkey);
const time = new Date(evt.created_at * 1000);
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: {kind: evt.kind, id: evt.id, pubkey: evt.pubkey}
});
};
export const renderEventDetails = (evt: Event, relay: string) => {
const {img, name, userName} = getMetadata(evt.pubkey);
const npub = nip19.npubEncode(evt.pubkey);
let content = (![1, 7].includes(evt.kind) && evt.content !== '') ? parseJSON(evt.content) : (evt.content || '<empty>');
switch (typeof content) {
case 'object':
content = JSON.stringify(content, null, 2);
break;
default:
content = `${content}`;
}
const body = elem('div', {className: 'mbox-body'}, [
elem('header', {className: 'mbox-header'}, [
elem('a', {
className: `mbox-username${name ? ' mbox-kind0-name' : ''}`,
data: {profile: evt.pubkey},
href: `/${npub}`,
}, name || userName),
]),
elem('dl', {}, [
elem('dt', {}, 'npub'),
elem('dd', {}, npub),
elem('dt', {}, 'created at'),
elem('dd', {}, dateTime.format(evt.created_at * 1000)),
elem('dt', {}, 'relay'),
elem('dd', {}, relay),
]),
elem('h2', {}, 'Event'),
elem('dl', {}, [
elem('dt', {}, 'id'),
elem('dd', {}, evt.id),
elem('dt', {}, 'kind'),
elem('dd', {}, evt.kind),
elem('dt', {}, 'pubkey'),
elem('dd', {}, evt.pubkey),
elem('dt', {}, 'tags count'),
elem('dd', {}, evt.tags.length),
elem('dt', {}, 'tags'),
elem('dd', {}, JSON.stringify(evt.tags)),
elem('dt', {}, 'content'),
elem('dd', {}, elem('pre', {}, content as string)),
]),
]);
return elemArticle([
elem('a', {className: 'mbox-img', href: `/${npub}`, tabIndex: -1}, img),
body,
], {
className: 'mbox-plain-event',
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},
});
};