diff --git a/src/main.js b/src/main.js index 9897fc3..d0a6fcb 100644 --- a/src/main.js +++ b/src/main.js @@ -1,15 +1,16 @@ import {nip19} from 'nostr-tools'; import {zeroLeadingBitsCount} from './utils/crypto'; -import {elem, elemCanvas, parseTextContent} from './utils/dom'; +import {elem, parseTextContent} from './utils/dom'; import {bounce, dateTime, formatTime} from './utils/time'; -import {getHost, getNoxyUrl, isWssUrl} from './utils/url'; +import {isWssUrl} from './utils/url'; import {sub24hFeed, subNote, subProfile} from './subscriptions' import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt, validatePow} from './events'; import {clearView, getViewContent, getViewElem, setViewElem, view} from './view'; import {closeSettingsView, config, toggleSettingsView} from './settings'; import {getReactions, getReactionContents, handleReaction, handleUpvote} from './reactions'; import {closePublishView, openWriteInput, togglePublishView} from './write'; -import {linkPreview, parseContent} from './media'; +import {linkPreview} from './media'; +import {getMetadata, handleMetadata} from './profiles'; // curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ @@ -196,43 +197,6 @@ function handleRecommendServer(evt, relay) { setViewElem(evt.id, art); } -function handleContactList(evt, relay) { - if (getViewElem(evt.id)) { - return; - } - const art = renderUpdateContact(evt, relay); - if (textNoteList.length < 2) { - getViewContent().append(art); - return; - } - const closestTextNotes = textNoteList.sort(sortEventCreatedAt(evt.created_at)); - getViewElem(closestTextNotes[0].id).after(art); - setViewElem(evt.id, art); - // const user = userList.find(u => u.pupkey === evt.pubkey); - // if (user) { - // console.log(`TODO: add contact list for ${evt.pubkey.slice(0, 8)} on ${relay}`, evt.tags); - // } else { - // tempContactList[relay] = tempContactList[relay] - // ? [...tempContactList[relay], evt] - // : [evt]; - // } -} - -function renderUpdateContact(evt, relay) { - const {img, time, userName} = getMetadata(evt, relay); - const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [ - elem('header', {className: 'mbox-header'}, [ - elem('small', {}, []), - ]), - elem('pre', {title: JSON.stringify(evt.content)}, [ - elem('strong', {}, userName), - ' updated contacts: ', - JSON.stringify(evt.tags), - ]), - ]); - return renderArticle([img, body], {className: 'mbox-updated-contact', data: {id: evt.id, pubkey: evt.pubkey, relay}}); -} - function renderRecommendServer(evt, relay) { const {img, name, time, userName} = getMetadata(evt, relay); const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [ @@ -253,76 +217,6 @@ function renderArticle(content, props = {}) { return elem('article', {...props, className}, content); } -const userList = []; -// const tempContactList = {}; - -function handleMetadata(evt, relay) { - const content = parseContent(evt.content); - if (content) { - setMetadata(evt, relay, content); - } -} - -function setMetadata(evt, relay, content) { - let user = userList.find(u => u.pubkey === evt.pubkey); - const picture = getNoxyUrl('data', content.picture, evt.id, relay).href; - if (!user) { - user = { - metadata: {[relay]: content}, - ...(content.picture && {picture}), - pubkey: evt.pubkey, - }; - userList.push(user); - } else { - user.metadata[relay] = { - ...user.metadata[relay], - timestamp: evt.created_at, - ...content, - }; - // use only the first profile pic (for now), different pics on each releay are not supported yet - if (!user.picture) { - user.picture = picture; - } - } - // update profile images - if (user.picture && validatePow(evt)) { - 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]) { - // const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey); - // if (updates) { - // console.log('TODO: add contact list (kind 3)', updates); - // } - // } -} - -function getMetadata(evt, relay) { - const host = getHost(relay); - const user = userList.find(user => user.pubkey === evt.pubkey); - const userImg = user?.picture; - const name = user?.metadata[relay]?.name; - const userName = name || evt.pubkey.slice(0, 8); - const userAbout = user?.metadata[relay]?.about || ''; - const img = (userImg && validatePow(evt)) ? elem('img', { - alt: `${userName} ${host}`, - loading: 'lazy', - src: userImg, - title: `${userName} on ${host} ${userAbout}`, - }) : elemCanvas(evt.pubkey); - const time = new Date(evt.created_at * 1000); - return {host, img, name, time, userName}; -} - // subscribe and change view function route(path) { if (path === '/') { diff --git a/src/media.ts b/src/media.ts index 5fa28c4..5653aa6 100644 --- a/src/media.ts +++ b/src/media.ts @@ -1,7 +1,7 @@ import { elem } from './utils/dom'; import { getNoxyUrl } from './utils/url'; -export const parseContent = (content: string) => { +export const parseContent = (content: string): unknown => { try { return JSON.parse(content); } catch(err) { diff --git a/src/profiles.ts b/src/profiles.ts new file mode 100644 index 0000000..ccbe0d9 --- /dev/null +++ b/src/profiles.ts @@ -0,0 +1,158 @@ +import {Event} from 'nostr-tools'; +import {elem, elemCanvas} from './utils/dom'; +import {getHost, getNoxyUrl} from './utils/url'; +import {validatePow} from './events'; +import {parseContent} from './media'; + +type Metadata = { + name?: string; + about?: string; + picture?: string; +}; + +type Profile = { + metadata: { + [relay: string]: Metadata; + }; + name?: string; + picture?: string; + pubkey: string; +}; + +const userList: Array = []; +// const tempContactList = {}; + +const setMetadata = ( + evt: Event, + relay: string, + metadata: Metadata, +) => { + let user = userList.find(u => u.pubkey === evt.pubkey); + if (!user) { + user = { + metadata: {[relay]: metadata}, + pubkey: evt.pubkey, + }; + userList.push(user); + } else { + user.metadata[relay] = { + ...user.metadata[relay], + // timestamp: evt.created_at, + ...metadata, + }; + } + + // store the first seen name (for now) as main user.name + if (!user.name && metadata.name) { + user.name = metadata.name; + } + + // use the first seen profile pic (for now), pics from different relays are not supported yet + if (!user.picture && metadata.picture) { + const imgUrl = getNoxyUrl('data', metadata.picture, evt.id, relay); + if (imgUrl) { + user.picture = imgUrl.href; + + // update profile images that used some nip-13 work + if (imgUrl.href && validatePow(evt)) { + document.body + .querySelectorAll(`canvas[data-pubkey="${evt.pubkey}"]`) + .forEach(canvas => canvas.parentNode?.replaceChild(elem('img', {src: imgUrl.href}), canvas)); + } + } + } + + // update profile names + const name = user.metadata[relay].name || user.name || ''; + if (name) { + document.body + .querySelectorAll(`[data-pubkey="${evt.pubkey}"] .mbox-username:not(.mbox-kind0-name)`) + .forEach((username: HTMLElement) => { + username.textContent = name; + username.classList.add('mbox-kind0-name'); + }); + } + // if (tempContactList[relay]) { + // const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey); + // if (updates) { + // console.log('TODO: add contact list (kind 3)', updates); + // } + // } +}; + +export const handleMetadata = (evt: Event, relay: string) => { + const content = parseContent(evt.content); + if (!content || typeof content !== 'object' || Array.isArray(content)) { + console.warn('expected nip-01 JSON object with user info, but got something funny', evt); + return; + } + const hasNameString = 'name' in content && typeof content.name === 'string'; + const hasAboutString = 'about' in content && typeof content.about === 'string'; + const hasPictureString = 'picture' in content && typeof content.picture === 'string'; + // custom + const hasDisplayName = 'display_name' in content && typeof content.display_name === 'string'; + if (!hasNameString && !hasAboutString && !hasPictureString && !hasDisplayName) { + console.warn('expected basic nip-01 user info (name, about, picture) but nothing found', evt); + return; + } + const metadata: Metadata = { + ...(hasNameString && {name: content.name as string} || hasDisplayName && {name: content.display_name as string}), + ...(hasAboutString && {about: content.about as string}), + ...(hasPictureString && {picture: content.picture as string}), + }; + setMetadata(evt, relay, metadata); +}; + +export const getMetadata = (evt: Event, relay: string) => { + const host = getHost(relay); + const user = userList.find(user => user.pubkey === evt.pubkey); + const userImg = user?.picture; + const name = user?.metadata[relay]?.name || user?.name; + const userName = name || evt.pubkey.slice(0, 8); + const userAbout = user?.metadata[relay]?.about || ''; + const img = (userImg && validatePow(evt)) ? elem('img', { + alt: `${userName} ${host}`, + loading: 'lazy', + src: userImg, + title: `${userName} on ${host} ${userAbout}`, + }) : elemCanvas(evt.pubkey); + const time = new Date(evt.created_at * 1000); + return {host, img, name, time, userName}; +}; + +/* export function handleContactList(evt, relay) { + if (getViewElem(evt.id)) { + return; + } + const art = renderUpdateContact(evt, relay); + if (textNoteList.length < 2) { + getViewContent().append(art); + return; + } + const closestTextNotes = textNoteList.sort(sortEventCreatedAt(evt.created_at)); + getViewElem(closestTextNotes[0].id).after(art); + setViewElem(evt.id, art); + // const user = userList.find(u => u.pupkey === evt.pubkey); + // if (user) { + // console.log(`TODO: add contact list for ${evt.pubkey.slice(0, 8)} on ${relay}`, evt.tags); + // } else { + // tempContactList[relay] = tempContactList[relay] + // ? [...tempContactList[relay], evt] + // : [evt]; + // } +} */ + +// function renderUpdateContact(evt, relay) { +// const {img, time, userName} = getMetadata(evt, relay); +// const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [ +// elem('header', {className: 'mbox-header'}, [ +// elem('small', {}, []), +// ]), +// elem('pre', {title: JSON.stringify(evt.content)}, [ +// elem('strong', {}, userName), +// ' updated contacts: ', +// JSON.stringify(evt.tags), +// ]), +// ]); +// return renderArticle([img, body], {className: 'mbox-updated-contact', data: {id: evt.id, pubkey: evt.pubkey, relay}}); +// }