feed: display link preview with noxy
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details

fetches the first link and shows it below a text note, all
resources are prxied through noxy.

see nostr/noxy#2
OFF0 2 years ago
parent d8f71b74ec
commit 80944adef7
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -116,3 +116,41 @@
color: var(--color-accent); color: var(--color-accent);
content: "…"; content: "…";
} }
.preview-loaded a {
background-color: var(--bgcolor-textinput);
border: 1px solid var(--bgcolor-inactive);
color: var(--color);
display: flex;
flex-direction: column;
margin: var(--gap) 0 0 0;
max-width: 48rem;
padding: 1.5rem 1.8rem;
text-decoration: none;
}
.preview-loaded a:visited {
color: inherit;
}
.preview-title {
font-size: inherit;
margin: 0;
}
.preview-descr {
font-size: var(--font-small);
}
.preview-image {
margin-bottom: var(--gap);
max-height: 30vh;
object-fit: cover;
}
.preview-image-only {
background-color: var(--bgcolor-textinput);
border: 1px solid var(--bgcolor-inactive);
margin: var(--gap) 0 0 0;
max-width: 48rem;
padding: 1.5rem 1.8rem;
}

@ -42,7 +42,8 @@ function isValidURL(url) {
} }
export function parseTextContent(string) { export function parseTextContent(string) {
return string let firstLink;
return [string
.trimRight() .trimRight()
.replaceAll(/\n{3,}/g, '\n\n') .replaceAll(/\n{3,}/g, '\n\n')
.split('\n') .split('\n')
@ -60,6 +61,7 @@ export function parseTextContent(string) {
if (!isValidURL(url)) { if (!isValidURL(url)) {
return word; return word;
} }
firstLink = firstLink || url.href;
return elem('a', { return elem('a', {
href: url.href, href: url.href,
target: '_blank', target: '_blank',
@ -71,5 +73,6 @@ export function parseTextContent(string) {
}) })
.reduce((acc, word) => [...acc, word, ' '], []); .reduce((acc, word) => [...acc, word, ' '], []);
}) })
.reduce((acc, words) => [...acc, ...words, elem('br')], []); .reduce((acc, words) => [...acc, ...words, elem('br')], []),
{firstLink}];
} }

@ -32,7 +32,7 @@
--bgcolor-inactive: #bababa; --bgcolor-inactive: #bababa;
--bgcolor-textinput: #fff; --bgcolor-textinput: #fff;
--color: rgb(68 68 68); --color: rgb(68 68 68);
--color-accent: #ff731d; --color-accent: rgb(16, 93, 176);
--bgcolor-danger: rgb(255, 80, 80); --bgcolor-danger: rgb(255, 80, 80);
} }
} }

