First small step to resolving mastodon links

And open them inside Phanpy instead of like an external link
This commit is contained in:
Lim Chee Aun 2023-02-23 16:45:53 +08:00
parent 4b88c6ca65
commit e6d6adb480
5 changed files with 125 additions and 16 deletions

17
package-lock.json generated
View file

@ -20,6 +20,7 @@
"masto": "~5.10.0", "masto": "~5.10.0",
"mem": "~9.0.2", "mem": "~9.0.2",
"p-retry": "~5.1.2", "p-retry": "~5.1.2",
"p-throttle": "~5.0.0",
"preact": "~10.12.1", "preact": "~10.12.1",
"react-hotkeys-hook": "~4.3.7", "react-hotkeys-hook": "~4.3.7",
"react-intersection-observer": "~9.4.2", "react-intersection-observer": "~9.4.2",
@ -4985,6 +4986,17 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-throttle": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-5.0.0.tgz",
"integrity": "sha512-iXBFjW4kP/5Ivw7uC9EDnj+/xo3pNn4Rws3zgMGPwXnWTv1M3P0LVdZxLrqRUI5JK0Fp3Du0bt6lCaVrI3WF7g==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/param-case": { "node_modules/param-case": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@ -10482,6 +10494,11 @@
"retry": "^0.13.1" "retry": "^0.13.1"
} }
}, },
"p-throttle": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-5.0.0.tgz",
"integrity": "sha512-iXBFjW4kP/5Ivw7uC9EDnj+/xo3pNn4Rws3zgMGPwXnWTv1M3P0LVdZxLrqRUI5JK0Fp3Du0bt6lCaVrI3WF7g=="
},
"param-case": { "param-case": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",

View file

@ -22,6 +22,7 @@
"masto": "~5.10.0", "masto": "~5.10.0",
"mem": "~9.0.2", "mem": "~9.0.2",
"p-retry": "~5.1.2", "p-retry": "~5.1.2",
"p-throttle": "~5.0.0",
"preact": "~10.12.1", "preact": "~10.12.1",
"react-hotkeys-hook": "~4.3.7", "react-hotkeys-hook": "~4.3.7",
"react-intersection-observer": "~9.4.2", "react-intersection-observer": "~9.4.2",

View file

@ -2,6 +2,7 @@ import './status.css';
import { Menu, MenuItem } from '@szhsin/react-menu'; import { Menu, MenuItem } from '@szhsin/react-menu';
import mem from 'mem'; import mem from 'mem';
import pThrottle from 'p-throttle';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import 'swiped-events'; import 'swiped-events';
@ -26,6 +27,11 @@ import Link from './link';
import Media from './media'; import Media from './media';
import RelativeTime from './relative-time'; import RelativeTime from './relative-time';
const throttle = pThrottle({
limit: 1,
interval: 1000,
});
function fetchAccount(id, masto) { function fetchAccount(id, masto) {
try { try {
return masto.v1.accounts.fetch(id); return masto.v1.accounts.fetch(id);
@ -374,14 +380,26 @@ function Status({
__html: enhanceContent(content, { __html: enhanceContent(content, {
emojis, emojis,
postEnhanceDOM: (dom) => { postEnhanceDOM: (dom) => {
// Remove target="_blank" from links
dom dom
.querySelectorAll('a.u-url[target="_blank"]') .querySelectorAll('a.u-url[target="_blank"]')
.forEach((a) => { .forEach((a) => {
// Remove target="_blank" from links
if (!/http/i.test(a.innerText.trim())) { if (!/http/i.test(a.innerText.trim())) {
a.removeAttribute('target'); a.removeAttribute('target');
} }
}); });
// Unfurl Mastodon links
dom
.querySelectorAll(
'a[href]:not(.u-url):not(.mention):not(.hashtag)',
)
.forEach((a) => {
if (isMastodonLinkMaybe(a.href)) {
unfurlMastodonLink(currentInstance, a.href).then(() => {
a.removeAttribute('target');
});
}
});
}, },
}), }),
}} }}
@ -463,7 +481,9 @@ function Status({
!sensitive && !sensitive &&
!spoilerText && !spoilerText &&
!poll && !poll &&
!mediaAttachments.length && <Card card={card} />} !mediaAttachments.length && (
<Card card={card} instance={currentInstance} />
)}
</div> </div>
{size === 'l' && ( {size === 'l' && (
<> <>
@ -702,7 +722,7 @@ function Status({
); );
} }
function Card({ card }) { function Card({ card, instance }) {
const { const {
blurhash, blurhash,
title, title,
@ -729,12 +749,38 @@ function Card({ card }) {
const isLandscape = width / height >= 1.2; const isLandscape = width / height >= 1.2;
const size = isLandscape ? 'large' : ''; const size = isLandscape ? 'large' : '';
const [cardStatusURL, setCardStatusURL] = useState(null);
// const [cardStatusID, setCardStatusID] = useState(null);
useEffect(() => {
if (hasText && image && isMastodonLinkMaybe(url)) {
unfurlMastodonLink(instance, url).then((result) => {
if (!result) return;
const { id, url } = result;
setCardStatusURL('#' + url);
// NOTE: This is for quote post
// (async () => {
// const { masto } = api({ instance });
// const status = await masto.v1.statuses.fetch(id);
// saveStatus(status, instance);
// setCardStatusID(id);
// })();
});
}
}, [hasText, image]);
// if (cardStatusID) {
// return (
// <Status statusID={cardStatusID} instance={instance} size="s" readOnly />
// );
// }
if (hasText && image) { if (hasText && image) {
const domain = new URL(url).hostname.replace(/^www\./, ''); const domain = new URL(url).hostname.replace(/^www\./, '');
return ( return (
<a <a
href={url} href={cardStatusURL || url}
target="_blank" target={cardStatusURL ? null : '_blank'}
rel="nofollow noopener noreferrer" rel="nofollow noopener noreferrer"
class={`card link ${size}`} class={`card link ${size}`}
> >
@ -1129,4 +1175,51 @@ export function formatDuration(time) {
} }
} }
function isMastodonLinkMaybe(url) {
return /^https:\/\/.*\/\d+$/i.test(url);
}
const denylistDomains = /(twitter|github)\.com/i;
function _unfurlMastodonLink(instance, url) {
if (denylistDomains.test(url)) {
return;
}
const instanceRegex = new RegExp(instance + '/');
if (instanceRegex.test(states.unfurledLinks[url]?.url)) {
return Promise.resolve(states.unfurledLinks[url]);
}
console.debug('🦦 Unfurling URL', url);
const { masto } = api({ instance });
return masto.v2
.search({
q: url,
type: 'statuses',
resolve: true,
limit: 1,
})
.then((results) => {
if (results.statuses.length > 0) {
const status = results.statuses[0];
const { id } = status;
const statusURL = `/${instance}/s/${id}`;
const result = {
id,
url: statusURL,
};
console.debug('🦦 Unfurled URL', url, id, statusURL);
states.unfurledLinks[url] = result;
return result;
} else {
throw new Error('No results');
}
})
.catch((e) => {
console.warn(e);
// Silently fail
});
}
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
export default memo(Status); export default memo(Status);

View file

@ -4,13 +4,9 @@ function handleContentLinks(opts) {
const { mentions = [], instance } = opts || {}; const { mentions = [], instance } = opts || {};
return (e) => { return (e) => {
let { target } = e; let { target } = e;
if (target.parentNode.tagName.toLowerCase() === 'a') { target = target.closest('a');
target = target.parentNode; if (!target) return;
} if (target.classList.contains('u-url')) {
if (
target.tagName.toLowerCase() === 'a' &&
target.classList.contains('u-url')
) {
const targetText = ( const targetText = (
target.querySelector('span') || target target.querySelector('span') || target
).innerText.trim(); ).innerText.trim();
@ -39,16 +35,17 @@ function handleContentLinks(opts) {
instance, instance,
}; };
} }
} else if ( } else if (target.classList.contains('hashtag')) {
target.tagName.toLowerCase() === 'a' &&
target.classList.contains('hashtag')
) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const tag = target.innerText.replace(/^#/, '').trim(); const tag = target.innerText.replace(/^#/, '').trim();
const hashURL = instance ? `#/${instance}/t/${tag}` : `#/t/${tag}`; const hashURL = instance ? `#/${instance}/t/${tag}` : `#/t/${tag}`;
console.log({ hashURL }); console.log({ hashURL });
location.hash = hashURL; location.hash = hashURL;
} else if (states.unfurledLinks[target.href]?.url) {
e.preventDefault();
e.stopPropagation();
location.hash = `#${states.unfurledLinks[target.href].url}`;
} }
}; };
} }

View file

@ -24,6 +24,7 @@ const states = proxy({
reloadStatusPage: 0, reloadStatusPage: 0,
spoilers: {}, spoilers: {},
scrollPositions: {}, scrollPositions: {},
unfurledLinks: {},
// Modals // Modals
showCompose: false, showCompose: false,
showSettings: false, showSettings: false,