this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Extract unfurling out of status component

+210 -164
+28 -160
src/components/status.jsx
··· 9 9 MenuItem, 10 10 } from '@szhsin/react-menu'; 11 11 import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash'; 12 - import pThrottle from 'p-throttle'; 13 12 import { memo } from 'preact/compat'; 14 13 import { 15 14 useCallback, ··· 20 19 useState, 21 20 } from 'preact/hooks'; 22 21 import { useHotkeys } from 'react-hotkeys-hook'; 23 - import { InView } from 'react-intersection-observer'; 24 22 import { useLongPress } from 'use-long-press'; 25 23 import { useSnapshot } from 'valtio'; 26 - import { snapshot } from 'valtio/vanilla'; 27 24 28 25 import AccountBlock from '../components/account-block'; 29 26 import EmojiText from '../components/emoji-text'; ··· 54 51 import states, { getStatus, saveStatus, statusKey } from '../utils/states'; 55 52 import statusPeek from '../utils/status-peek'; 56 53 import store from '../utils/store'; 54 + import unfurlMastodonLink from '../utils/unfurl-link'; 57 55 import useTruncated from '../utils/useTruncated'; 58 56 import visibilityIconsMap from '../utils/visibility-icons-map'; 59 57 ··· 68 66 69 67 const SHOW_COMMENT_COUNT_LIMIT = 280; 70 68 const INLINE_TRANSLATE_LIMIT = 140; 71 - const throttle = pThrottle({ 72 - limit: 1, 73 - interval: 1000, 74 - }); 75 69 76 70 function fetchAccount(id, masto) { 77 71 return masto.v1.accounts.$select(id).fetch(); ··· 1587 1581 a.removeAttribute('target'); 1588 1582 } 1589 1583 }); 1590 - if (previewMode) return; 1584 + // if (previewMode) return; 1591 1585 // Unfurl Mastodon links 1592 - Array.from( 1593 - dom.querySelectorAll( 1594 - 'a[href]:not(.u-url):not(.mention):not(.hashtag)', 1595 - ), 1596 - ) 1597 - .filter((a) => { 1598 - const url = a.href; 1599 - const isPostItself = 1600 - url === status.url || url === status.uri; 1601 - return !isPostItself && isMastodonLinkMaybe(url); 1602 - }) 1603 - .forEach((a, i) => { 1604 - unfurlMastodonLink(currentInstance, a.href).then( 1605 - (result) => { 1606 - if (!result) return; 1607 - a.removeAttribute('target'); 1608 - if (!sKey) return; 1609 - if (!Array.isArray(states.statusQuotes[sKey])) { 1610 - states.statusQuotes[sKey] = []; 1611 - } 1612 - if (!states.statusQuotes[sKey][i]) { 1613 - states.statusQuotes[sKey].splice(i, 0, result); 1614 - } 1615 - }, 1616 - ); 1617 - }); 1586 + // Array.from( 1587 + // dom.querySelectorAll( 1588 + // 'a[href]:not(.u-url):not(.mention):not(.hashtag)', 1589 + // ), 1590 + // ) 1591 + // .filter((a) => { 1592 + // const url = a.href; 1593 + // const isPostItself = 1594 + // url === status.url || url === status.uri; 1595 + // return !isPostItself && isMastodonLinkMaybe(url); 1596 + // }) 1597 + // .forEach((a, i) => { 1598 + // unfurlMastodonLink(currentInstance, a.href).then( 1599 + // (result) => { 1600 + // if (!result) return; 1601 + // a.removeAttribute('target'); 1602 + // if (!sKey) return; 1603 + // if (!Array.isArray(states.statusQuotes[sKey])) { 1604 + // states.statusQuotes[sKey] = []; 1605 + // } 1606 + // if (!states.statusQuotes[sKey][i]) { 1607 + // states.statusQuotes[sKey].splice(i, 0, result); 1608 + // } 1609 + // }, 1610 + // ); 1611 + // }); 1618 1612 }, 1619 1613 }), 1620 1614 }} ··· 2257 2251 } 2258 2252 } 2259 2253 2260 - const denylistDomains = /(twitter|github)\.com/i; 2261 - const failedUnfurls = {}; 2262 - 2263 - function _unfurlMastodonLink(instance, url) { 2264 - const snapStates = snapshot(states); 2265 - if (denylistDomains.test(url)) { 2266 - return; 2267 - } 2268 - if (failedUnfurls[url]) { 2269 - return; 2270 - } 2271 - const instanceRegex = new RegExp(instance + '/'); 2272 - if (instanceRegex.test(snapStates.unfurledLinks[url]?.url)) { 2273 - return Promise.resolve(snapStates.unfurledLinks[url]); 2274 - } 2275 - console.debug('🦦 Unfurling URL', url); 2276 - 2277 - let remoteInstanceFetch; 2278 - let theURL = url; 2279 - 2280 - // https://elk.zone/domain.com/@stest/123 -> https://domain.com/@stest/123 2281 - if (/\/\/elk\.[^\/]+\/[^\/]+\.[^\/]+/i.test(theURL)) { 2282 - theURL = theURL.replace(/elk\.[^\/]+\//i, ''); 2283 - } 2284 - 2285 - // https://trunks.social/status/domain.com/@stest/123 -> https://domain.com/@stest/123 2286 - if (/\/\/trunks\.[^\/]+\/status\/[^\/]+\.[^\/]+/i.test(theURL)) { 2287 - theURL = theURL.replace(/trunks\.[^\/]+\/status\//i, ''); 2288 - } 2289 - 2290 - // https://phanpy.social/#/domain.com/s/123 -> https://domain.com/statuses/123 2291 - if (/\/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(theURL)) { 2292 - const urlAfterHash = theURL.split('/#/')[1]; 2293 - const finalURL = urlAfterHash.replace(/\/s\//i, '/@fakeUsername/'); 2294 - theURL = `https://${finalURL}`; 2295 - } 2296 - 2297 - let urlObj; 2298 - try { 2299 - urlObj = new URL(theURL); 2300 - } catch (e) { 2301 - return; 2302 - } 2303 - const domain = urlObj.hostname; 2304 - const path = urlObj.pathname; 2305 - // Regex /:username/:id, where username = @username or @username@domain, id = number 2306 - const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/(\d+)$/i; 2307 - const statusMatch = statusRegex.exec(path); 2308 - if (statusMatch) { 2309 - const id = statusMatch[3]; 2310 - const { masto } = api({ instance: domain }); 2311 - remoteInstanceFetch = masto.v1.statuses 2312 - .$select(id) 2313 - .fetch() 2314 - .then((status) => { 2315 - if (status?.id) { 2316 - return { 2317 - status, 2318 - instance: domain, 2319 - }; 2320 - } else { 2321 - throw new Error('No results'); 2322 - } 2323 - }); 2324 - } 2325 - 2326 - const { masto } = api({ instance }); 2327 - const mastoSearchFetch = masto.v2.search 2328 - .fetch({ 2329 - q: theURL, 2330 - type: 'statuses', 2331 - resolve: true, 2332 - limit: 1, 2333 - }) 2334 - .then((results) => { 2335 - if (results.statuses.length > 0) { 2336 - const status = results.statuses[0]; 2337 - return { 2338 - status, 2339 - instance, 2340 - }; 2341 - } else { 2342 - throw new Error('No results'); 2343 - } 2344 - }); 2345 - 2346 - function handleFulfill(result) { 2347 - const { status, instance } = result; 2348 - const { id } = status; 2349 - const selfURL = `/${instance}/s/${id}`; 2350 - console.debug('🦦 Unfurled URL', url, id, selfURL); 2351 - const data = { 2352 - id, 2353 - instance, 2354 - url: selfURL, 2355 - }; 2356 - states.unfurledLinks[url] = data; 2357 - saveStatus(status, instance, { 2358 - skipThreading: true, 2359 - }); 2360 - return data; 2361 - } 2362 - function handleCatch(e) { 2363 - failedUnfurls[url] = true; 2364 - } 2365 - 2366 - if (remoteInstanceFetch) { 2367 - // return Promise.any([remoteInstanceFetch, mastoSearchFetch]) 2368 - // .then(handleFulfill) 2369 - // .catch(handleCatch); 2370 - // If mastoSearchFetch is fulfilled within 3s, return it, else return remoteInstanceFetch 2371 - const finalPromise = Promise.race([ 2372 - mastoSearchFetch, 2373 - new Promise((resolve, reject) => setTimeout(reject, 3000)), 2374 - ]).catch(() => { 2375 - // If remoteInstanceFetch is fullfilled, return it, else return mastoSearchFetch 2376 - return remoteInstanceFetch.catch(() => mastoSearchFetch); 2377 - }); 2378 - return finalPromise.then(handleFulfill).catch(handleCatch); 2379 - } else { 2380 - return mastoSearchFetch.then(handleFulfill).catch(handleCatch); 2381 - } 2382 - } 2383 - 2384 2254 function nicePostURL(url) { 2385 2255 if (!url) return; 2386 2256 const urlObj = new URL(url); ··· 2403 2273 </> 2404 2274 ); 2405 2275 } 2406 - 2407 - const unfurlMastodonLink = throttle(_unfurlMastodonLink); 2408 2276 2409 2277 function FilteredStatus({ 2410 2278 status,
+46 -4
src/utils/states.js
··· 2 2 import { subscribeKey } from 'valtio/utils'; 3 3 4 4 import { api } from './api'; 5 + import isMastodonLinkMaybe from './isMastodonLinkMaybe'; 5 6 import pmem from './pmem'; 6 7 import rateLimit from './ratelimit'; 7 8 import store from './store'; 9 + import unfurlMastodonLink from './unfurl-link'; 8 10 9 11 const states = proxy({ 10 12 appVersion: {}, ··· 168 170 opts = instance; 169 171 instance = null; 170 172 } 171 - const { override, skipThreading } = Object.assign( 172 - { override: true, skipThreading: false }, 173 - opts, 174 - ); 173 + const { 174 + override = true, 175 + skipThreading = false, 176 + skipUnfurling = false, 177 + } = opts || {}; 175 178 if (!status) return; 176 179 const oldStatus = getStatus(status.id, instance); 177 180 if (!override && oldStatus) return; ··· 195 198 threadifyStatus(status.reblog, instance); 196 199 }); 197 200 } 201 + }); 202 + } 203 + 204 + // UNFURLER 205 + if (!skipUnfurling) { 206 + queueMicrotask(() => { 207 + unfurlStatus(status, instance); 198 208 }); 199 209 } 200 210 } ··· 239 249 }); 240 250 } 241 251 export const threadifyStatus = rateLimit(_threadifyStatus, 100); 252 + 253 + const fauxDiv = document.createElement('div'); 254 + export function unfurlStatus(status, instance) { 255 + const { instance: currentInstance } = api(); 256 + const content = status.reblog?.content || status.content; 257 + const hasLink = /<a/i.test(content); 258 + if (hasLink) { 259 + const sKey = statusKey(status?.reblog?.id || status?.id, instance); 260 + fauxDiv.innerHTML = content; 261 + const links = fauxDiv.querySelectorAll( 262 + 'a[href]:not(.u-url):not(.mention):not(.hashtag)', 263 + ); 264 + [...links] 265 + .filter((a) => { 266 + const url = a.href; 267 + const isPostItself = url === status.url || url === status.uri; 268 + return !isPostItself && isMastodonLinkMaybe(url); 269 + }) 270 + .forEach((a, i) => { 271 + unfurlMastodonLink(currentInstance, a.href).then((result) => { 272 + if (!result) return; 273 + if (!sKey) return; 274 + if (!Array.isArray(states.statusQuotes[sKey])) { 275 + states.statusQuotes[sKey] = []; 276 + } 277 + if (!states.statusQuotes[sKey][i]) { 278 + states.statusQuotes[sKey].splice(i, 0, result); 279 + } 280 + }); 281 + }); 282 + } 283 + } 242 284 243 285 const fetchStatus = pmem((statusID, masto) => { 244 286 return masto.v1.statuses.$select(statusID).fetch();
+136
src/utils/unfurl-link.jsx
··· 1 + import pThrottle from 'p-throttle'; 2 + import { snapshot } from 'valtio/vanilla'; 3 + 4 + import { api } from './api'; 5 + import states, { saveStatus } from './states'; 6 + 7 + export const throttle = pThrottle({ 8 + limit: 1, 9 + interval: 1000, 10 + }); 11 + 12 + const denylistDomains = /(twitter|github)\.com/i; 13 + const failedUnfurls = {}; 14 + function _unfurlMastodonLink(instance, url) { 15 + const snapStates = snapshot(states); 16 + if (denylistDomains.test(url)) { 17 + return; 18 + } 19 + if (failedUnfurls[url]) { 20 + return; 21 + } 22 + const instanceRegex = new RegExp(instance + '/'); 23 + if (instanceRegex.test(snapStates.unfurledLinks[url]?.url)) { 24 + return Promise.resolve(snapStates.unfurledLinks[url]); 25 + } 26 + console.debug('🦦 Unfurling URL', url); 27 + 28 + let remoteInstanceFetch; 29 + let theURL = url; 30 + 31 + // https://elk.zone/domain.com/@stest/123 -> https://domain.com/@stest/123 32 + if (/\/\/elk\.[^\/]+\/[^\/]+\.[^\/]+/i.test(theURL)) { 33 + theURL = theURL.replace(/elk\.[^\/]+\//i, ''); 34 + } 35 + 36 + // https://trunks.social/status/domain.com/@stest/123 -> https://domain.com/@stest/123 37 + if (/\/\/trunks\.[^\/]+\/status\/[^\/]+\.[^\/]+/i.test(theURL)) { 38 + theURL = theURL.replace(/trunks\.[^\/]+\/status\//i, ''); 39 + } 40 + 41 + // https://phanpy.social/#/domain.com/s/123 -> https://domain.com/statuses/123 42 + if (/\/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(theURL)) { 43 + const urlAfterHash = theURL.split('/#/')[1]; 44 + const finalURL = urlAfterHash.replace(/\/s\//i, '/@fakeUsername/'); 45 + theURL = `https://${finalURL}`; 46 + } 47 + 48 + let urlObj; 49 + try { 50 + urlObj = new URL(theURL); 51 + } catch (e) { 52 + return; 53 + } 54 + const domain = urlObj.hostname; 55 + const path = urlObj.pathname; 56 + // Regex /:username/:id, where username = @username or @username@domain, id = number 57 + const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/(\d+)$/i; 58 + const statusMatch = statusRegex.exec(path); 59 + if (statusMatch) { 60 + const id = statusMatch[3]; 61 + const { masto } = api({ instance: domain }); 62 + remoteInstanceFetch = masto.v1.statuses 63 + .$select(id) 64 + .fetch() 65 + .then((status) => { 66 + if (status?.id) { 67 + return { 68 + status, 69 + instance: domain, 70 + }; 71 + } else { 72 + throw new Error('No results'); 73 + } 74 + }); 75 + } 76 + 77 + const { masto } = api({ instance }); 78 + const mastoSearchFetch = masto.v2.search 79 + .fetch({ 80 + q: theURL, 81 + type: 'statuses', 82 + resolve: true, 83 + limit: 1, 84 + }) 85 + .then((results) => { 86 + if (results.statuses.length > 0) { 87 + const status = results.statuses[0]; 88 + return { 89 + status, 90 + instance, 91 + }; 92 + } else { 93 + throw new Error('No results'); 94 + } 95 + }); 96 + 97 + function handleFulfill(result) { 98 + const { status, instance } = result; 99 + const { id } = status; 100 + const selfURL = `/${instance}/s/${id}`; 101 + console.debug('🦦 Unfurled URL', url, id, selfURL); 102 + const data = { 103 + id, 104 + instance, 105 + url: selfURL, 106 + }; 107 + states.unfurledLinks[url] = data; 108 + saveStatus(status, instance, { 109 + skipThreading: true, 110 + }); 111 + return data; 112 + } 113 + function handleCatch(e) { 114 + failedUnfurls[url] = true; 115 + } 116 + 117 + if (remoteInstanceFetch) { 118 + // return Promise.any([remoteInstanceFetch, mastoSearchFetch]) 119 + // .then(handleFulfill) 120 + // .catch(handleCatch); 121 + // If mastoSearchFetch is fulfilled within 3s, return it, else return remoteInstanceFetch 122 + const finalPromise = Promise.race([ 123 + mastoSearchFetch, 124 + new Promise((resolve, reject) => setTimeout(reject, 3000)), 125 + ]).catch(() => { 126 + // If remoteInstanceFetch is fullfilled, return it, else return mastoSearchFetch 127 + return remoteInstanceFetch.catch(() => mastoSearchFetch); 128 + }); 129 + return finalPromise.then(handleFulfill).catch(handleCatch); 130 + } else { 131 + return mastoSearchFetch.then(handleFulfill).catch(handleCatch); 132 + } 133 + } 134 + 135 + const unfurlMastodonLink = throttle(_unfurlMastodonLink); 136 + export default unfurlMastodonLink;