@ -52,7 +52,7 @@ const subscription = pool.sub({
// '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55 // '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55
// ], // ],
// since: new Date(Date.now() - (24 * 60 * 60 * 1000)), // since: new Date(Date.now() - (24 * 60 * 60 * 1000)),
limit: 75, limit: 250,
} }
}); });
@ -157,6 +157,45 @@ setInterval(() => {
}); });
}, 10000); }, 10000);
const getNoxyUrl = (type, url, id, relay) => {
if (!isHttpUrl(url)) {
return false;
}
const link = new URL(`https://noxy.nostr.ch/${type}`);
link.searchParams.set('id', id);
link.searchParams.set('relay', relay);
link.searchParams.set('url', url);
return link;
}
function linkPreview(href, id, relay) {
if ((/\.(gif|jpe?g|png)$/i).test(href)) {
return elem('div', {},
[elem('img', {className: 'preview-image-only', src: getNoxyUrl('data', href, id, relay).href})]
);
}
const noxy = getNoxyUrl('meta', href, id, relay);
const previewId = noxy.searchParams.toString();
fetch(noxy.href)
.then(data => data.json ? data.json() : data)
.then(meta => {
const container = document.getElementById(previewId);
container.append(elem('a', {href, rel: 'noopener noreferrer', target: '_blank'}, [
meta.images[0] && (
elem('img', {className: 'preview-image', src: getNoxyUrl('data', meta.images[0], id, relay).href})
),
elem('h2', {className: 'preview-title'}, meta.title),
elem('p', {className: 'preview-descr'}, meta.descr),
]));
container.classList.add('preview-loaded');
})
.catch(console.warn);
return elem('div', {
className: 'preview',
id: previewId
});
}
function createTextNote(evt, relay) { function createTextNote(evt, relay) {
const {host, img, isReply, name, replies, time, userName} = getMetadata(evt, relay); const {host, img, isReply, name, replies, time, userName} = getMetadata(evt, relay);
// const isLongContent = evt.content.trimRight().length > 280; // const isLongContent = evt.content.trimRight().length > 280;
@ -164,13 +203,14 @@ function createTextNote(evt, relay) {
const hasReactions = reactionMap[evt.id]?.length > 0; const hasReactions = reactionMap[evt.id]?.length > 0;
const didReact = hasReactions && !!reactionMap[evt.id].find(reaction => reaction.pubkey === pubkey); const didReact = hasReactions && !!reactionMap[evt.id].find(reaction => reaction.pubkey === pubkey);
const replyFeed = replies[0] ? replies.map(e => replyDomMap[e.id] = createTextNote(e, relay)) : []; const replyFeed = replies[0] ? replies.map(e => replyDomMap[e.id] = createTextNote(e, relay)) : [];
const content = parseTextContent(evt.content); const [content, {firstLink}] = parseTextContent(evt.content);
const body = elem('div', {className: 'mbox-body'}, [ const body = elem('div', {className: 'mbox-body'}, [
elem('header', { elem('header', {
className: 'mbox-header', className: 'mbox-header',
title: `User: ${userName}\n${time}\n\nUser pubkey: ${evt.pubkey}\n\nRelay: ${host}\n\nEvent-id: ${evt.id} title: `User: ${userName}\n${time}\n\nUser pubkey: ${evt.pubkey}\n\nRelay: ${host}\n\nEvent-id: ${evt.id}
${evt.tags.length ? `\nTags ${JSON.stringify(evt.tags)}\n` : ''} ${evt.tags.length ? `\nTags ${JSON.stringify(evt.tags)}\n` : ''}
${isReply ? `\nReply to ${evt.tags[0][1]}\n` : ''}` ${isReply ? `\nReply to ${evt.tags[0][1]}\n` : ''}
${evt.content}`
}, [ }, [
elem('small', {}, [ elem('small', {}, [
elem('strong', {className: name ? 'mbox-kind0-name' : 'mbox-username'}, name || userName), elem('strong', {className: name ? 'mbox-kind0-name' : 'mbox-username'}, name || userName),
@ -178,7 +218,10 @@ function createTextNote(evt, relay) {
elem('time', {dateTime: time.toISOString()}, formatTime(time)), elem('time', {dateTime: time.toISOString()}, formatTime(time)),
]), ]),
]), ]),
elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, content), elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, [
...content,
firstLink ? linkPreview(firstLink, evt.id, relay) : ''
]),
elem('button', { elem('button', {
className: 'btn-inline', name: 'star', type: 'button', className: 'btn-inline', name: 'star', type: 'button',
data: {'eventId': evt.id, relay}, data: {'eventId': evt.id, relay},
@ -325,20 +368,9 @@ function handleMetadata(evt, relay) {
} }
} }
const getNoxyUrl = (id, relay, url) => {
if (!isHttpUrl(url)) {
return false;
}
const picture = new URL('https://noxy.nostr.ch/data');
picture.searchParams.set('id', id);
picture.searchParams.set('relay', relay);
picture.searchParams.set('url', url);
return picture.href;
}
function setMetadata(evt, relay, content) { function setMetadata(evt, relay, content) {
const user = userList.find(u => u.pubkey === evt.pubkey); const user = userList.find(u => u.pubkey === evt.pubkey);
const picture = getNoxyUrl(evt.id, relay, content.picture); const picture = getNoxyUrl('data', content.picture, evt.id, relay).href;
if (!user) { if (!user) {
userList.push({ userList.push({
metadata: {[relay]: content}, metadata: {[relay]: content},

Loading…
Cancel
Save