diff --git a/src/components/status.jsx b/src/components/status.jsx index b776299f..25490917 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -1819,7 +1819,7 @@ const unfurlMastodonLink = throttle( const root = document.documentElement; const defaultBoundingBoxPadding = 8; -function safeBoundingBoxPadding() { +function _safeBoundingBoxPadding() { // Get safe area inset variables from root const style = getComputedStyle(root); const safeAreaInsetTop = style.getPropertyValue('--sai-top'); @@ -1837,6 +1837,9 @@ function safeBoundingBoxPadding() { // console.log(str); return str; } +const safeBoundingBoxPadding = mem(_safeBoundingBoxPadding, { + maxAge: 10_000, // 10 seconds +}); function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) { const { diff --git a/src/utils/enhance-content.js b/src/utils/enhance-content.js index c91c6328..bf91733b 100644 --- a/src/utils/enhance-content.js +++ b/src/utils/enhance-content.js @@ -7,174 +7,193 @@ function enhanceContent(content, opts = {}) { let enhancedContent = content; const dom = document.createElement('div'); dom.innerHTML = enhancedContent; + const hasLink = / { - link.setAttribute('target', '_blank'); - }); + if (hasLink) { + const noTargetBlankLinks = Array.from( + dom.querySelectorAll('a:not([target="_blank"])'), + ); + noTargetBlankLinks.forEach((link) => { + link.setAttribute('target', '_blank'); + }); + } // Spanify un-spanned mentions - const notMentionLinks = Array.from(dom.querySelectorAll('a[href]')); - notMentionLinks.forEach((link) => { - const text = link.innerText.trim(); - const hasChildren = link.querySelector('*'); - // If text looks like @username@domain, then it's a mention - if (/^@[^@]+(@[^@]+)?$/g.test(text)) { - // Only show @username - const username = text.split('@')[1]; - if (!hasChildren) link.innerHTML = `@${username}`; - link.classList.add('mention'); - } - // If text looks like #hashtag, then it's a hashtag - if (/^#[^#]+$/g.test(text)) { - if (!hasChildren) link.innerHTML = `#${text.slice(1)}`; - link.classList.add('mention', 'hashtag'); - } - }); + if (hasLink) { + const notMentionLinks = Array.from(dom.querySelectorAll('a[href]')); + notMentionLinks.forEach((link) => { + const text = link.innerText.trim(); + const hasChildren = link.querySelector('*'); + // If text looks like @username@domain, then it's a mention + if (/^@[^@]+(@[^@]+)?$/g.test(text)) { + // Only show @username + const username = text.split('@')[1]; + if (!hasChildren) link.innerHTML = `@${username}`; + link.classList.add('mention'); + } + // If text looks like #hashtag, then it's a hashtag + if (/^#[^#]+$/g.test(text)) { + if (!hasChildren) link.innerHTML = `#${text.slice(1)}`; + link.classList.add('mention', 'hashtag'); + } + }); + } // EMOJIS // ====== // Convert :shortcode: to - let textNodes = extractTextNodes(dom); - textNodes.forEach((node) => { - let html = node.nodeValue - .replace(/&/g, '&') - .replace(//g, '>'); - if (emojis) { - html = emojifyText(html, emojis); - } - fauxDiv.innerHTML = html; - const nodes = Array.from(fauxDiv.childNodes); - node.replaceWith(...nodes); - }); + let textNodes; + if (enhancedContent.indexOf(':') !== -1) { + textNodes = extractTextNodes(dom); + textNodes.forEach((node) => { + let html = node.nodeValue + .replace(/&/g, '&') + .replace(//g, '>'); + if (emojis) { + html = emojifyText(html, emojis); + } + fauxDiv.innerHTML = html; + const nodes = Array.from(fauxDiv.childNodes); + node.replaceWith(...nodes); + }); + } // CODE BLOCKS // =========== // Convert ```code``` to
code
- const blocks = Array.from(dom.querySelectorAll('p')).filter((p) => - /^```[^]+```$/g.test(p.innerText.trim()), - ); - blocks.forEach((block) => { - const pre = document.createElement('pre'); - // Replace
with newlines - block.querySelectorAll('br').forEach((br) => br.replaceWith('\n')); - pre.innerHTML = `${block.innerHTML.trim()}`; - block.replaceWith(pre); - }); + if (hasCodeBlock) { + const blocks = Array.from(dom.querySelectorAll('p')).filter((p) => + /^```[^]+```$/g.test(p.innerText.trim()), + ); + blocks.forEach((block) => { + const pre = document.createElement('pre'); + // Replace
with newlines + block.querySelectorAll('br').forEach((br) => br.replaceWith('\n')); + pre.innerHTML = `${block.innerHTML.trim()}`; + block.replaceWith(pre); + }); + } // Convert multi-paragraph code blocks to
code
- const paragraphs = Array.from(dom.querySelectorAll('p')); - // Filter out paragraphs with ``` in beginning only - const codeBlocks = paragraphs.filter((p) => /^```/g.test(p.innerText)); - // For each codeBlocks, get all paragraphs until the last paragraph with ``` at the end only - codeBlocks.forEach((block) => { - const nextParagraphs = [block]; - let hasCodeBlock = false; - let currentBlock = block; - while (currentBlock.nextElementSibling) { - const next = currentBlock.nextElementSibling; - if (next && next.tagName === 'P') { - if (/```$/g.test(next.innerText)) { - nextParagraphs.push(next); - hasCodeBlock = true; - break; + if (hasCodeBlock) { + const paragraphs = Array.from(dom.querySelectorAll('p')); + // Filter out paragraphs with ``` in beginning only + const codeBlocks = paragraphs.filter((p) => /^```/g.test(p.innerText)); + // For each codeBlocks, get all paragraphs until the last paragraph with ``` at the end only + codeBlocks.forEach((block) => { + const nextParagraphs = [block]; + let hasCodeBlock = false; + let currentBlock = block; + while (currentBlock.nextElementSibling) { + const next = currentBlock.nextElementSibling; + if (next && next.tagName === 'P') { + if (/```$/g.test(next.innerText)) { + nextParagraphs.push(next); + hasCodeBlock = true; + break; + } else { + nextParagraphs.push(next); + } } else { - nextParagraphs.push(next); + break; } - } else { - break; + currentBlock = next; } - currentBlock = next; - } - if (hasCodeBlock) { - const pre = document.createElement('pre'); - nextParagraphs.forEach((p) => { - // Replace
with newlines - p.querySelectorAll('br').forEach((br) => br.replaceWith('\n')); - }); - const codeText = nextParagraphs.map((p) => p.innerHTML).join('\n\n'); - pre.innerHTML = `${codeText}`; - block.replaceWith(pre); - nextParagraphs.forEach((p) => p.remove()); - } - }); + if (hasCodeBlock) { + const pre = document.createElement('pre'); + nextParagraphs.forEach((p) => { + // Replace
with newlines + p.querySelectorAll('br').forEach((br) => br.replaceWith('\n')); + }); + const codeText = nextParagraphs.map((p) => p.innerHTML).join('\n\n'); + pre.innerHTML = `${codeText}`; + block.replaceWith(pre); + nextParagraphs.forEach((p) => p.remove()); + } + }); + } // INLINE CODE // =========== // Convert `code` to code - textNodes = extractTextNodes(dom); - textNodes.forEach((node) => { - let html = node.nodeValue - .replace(/&/g, '&') - .replace(//g, '>'); - if (/`[^`]+`/g.test(html)) { - html = html.replaceAll(/(`[^]+?`)/g, '$1'); - } - fauxDiv.innerHTML = html; - const nodes = Array.from(fauxDiv.childNodes); - node.replaceWith(...nodes); - }); + if (enhancedContent.indexOf('`') !== -1) { + textNodes = extractTextNodes(dom); + textNodes.forEach((node) => { + let html = node.nodeValue + .replace(/&/g, '&') + .replace(//g, '>'); + if (/`[^`]+`/g.test(html)) { + html = html.replaceAll(/(`[^]+?`)/g, '$1'); + } + fauxDiv.innerHTML = html; + const nodes = Array.from(fauxDiv.childNodes); + node.replaceWith(...nodes); + }); + } // TWITTER USERNAMES // ================= // Convert @username@twitter.com to
@username@twitter.com - textNodes = extractTextNodes(dom, { - rejectFilter: ['A'], - }); - textNodes.forEach((node) => { - let html = node.nodeValue - .replace(/&/g, '&') - .replace(//g, '>'); - if (/@[a-zA-Z0-9_]+@twitter\.com/g.test(html)) { - html = html.replaceAll( - /(@([a-zA-Z0-9_]+)@twitter\.com)/g, - '$1', - ); - } - fauxDiv.innerHTML = html; - const nodes = Array.from(fauxDiv.childNodes); - node.replaceWith(...nodes); - }); + if (/twitter\.com/i.test(enhancedContent)) { + textNodes = extractTextNodes(dom, { + rejectFilter: ['A'], + }); + textNodes.forEach((node) => { + let html = node.nodeValue + .replace(/&/g, '&') + .replace(//g, '>'); + if (/@[a-zA-Z0-9_]+@twitter\.com/g.test(html)) { + html = html.replaceAll( + /(@([a-zA-Z0-9_]+)@twitter\.com)/g, + '$1', + ); + } + fauxDiv.innerHTML = html; + const nodes = Array.from(fauxDiv.childNodes); + node.replaceWith(...nodes); + }); + } // HASHTAG STUFFING // ================ // Get the

that contains a lot of hashtags, add a class to it - const hashtagStuffedParagraph = Array.from(dom.querySelectorAll('p')).find( - (p) => { - let hashtagCount = 0; - for (let i = 0; i < p.childNodes.length; i++) { - const node = p.childNodes[i]; + if (enhancedContent.indexOf('#') !== -1) { + const hashtagStuffedParagraph = Array.from(dom.querySelectorAll('p')).find( + (p) => { + let hashtagCount = 0; + for (let i = 0; i < p.childNodes.length; i++) { + const node = p.childNodes[i]; - if (node.nodeType === Node.TEXT_NODE) { - const text = node.textContent.trim(); - if (text !== '') { - return false; - } - } else if (node.tagName === 'A') { - const linkText = node.textContent.trim(); - if (!linkText || !linkText.startsWith('#')) { - return false; + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.trim(); + if (text !== '') { + return false; + } + } else if (node.tagName === 'A') { + const linkText = node.textContent.trim(); + if (!linkText || !linkText.startsWith('#')) { + return false; + } else { + hashtagCount++; + } } else { - hashtagCount++; + return false; } - } else { - return false; } - } - // Only consider "stuffing" if there are more than 3 hashtags - return hashtagCount > 3; - }, - ); - if (hashtagStuffedParagraph) { - hashtagStuffedParagraph.classList.add('hashtag-stuffing'); - hashtagStuffedParagraph.title = hashtagStuffedParagraph.innerText; + // Only consider "stuffing" if there are more than 3 hashtags + return hashtagCount > 3; + }, + ); + if (hashtagStuffedParagraph) { + hashtagStuffedParagraph.classList.add('hashtag-stuffing'); + hashtagStuffedParagraph.title = hashtagStuffedParagraph.innerText; + } } if (postEnhanceDOM) { diff --git a/src/utils/locale-match.jsx b/src/utils/locale-match.jsx index f67d1fad..8c46ec65 100644 --- a/src/utils/locale-match.jsx +++ b/src/utils/locale-match.jsx @@ -1,6 +1,7 @@ import { match } from '@formatjs/intl-localematcher'; +import mem from 'mem'; -function localeMatch(...args) { +function _localeMatch(...args) { // Wrap in try/catch because localeMatcher throws on invalid locales try { return match(...args); @@ -8,5 +9,8 @@ function localeMatch(...args) { return false; } } +const localeMatch = mem(_localeMatch, { + cacheKey: (args) => args.join(), +}); export default localeMatch;