feed: render links in text notes
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details

Links should be rendered as anchors so that they are clickable.
For now only links starting with http:// https:// or www. are
supported.

Temporary disabled text notes shortening.
pull/29/head
OFF0 2 years ago
parent ec9945cf05
commit 8ec9420be8
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -24,16 +24,52 @@ export function elem(name = 'div', {data, ...props} = {}, children = []) {
return el; return el;
} }
/** function isValidURL(url) {
* Renders line breaks if (!['http:', 'https:'].includes(url.protocol)) {
* return false;
* @param {string} text with newlines }
* @return Array<TextNode | HTMLBRElement> if (!['', '443', '80'].includes(url.port)) {
*/ return false;
export function multilineText(string) { }
const lastDot = url.hostname.lastIndexOf('.');
if (lastDot < 1) {
return false;
}
if (url.hostname.slice(lastDot) === '.local') {
return false;
}
return true;
}
export function parseTextContent(string) {
return string return string
.trimRight() .trimRight()
.replaceAll(/\n{3,}/g, '\n\n') .replaceAll(/\n{3,}/g, '\n\n')
.split('\n') .split('\n')
.reduce((acc, next, i) => acc.concat(i === 0 ? next : [elem('br'), next]), []); .map(line => {
const words = line.split(' ');
return words.map(word => {
if (!word.match(/(https?:\/\/|www\.)\S*/)) {
return word;
}
try {
if (!word.startsWith('http')) {
word = 'https://' + word;
}
const url = new URL(word);
if (!isValidURL(url)) {
return word;
}
return elem('a', {
href: url.href,
target: '_blank',
rel: 'noopener noreferrer'
}, url.href.slice(url.protocol.length + 2));
} catch (err) {
return word;
}
})
.reduce((acc, word) => [...acc, word, ' '], []);
})
.reduce((acc, words) => [...acc, ...words, elem('br')], []);
} }

@ -1,5 +1,5 @@
import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools'; import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools';
import {elem, multilineText} from './domutil.js'; import {elem, parseTextContent} from './domutil.js';
import {dateTime, formatTime} from './timeutil.js'; import {dateTime, formatTime} from './timeutil.js';
// curl -H 'accept: application/nostr+json' https://nostr.x1ddos.ch // curl -H 'accept: application/nostr+json' https://nostr.x1ddos.ch
const pool = relayPool(); const pool = relayPool();
@ -155,11 +155,12 @@ setInterval(() => {
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;
const content = isLongContent ? evt.content.slice(0, 280) : evt.content; // const content = isLongContent ? evt.content.slice(0, 280) : evt.content;
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 body = elem('div', {className: 'mbox-body'}, [ const body = elem('div', {className: 'mbox-body'}, [
elem('header', { elem('header', {
className: 'mbox-header', className: 'mbox-header',
@ -173,7 +174,7 @@ 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}, multilineText(content)), elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, content),
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},
@ -630,12 +631,12 @@ privateKeyInput.value = localStorage.getItem('private_key');
pubKeyInput.value = localStorage.getItem('pub_key'); pubKeyInput.value = localStorage.getItem('pub_key');
document.body.addEventListener('click', (e) => { document.body.addEventListener('click', (e) => {
const container = e.target.closest('[data-append]'); // const container = e.target.closest('[data-append]');
if (container) { // if (container) {
container.append(...multilineText(container.dataset.append)); // container.append(...parseTextContent(container.dataset.append));
delete container.dataset.append; // delete container.dataset.append;
return; // return;
} // }
const back = e.target.closest('[name="back"]') const back = e.target.closest('[name="back"]')
if (back) { if (back) {
hideNewMessage(true); hideNewMessage(true);

Loading…
Cancel
Save