diff --git a/src/domutil.js b/src/domutil.js index 96c94fb..e364a9b 100644 --- a/src/domutil.js +++ b/src/domutil.js @@ -24,16 +24,52 @@ export function elem(name = 'div', {data, ...props} = {}, children = []) { return el; } -/** - * Renders line breaks - * - * @param {string} text with newlines - * @return Array - */ -export function multilineText(string) { - return string - .trimRight() - .replaceAll(/\n{3,}/g, '\n\n') - .split('\n') - .reduce((acc, next, i) => acc.concat(i === 0 ? next : [elem('br'), next]), []); +function isValidURL(url) { + if (!['http:', 'https:'].includes(url.protocol)) { + return false; + } + if (!['', '443', '80'].includes(url.port)) { + return false; + } + 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 + .trimRight() + .replaceAll(/\n{3,}/g, '\n\n') + .split('\n') + .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')], []); } diff --git a/src/main.js b/src/main.js index 46e5c08..7a98a60 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,5 @@ 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'; // curl -H 'accept: application/nostr+json' https://nostr.x1ddos.ch const pool = relayPool(); @@ -155,11 +155,12 @@ setInterval(() => { function createTextNote(evt, relay) { const {host, img, isReply, name, replies, time, userName} = getMetadata(evt, relay); - const isLongContent = evt.content.trimRight().length > 280; - const content = isLongContent ? evt.content.slice(0, 280) : evt.content; + // const isLongContent = evt.content.trimRight().length > 280; + // const content = isLongContent ? evt.content.slice(0, 280) : evt.content; const hasReactions = reactionMap[evt.id]?.length > 0; 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 content = parseTextContent(evt.content); const body = elem('div', {className: 'mbox-body'}, [ elem('header', { className: 'mbox-header', @@ -173,7 +174,7 @@ function createTextNote(evt, relay) { 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', { className: 'btn-inline', name: 'star', type: 'button', data: {'eventId': evt.id, relay}, @@ -630,12 +631,12 @@ privateKeyInput.value = localStorage.getItem('private_key'); pubKeyInput.value = localStorage.getItem('pub_key'); document.body.addEventListener('click', (e) => { - const container = e.target.closest('[data-append]'); - if (container) { - container.append(...multilineText(container.dataset.append)); - delete container.dataset.append; - return; - } + // const container = e.target.closest('[data-append]'); + // if (container) { + // container.append(...parseTextContent(container.dataset.append)); + // delete container.dataset.append; + // return; + // } const back = e.target.closest('[name="back"]') if (back) { hideNewMessage(true);