commit
0b1974e94b
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -33,9 +33,13 @@ const imageRoute = new Route(
|
||||||
);
|
);
|
||||||
registerRoute(imageRoute);
|
registerRoute(imageRoute);
|
||||||
|
|
||||||
// 1-day cache for /api/v1/instance and /api/v1/custom_emojis
|
// 1-day cache for
|
||||||
|
// - /api/v1/instance
|
||||||
|
// - /api/v1/custom_emojis
|
||||||
|
// - /api/v1/preferences
|
||||||
|
// - /api/v1/lists/:id
|
||||||
const apiExtendedRoute = new RegExpRoute(
|
const apiExtendedRoute = new RegExpRoute(
|
||||||
/^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis)/,
|
/^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis|preferences|lists\/\d+)/,
|
||||||
new StaleWhileRevalidate({
|
new StaleWhileRevalidate({
|
||||||
cacheName: 'api-extended',
|
cacheName: 'api-extended',
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
39
src/app.css
39
src/app.css
|
@ -745,14 +745,6 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
background-color: var(--button-bg-color);
|
|
||||||
background-image: linear-gradient(
|
|
||||||
160deg,
|
|
||||||
rgba(255, 255, 255, 0.5),
|
|
||||||
rgba(255, 255, 255, 0) 50%
|
|
||||||
);
|
|
||||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
|
||||||
0 10px 36px -4px var(--button-bg-blur-color);
|
|
||||||
}
|
}
|
||||||
.updates-button .icon {
|
.updates-button .icon {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@ -905,12 +897,16 @@ body:has(.status-deck) .media-post-link {
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
/* Don't do this if there's a modal sheet (.sheet) */
|
/* Don't do this if there's a modal sheet (.sheet) */
|
||||||
:has(#modal-container .carousel):has(.status-deck):not(:has(.sheet))
|
:has(#modal-container .carousel):has(.status-deck):not(
|
||||||
|
:has(.sheet, #compose-container)
|
||||||
|
)
|
||||||
.status-deck {
|
.status-deck {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
:has(#modal-container .carousel):has(.status-deck):not(:has(.sheet))
|
:has(#modal-container .carousel):has(.status-deck):not(
|
||||||
|
:has(.sheet, #compose-container)
|
||||||
|
)
|
||||||
#modal-container
|
#modal-container
|
||||||
> div {
|
> div {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -1099,9 +1095,11 @@ body:has(.status-deck) .media-post-link {
|
||||||
backdrop-filter: blur(8px) saturate(3);
|
backdrop-filter: blur(8px) saturate(3);
|
||||||
border: var(--hairline-width) solid var(--bg-color);
|
border: var(--hairline-width) solid var(--bg-color);
|
||||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color);
|
box-shadow: 0 3px 8px -1px var(--drop-shadow-color);
|
||||||
|
text-shadow: 0 var(--hairline-width) var(--bg-color), 0 0 8px var(--bg-color);
|
||||||
}
|
}
|
||||||
.glass-menu .szh-menu__item--hover {
|
.glass-menu .szh-menu__item--hover {
|
||||||
background-color: var(--button-bg-blur-color);
|
background-color: var(--button-bg-blur-color);
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DONUT METER */
|
/* DONUT METER */
|
||||||
|
@ -1159,20 +1157,26 @@ meter.donut:is(.danger, .explode):after {
|
||||||
color: var(--red-color);
|
color: var(--red-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TOAST */
|
/* SHINY PILL */
|
||||||
|
|
||||||
:root .toastify {
|
.shiny-pill {
|
||||||
|
color: var(--button-text-color);
|
||||||
|
text-shadow: 0 -1px var(--drop-shadow-color);
|
||||||
background-color: var(--button-bg-color);
|
background-color: var(--button-bg-color);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
160deg,
|
160deg,
|
||||||
rgba(255, 255, 255, 0.5),
|
rgba(255, 255, 255, 0.5),
|
||||||
rgba(255, 255, 255, 0) 50%
|
rgba(255, 255, 255, 0) 50%
|
||||||
);
|
);
|
||||||
color: var(--button-text-color);
|
|
||||||
border-radius: 10em;
|
|
||||||
padding: 8px 16px;
|
|
||||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
||||||
0 10px 36px -4px var(--button-bg-blur-color);
|
0 10px 36px -4px var(--button-bg-blur-color),
|
||||||
|
inset var(--hairline-width) var(--hairline-width) rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TOAST */
|
||||||
|
|
||||||
|
:root .toastify {
|
||||||
|
padding: 8px 16px;
|
||||||
}
|
}
|
||||||
.toastify-bottom {
|
.toastify-bottom {
|
||||||
margin-bottom: env(safe-area-inset-bottom);
|
margin-bottom: env(safe-area-inset-bottom);
|
||||||
|
@ -1487,6 +1491,9 @@ ul.link-list li a .icon {
|
||||||
transition: transform 0.4s var(--timing-function);
|
transition: transform 0.4s var(--timing-function);
|
||||||
--back-transition: transform 0.4s ease-out;
|
--back-transition: transform 0.4s ease-out;
|
||||||
}
|
}
|
||||||
|
.timeline:not(.flat) > li:not(:has(.status-carousel)) {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
.timeline:not(.flat) > li:has(.status-link.is-active) {
|
.timeline:not(.flat) > li:has(.status-link.is-active) {
|
||||||
transition: var(--back-transition);
|
transition: var(--back-transition);
|
||||||
transform: translate3d(-2.5vw, 0, 0);
|
transform: translate3d(-2.5vw, 0, 0);
|
||||||
|
|
|
@ -318,13 +318,14 @@ function App() {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
onClose={(results) => {
|
onClose={(results) => {
|
||||||
const { newStatus } = results || {};
|
const { newStatus, instance } = results || {};
|
||||||
states.showCompose = false;
|
states.showCompose = false;
|
||||||
window.__COMPOSE__ = null;
|
window.__COMPOSE__ = null;
|
||||||
if (newStatus) {
|
if (newStatus) {
|
||||||
states.reloadStatusPage++;
|
states.reloadStatusPage++;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const toast = Toastify({
|
const toast = Toastify({
|
||||||
|
className: 'shiny-pill',
|
||||||
text: 'Status posted. Check it out.',
|
text: 'Status posted. Check it out.',
|
||||||
duration: 10_000, // 10 seconds
|
duration: 10_000, // 10 seconds
|
||||||
gravity: 'bottom',
|
gravity: 'bottom',
|
||||||
|
@ -333,7 +334,11 @@ function App() {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
toast.hideToast();
|
toast.hideToast();
|
||||||
states.prevLocation = location;
|
states.prevLocation = location;
|
||||||
navigate(`/s/${newStatus.id}`);
|
navigate(
|
||||||
|
instance
|
||||||
|
? `/${instance}/s/${newStatus.id}`
|
||||||
|
: `/s/${newStatus.id}`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.showToast();
|
toast.showToast();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import './account.css';
|
import './account.css';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
|
@ -82,8 +83,11 @@ function Account({ account, instance: propInstance, onClose }) {
|
||||||
username,
|
username,
|
||||||
} = info || {};
|
} = info || {};
|
||||||
|
|
||||||
|
const escRef = useHotkeys('esc', onClose, [onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={escRef}
|
||||||
id="account-container"
|
id="account-container"
|
||||||
class={`sheet ${uiState === 'loading' ? 'skeleton' : ''}`}
|
class={`sheet ${uiState === 'loading' ? 'skeleton' : ''}`}
|
||||||
>
|
>
|
||||||
|
|
|
@ -113,7 +113,8 @@ function Compose({
|
||||||
const currentAccount = getCurrentAccount();
|
const currentAccount = getCurrentAccount();
|
||||||
const currentAccountInfo = currentAccount.info;
|
const currentAccountInfo = currentAccount.info;
|
||||||
|
|
||||||
const { configuration } = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
|
const { configuration } = instance;
|
||||||
console.log('⚙️ Configuration', configuration);
|
console.log('⚙️ Configuration', configuration);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -141,20 +142,6 @@ function Compose({
|
||||||
|
|
||||||
const prefs = store.account.get('preferences') || {};
|
const prefs = store.account.get('preferences') || {};
|
||||||
|
|
||||||
const customEmojis = useRef();
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const emojis = await masto.v1.customEmojis.list();
|
|
||||||
console.log({ emojis });
|
|
||||||
customEmojis.current = emojis;
|
|
||||||
} catch (e) {
|
|
||||||
// silent fail
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const oninputTextarea = () => {
|
const oninputTextarea = () => {
|
||||||
if (!textareaRef.current) return;
|
if (!textareaRef.current) return;
|
||||||
textareaRef.current.dispatchEvent(new Event('input'));
|
textareaRef.current.dispatchEvent(new Event('input'));
|
||||||
|
@ -799,6 +786,7 @@ function Compose({
|
||||||
// Close
|
// Close
|
||||||
onClose({
|
onClose({
|
||||||
newStatus,
|
newStatus,
|
||||||
|
instance,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -1057,6 +1045,20 @@ const Textarea = forwardRef((props, ref) => {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const charCount = snapStates.composerCharacterCount;
|
const charCount = snapStates.composerCharacterCount;
|
||||||
|
|
||||||
|
const customEmojis = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const emojis = await masto.v1.customEmojis.list();
|
||||||
|
console.log({ emojis });
|
||||||
|
customEmojis.current = emojis;
|
||||||
|
} catch (e) {
|
||||||
|
// silent fail
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const textExpanderRef = useRef();
|
const textExpanderRef = useRef();
|
||||||
const textExpanderTextRef = useRef('');
|
const textExpanderTextRef = useRef('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ audio = Audio track
|
||||||
function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
|
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
|
||||||
media;
|
media;
|
||||||
const { original, small, focus } = meta || {};
|
const { original = {}, small, focus } = meta || {};
|
||||||
|
|
||||||
const width = showOriginal ? original?.width : small?.width;
|
const width = showOriginal ? original?.width : small?.width;
|
||||||
const height = showOriginal ? original?.height : small?.height;
|
const height = showOriginal ? original?.height : small?.height;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './shortcuts-settings.css';
|
import './shortcuts-settings.css';
|
||||||
|
|
||||||
|
import mem from 'mem';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -90,10 +91,15 @@ export const SHORTCUTS_META = {
|
||||||
icon: 'notification',
|
icon: 'notification',
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
title: async ({ id }) => {
|
title: mem(
|
||||||
const list = await api().masto.v1.lists.fetch(id);
|
async ({ id }) => {
|
||||||
return list.title;
|
const list = await api().masto.v1.lists.fetch(id);
|
||||||
},
|
return list.title;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cacheKey: ([{ id }]) => id,
|
||||||
|
},
|
||||||
|
),
|
||||||
path: ({ id }) => `/l/${id}`,
|
path: ({ id }) => `/l/${id}`,
|
||||||
icon: 'list',
|
icon: 'list',
|
||||||
},
|
},
|
||||||
|
@ -109,10 +115,15 @@ export const SHORTCUTS_META = {
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
},
|
},
|
||||||
'account-statuses': {
|
'account-statuses': {
|
||||||
title: async ({ id }) => {
|
title: mem(
|
||||||
const account = await api().masto.v1.accounts.fetch(id);
|
async ({ id }) => {
|
||||||
return account.username || account.acct || account.displayName;
|
const account = await api().masto.v1.accounts.fetch(id);
|
||||||
},
|
return account.username || account.acct || account.displayName;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cacheKey: ([{ id }]) => id,
|
||||||
|
},
|
||||||
|
),
|
||||||
path: ({ id }) => `/a/${id}`,
|
path: ({ id }) => `/a/${id}`,
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import './shortcuts.css';
|
import './shortcuts.css';
|
||||||
|
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||||
import { useRef } from 'preact/hooks';
|
import { useMemo, useRef } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -23,29 +23,33 @@ function Shortcuts() {
|
||||||
|
|
||||||
const menuRef = useRef();
|
const menuRef = useRef();
|
||||||
|
|
||||||
const formattedShortcuts = shortcuts
|
const formattedShortcuts = useMemo(
|
||||||
.map((pin, i) => {
|
() =>
|
||||||
const { type, ...data } = pin;
|
shortcuts
|
||||||
if (!SHORTCUTS_META[type]) return null;
|
.map((pin, i) => {
|
||||||
let { path, title, icon } = SHORTCUTS_META[type];
|
const { type, ...data } = pin;
|
||||||
|
if (!SHORTCUTS_META[type]) return null;
|
||||||
|
let { path, title, icon } = SHORTCUTS_META[type];
|
||||||
|
|
||||||
if (typeof path === 'function') {
|
if (typeof path === 'function') {
|
||||||
path = path(data, i);
|
path = path(data, i);
|
||||||
}
|
}
|
||||||
if (typeof title === 'function') {
|
if (typeof title === 'function') {
|
||||||
title = title(data);
|
title = title(data);
|
||||||
}
|
}
|
||||||
if (typeof icon === 'function') {
|
if (typeof icon === 'function') {
|
||||||
icon = icon(data);
|
icon = icon(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path,
|
path,
|
||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean),
|
||||||
|
[shortcuts],
|
||||||
|
);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => {
|
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => {
|
||||||
|
|
|
@ -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,57 @@ export function formatDuration(time) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMastodonLinkMaybe(url) {
|
||||||
|
return /^https:\/\/.*\/\d+$/i.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const denylistDomains = /(twitter|github)\.com/i;
|
||||||
|
const failedUnfurls = {};
|
||||||
|
|
||||||
|
function _unfurlMastodonLink(instance, url) {
|
||||||
|
if (denylistDomains.test(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failedUnfurls[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 {
|
||||||
|
failedUnfurls[url] = true;
|
||||||
|
throw new Error('No results');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
failedUnfurls[url] = true;
|
||||||
|
console.warn(e);
|
||||||
|
// Silently fail
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
||||||
|
|
||||||
export default memo(Status);
|
export default memo(Status);
|
||||||
|
|
|
@ -275,7 +275,7 @@ function Timeline({
|
||||||
!hiddenUI &&
|
!hiddenUI &&
|
||||||
showNew && (
|
showNew && (
|
||||||
<button
|
<button
|
||||||
class="updates-button"
|
class="updates-button shiny-pill"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loadItems(true);
|
loadItems(true);
|
||||||
|
|
|
@ -85,8 +85,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, '.SFNSText-Regular',
|
font-family: ui-rounded, system-ui;
|
||||||
sans-serif;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
@ -267,8 +266,8 @@ pre {
|
||||||
pre code,
|
pre code,
|
||||||
code {
|
code {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
|
font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono',
|
||||||
monospace;
|
Menlo, Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
|
@ -165,7 +165,7 @@ function Notifications() {
|
||||||
</div>
|
</div>
|
||||||
{snapStates.notificationsShowNew && uiState !== 'loading' && (
|
{snapStates.notificationsShowNew && uiState !== 'loading' && (
|
||||||
<button
|
<button
|
||||||
class="updates-button"
|
class="updates-button shiny-pill"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loadNotifications(true);
|
loadNotifications(true);
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import mem from 'mem';
|
||||||
import { proxy, subscribe } from 'valtio';
|
import { proxy, subscribe } from 'valtio';
|
||||||
import { subscribeKey } from 'valtio/utils';
|
import { subscribeKey } from 'valtio/utils';
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ const states = proxy({
|
||||||
reloadStatusPage: 0,
|
reloadStatusPage: 0,
|
||||||
spoilers: {},
|
spoilers: {},
|
||||||
scrollPositions: {},
|
scrollPositions: {},
|
||||||
|
unfurledLinks: {},
|
||||||
// Modals
|
// Modals
|
||||||
showCompose: false,
|
showCompose: false,
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
|
@ -129,7 +131,8 @@ export function threadifyStatus(status, propInstance) {
|
||||||
if (!prevStatus) {
|
if (!prevStatus) {
|
||||||
if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
|
if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
|
||||||
await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
|
await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
|
||||||
prevStatus = await masto.v1.statuses.fetch(inReplyToId);
|
// prevStatus = await masto.v1.statuses.fetch(inReplyToId);
|
||||||
|
prevStatus = await fetchStatus(inReplyToId, masto);
|
||||||
saveStatus(prevStatus, instance, { skipThreading: true });
|
saveStatus(prevStatus, instance, { skipThreading: true });
|
||||||
}
|
}
|
||||||
// Prepend so that first status in thread will be index 0
|
// Prepend so that first status in thread will be index 0
|
||||||
|
@ -149,3 +152,7 @@ export function threadifyStatus(status, propInstance) {
|
||||||
console.error(e, status);
|
console.error(e, status);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchStatus = mem((statusID, masto) => {
|
||||||
|
return masto.v1.statuses.fetch(statusID);
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue