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/profiles.ts

150 lines
4.7 KiB
TypeScript

import {Event} from 'nostr-tools';
import {elem, elemCanvas, parseTextContent} from './utils/dom';
import {getNoxyUrl} from './utils/url';
import {getViewElem, getViewOptions} from './view';
import {parseJSON} from './media';
import {sortByCreatedAt} from './events';
type Profile = {
name: string;
about?: string;
picture?: string;
website?: string;
}
const transformMetadata = (data: unknown): Profile | undefined => {
if (!data || typeof data !== 'object' || Array.isArray(data)) {
console.warn('expected nip-01 JSON object with user info, but got something funny', data);
return;
}
const hasNameString = 'name' in data && typeof data.name === 'string';
const hasAboutString = 'about' in data && typeof data.about === 'string';
const hasPictureString = 'picture' in data && typeof data.picture === 'string';
// custom
const hasDisplayName = 'display_name' in data && typeof data.display_name === 'string';
const hasWebsite = 'website' in data && typeof data.website === 'string';
if (!hasNameString && !hasAboutString && !hasPictureString && !hasDisplayName) {
console.warn('expected basic nip-01 user info (name, about, picture) but nothing found', data);
return;
}
const name = (
hasNameString && data.name as string
|| hasDisplayName && data.display_name as string
|| ''
);
return {
name,
...(hasAboutString && {about: data.about as string}),
...(hasPictureString && {picture: data.picture as string}),
...(hasWebsite && {website: data.website as string})
};
};
const profileMap: {
[pubkey: string]: Profile
} = {};
const metadataList: Array<Event> = [];
export const handleMetadata = (evt: Event, relay: string) => {
if (metadataList.find(({id}) => id === evt.id)) {
return;
}
const contactEventList = metadataList.filter(({pubkey}) => pubkey === evt.pubkey);
metadataList.push(evt);
contactEventList.push(evt);
contactEventList.sort(sortByCreatedAt);
if (contactEventList.some(({created_at}) => created_at > evt.created_at) ) {
// evt is older
return;
}
const content = parseJSON(evt.content);
const metadata = transformMetadata(content);
if (!metadata) {
return;
}
profileMap[evt.pubkey] = metadata;
if (metadata.picture) {
const imgUrl = getNoxyUrl('data', metadata.picture, evt.id, relay);
if (imgUrl) {
// 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));
// }
}
}
if (metadata.name) {
// update profile names
document.body
.querySelectorAll(`[data-profile="${evt.pubkey}"]`)
.forEach((username: HTMLElement) => {
username.textContent = metadata.name;
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;
const img = /* (userImg && validatePow(evt)) ? elem('img', {
alt: `${userName} ${host}`,
loading: 'lazy',
src: userImg,
title: `${userName} on ${host} ${userAbout}`,
}) : */ elemCanvas(pubkey);
return {about, img, name, userName};
};
export const renderProfile = (pubkey: string) => {
const header = getViewElem('header');
const metadata = profileMap[pubkey];
if (!header || !metadata) {
return;
}
if (metadata.name) {
const h1 = header.querySelector('h1');
if (h1) {
h1.textContent = metadata.name;
document.title = metadata.name;
} else {
header.append(elem('h1', {}, metadata.name));
}
}
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]');
if (website) {
const url = metadata.website.toLowerCase().startsWith('http') ? metadata.website : `https://${metadata.website}`;
const [content] = parseTextContent(url);
website.replaceChildren(...content);
(website as HTMLDivElement).hidden = false;
} else {
detail.append(elem('div', {data: {website: ''}}, metadata.name));
}
}
};