phanpy/src/app.jsx

491 lines
14 KiB
React
Raw Permalink Normal View History

2023-02-11 00:37:42 +00:00
import './app.css';
import debounce from 'just-debounce-it';
import { lazy, Suspense } from 'preact/compat';
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
2023-04-25 12:41:08 +00:00
import 'swiped-events';
2023-10-31 12:21:49 +00:00
import { subscribe } from 'valtio';
2022-12-10 09:14:48 +00:00
2023-09-02 10:19:09 +00:00
import BackgroundService from './components/background-service';
2023-09-05 10:49:16 +00:00
import ComposeButton from './components/compose-button';
2024-01-20 02:25:47 +00:00
import { ICONS } from './components/ICONS';
2023-09-06 14:54:05 +00:00
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
2022-12-10 09:14:48 +00:00
import Loader from './components/loader';
// import Modals from './components/modals';
2023-09-02 10:19:09 +00:00
import NotificationService from './components/notification-service';
2023-09-04 06:49:39 +00:00
import SearchCommand from './components/search-command';
2023-02-16 09:51:54 +00:00
import Shortcuts from './components/shortcuts';
import NotFound from './pages/404';
import AccountStatuses from './pages/account-statuses';
import Bookmarks from './pages/bookmarks';
// import Catchup from './pages/catchup';
import Favourites from './pages/favourites';
2023-02-11 08:48:47 +00:00
import FollowedHashtags from './pages/followed-hashtags';
2023-02-03 13:08:08 +00:00
import Following from './pages/following';
2023-02-18 12:48:24 +00:00
import Hashtag from './pages/hashtag';
2022-12-10 09:14:48 +00:00
import Home from './pages/home';
2023-09-02 10:19:09 +00:00
import HttpRoute from './pages/http-route';
2023-02-10 16:05:18 +00:00
import List from './pages/list';
import Lists from './pages/lists';
2022-12-10 09:14:48 +00:00
import Login from './pages/login';
2023-04-06 11:32:26 +00:00
import Mentions from './pages/mentions';
2022-12-10 09:14:48 +00:00
import Notifications from './pages/notifications';
import Public from './pages/public';
import Search from './pages/search';
2023-09-02 10:19:09 +00:00
import StatusRoute from './pages/status-route';
2023-04-05 17:14:38 +00:00
import Trending from './pages/trending';
2022-12-10 09:14:48 +00:00
import Welcome from './pages/welcome';
2023-02-09 15:59:57 +00:00
import {
api,
initAccount,
initClient,
initInstance,
initPreferences,
} from './utils/api';
2022-12-10 09:14:48 +00:00
import { getAccessToken } from './utils/auth';
import focusDeck from './utils/focus-deck';
import states, { initStates, statusKey } from './utils/states';
2022-12-10 09:14:48 +00:00
import store from './utils/store';
2023-09-02 10:19:09 +00:00
import { getCurrentAccount } from './utils/store-utils';
import './utils/toast-alert';
2022-12-10 09:14:48 +00:00
const Catchup = lazy(() => import('./pages/catchup'));
const Modals = lazy(() => import('./components/modals'));
window.__STATES__ = states;
2023-10-28 03:07:35 +00:00
window.__STATES_STATS__ = () => {
const keys = [
'statuses',
'accounts',
'spoilers',
'unfurledLinks',
'statusQuotes',
];
const counts = {};
keys.forEach((key) => {
counts[key] = Object.keys(states[key]).length;
});
console.warn('STATE stats', counts);
2023-11-04 11:18:12 +00:00
const { statuses } = states;
const unmountedPosts = [];
for (const key in statuses) {
const $post = document.querySelector(
`[data-state-post-id~="${key}"], [data-state-post-ids~="${key}"]`,
);
2023-11-04 11:18:12 +00:00
if (!$post) {
unmountedPosts.push(key);
}
}
console.warn('Unmounted posts', unmountedPosts.length, unmountedPosts);
2023-10-28 03:07:35 +00:00
};
2022-12-10 09:14:48 +00:00
2023-11-04 11:18:12 +00:00
// Experimental "garbage collection" for states
// Every 15 minutes
2023-11-04 11:18:12 +00:00
// Only posts for now
setInterval(() => {
2023-11-05 00:26:51 +00:00
if (!window.__IDLE__) return;
2023-11-06 08:47:35 +00:00
const { statuses, unfurledLinks, notifications } = states;
2023-11-05 06:31:20 +00:00
let keysCount = 0;
2023-11-06 08:47:35 +00:00
const { instance } = api();
2023-11-04 11:18:12 +00:00
for (const key in statuses) {
if (!window.__IDLE__) break;
try {
const $post = document.querySelector(
`[data-state-post-id~="${key}"], [data-state-post-ids~="${key}"]`,
);
2023-11-06 08:47:35 +00:00
const postInNotifications = notifications.some(
(n) => key === statusKey(n.status?.id, instance),
);
if (!$post && !postInNotifications) {
delete states.statuses[key];
delete states.statusQuotes[key];
for (const link in unfurledLinks) {
const unfurled = unfurledLinks[link];
const sKey = statusKey(unfurled.id, unfurled.instance);
if (sKey === key) {
delete states.unfurledLinks[link];
break;
}
}
2023-11-05 06:31:20 +00:00
keysCount++;
}
} catch (e) {}
2023-11-04 11:18:12 +00:00
}
2023-11-05 06:31:20 +00:00
if (keysCount) {
console.info(`GC: Removed ${keysCount} keys`);
}
}, 15 * 60 * 1000);
2023-11-04 11:18:12 +00:00
2023-08-13 09:15:49 +00:00
// Preload icons
// There's probably a better way to do this
// Related: https://github.com/vitejs/vite/issues/10600
setTimeout(() => {
for (const icon in ICONS) {
2023-12-27 15:44:27 +00:00
queueMicrotask(() => {
if (Array.isArray(ICONS[icon])) {
ICONS[icon][0]?.();
} else {
ICONS[icon]?.();
}
});
2023-08-13 09:15:49 +00:00
}
}, 5000);
(() => {
2023-10-30 16:38:59 +00:00
window.__IDLE__ = true;
const nonIdleEvents = [
'mousemove',
'mousedown',
'resize',
'keydown',
'touchstart',
'pointerdown',
'pointermove',
'wheel',
];
2023-10-30 16:38:59 +00:00
const setIdle = () => {
window.__IDLE__ = true;
2023-10-30 16:38:59 +00:00
};
const IDLE_TIME = 3_000; // 3 seconds
const debouncedSetIdle = debounce(setIdle, IDLE_TIME);
const onNonIdle = () => {
window.__IDLE__ = false;
2023-10-30 16:38:59 +00:00
debouncedSetIdle();
};
nonIdleEvents.forEach((event) => {
window.addEventListener(event, onNonIdle, {
passive: true,
capture: true,
});
});
2023-10-30 16:38:59 +00:00
window.addEventListener('blur', setIdle, {
passive: true,
});
// When cursor leaves the window, set idle
document.documentElement.addEventListener(
'mouseleave',
(e) => {
if (!e.relatedTarget && !e.toElement) {
setIdle();
}
},
{
passive: true,
},
);
// document.addEventListener(
// 'visibilitychange',
// () => {
// if (document.visibilityState === 'visible') {
// onNonIdle();
// }
// },
// {
// passive: true,
// },
// );
})();
2023-12-01 16:07:13 +00:00
// Possible fix for iOS PWA theme-color bug
// It changes when loading web pages in "webview"
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
if (isIOS) {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
2023-12-23 15:04:34 +00:00
const theme = store.local.get('theme');
let $meta;
if (theme) {
// Get current meta
$meta = document.querySelector(
`meta[name="theme-color"][data-theme-setting="manual"]`,
);
if ($meta) {
const color = $meta.content;
const tempColor =
theme === 'light'
? $meta.dataset.themeLightColorTemp
: $meta.dataset.themeDarkColorTemp;
$meta.content = tempColor || '';
setTimeout(() => {
$meta.content = color;
}, 10);
}
} else {
// Get current color scheme
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light';
// Get current theme-color
$meta = document.querySelector(
`meta[name="theme-color"][media*="${colorScheme}"]`,
);
if ($meta) {
2023-12-31 00:02:32 +00:00
const color = $meta.dataset.content;
2023-12-23 15:04:34 +00:00
const tempColor = $meta.dataset.contentTemp;
$meta.content = tempColor || '';
setTimeout(() => {
$meta.content = color;
}, 10);
}
2023-12-01 16:07:13 +00:00
}
}
});
}
2023-12-23 15:04:34 +00:00
{
const theme = store.local.get('theme');
// If there's a theme, it's NOT auto
if (theme) {
// dark | light
document.documentElement.classList.add(`is-${theme}`);
document
.querySelector('meta[name="color-scheme"]')
.setAttribute('content', theme || 'dark light');
// Enable manual theme <meta>
const $manualMeta = document.querySelector(
'meta[data-theme-setting="manual"]',
);
if ($manualMeta) {
$manualMeta.name = 'theme-color';
$manualMeta.content =
theme === 'light'
? $manualMeta.dataset.themeLightColor
: $manualMeta.dataset.themeDarkColor;
}
// Disable auto theme <meta>s
const $autoMetas = document.querySelectorAll(
'meta[data-theme-setting="auto"]',
);
$autoMetas.forEach((m) => {
m.name = '';
});
}
const textSize = store.local.get('textSize');
if (textSize) {
document.documentElement.style.setProperty('--text-size', `${textSize}px`);
}
}
2023-10-21 09:40:03 +00:00
subscribe(states, (changes) => {
for (const [action, path, value, prevValue] of changes) {
// Change #app dataset based on settings.shortcutsViewMode
if (path.join('.') === 'settings.shortcutsViewMode') {
const $app = document.getElementById('app');
if ($app) {
$app.dataset.shortcutsViewMode = states.shortcuts?.length ? value : '';
}
}
// Add/Remove cloak class to body
if (path.join('.') === 'settings.cloakMode') {
const $body = document.body;
$body.classList.toggle('cloak', value);
}
}
});
2022-12-31 17:46:08 +00:00
function App() {
2022-12-10 09:14:48 +00:00
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [uiState, setUIState] = useState('loading');
2022-12-10 09:14:48 +00:00
useEffect(() => {
const instanceURL = store.local.get('instanceURL');
2023-05-20 02:08:41 +00:00
const code = decodeURIComponent(
(window.location.search.match(/code=([^&]+)/) || [, ''])[1],
);
2022-12-10 09:14:48 +00:00
if (code) {
console.log({ code });
// Clear the code from the URL
window.history.replaceState(
{},
document.title,
window.location.pathname || '/',
);
2022-12-10 09:14:48 +00:00
const clientID = store.session.get('clientID');
const clientSecret = store.session.get('clientSecret');
const vapidKey = store.session.get('vapidKey');
2022-12-10 09:14:48 +00:00
(async () => {
setUIState('loading');
const { access_token: accessToken } = await getAccessToken({
2022-12-10 09:14:48 +00:00
instanceURL,
client_id: clientID,
client_secret: clientSecret,
code,
});
const client = initClient({ instance: instanceURL, accessToken });
await Promise.allSettled([
initInstance(client, instanceURL),
initAccount(client, instanceURL, accessToken, vapidKey),
]);
initStates();
initPreferences(client);
2022-12-10 09:14:48 +00:00
setIsLoggedIn(true);
setUIState('default');
})();
} else {
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
const account = getCurrentAccount();
if (account) {
store.session.set('currentAccount', account.info.id);
const { client } = api({ account });
const { instance } = client;
// console.log('masto', masto);
initStates();
initPreferences(client);
setUIState('loading');
2023-02-07 16:31:46 +00:00
(async () => {
try {
await initInstance(client, instance);
} catch (e) {
} finally {
setIsLoggedIn(true);
setUIState('default');
}
2023-02-07 16:31:46 +00:00
})();
2023-02-10 04:29:07 +00:00
} else {
setUIState('default');
}
2022-12-10 09:14:48 +00:00
}
}, []);
let location = useLocation();
2023-10-22 11:25:22 +00:00
states.currentLocation = location.pathname;
// useLayoutEffect(() => {
// states.currentLocation = location.pathname;
// }, [location.pathname]);
useEffect(focusDeck, [location, isLoggedIn]);
2022-12-10 09:14:48 +00:00
2023-10-21 09:40:03 +00:00
if (/\/https?:/.test(location.pathname)) {
return <HttpRoute />;
}
return (
<>
<PrimaryRoutes isLoggedIn={isLoggedIn} loading={uiState === 'loading'} />
<SecondaryRoutes isLoggedIn={isLoggedIn} />
{uiState === 'default' && (
<Routes>
<Route path="/:instance?/s/:id" element={<StatusRoute />} />
</Routes>
)}
{isLoggedIn && <ComposeButton />}
{isLoggedIn && <Shortcuts />}
<Suspense>
<Modals />
</Suspense>
2023-10-21 09:40:03 +00:00
{isLoggedIn && <NotificationService />}
<BackgroundService isLoggedIn={isLoggedIn} />
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
<KeyboardShortcutsHelp />
</>
);
}
function PrimaryRoutes({ isLoggedIn, loading }) {
const location = useLocation();
const nonRootLocation = useMemo(() => {
const { pathname } = location;
2024-02-26 06:02:58 +00:00
return !/^\/(login|welcome)/i.test(pathname);
2023-10-21 09:40:03 +00:00
}, [location]);
return (
<Routes location={nonRootLocation || location}>
<Route
path="/"
element={
isLoggedIn ? (
<Home />
) : loading ? (
<Loader id="loader-root" />
) : (
<Welcome />
)
}
/>
<Route path="/login" element={<Login />} />
<Route path="/welcome" element={<Welcome />} />
</Routes>
);
}
2023-10-23 08:23:33 +00:00
function getPrevLocation() {
return states.prevLocation || null;
}
2023-10-21 09:40:03 +00:00
function SecondaryRoutes({ isLoggedIn }) {
2023-10-23 08:23:33 +00:00
// const snapStates = useSnapshot(states);
2023-10-21 09:40:03 +00:00
const location = useLocation();
2023-10-23 08:23:33 +00:00
// const prevLocation = snapStates.prevLocation;
const backgroundLocation = useRef(getPrevLocation());
2023-10-21 09:40:03 +00:00
2023-09-10 07:30:04 +00:00
const isModalPage = useMemo(() => {
return (
matchPath('/:instance/s/:id', location.pathname) ||
matchPath('/s/:id', location.pathname)
);
}, [location.pathname, matchPath]);
if (isModalPage) {
2023-10-23 08:23:33 +00:00
if (!backgroundLocation.current)
backgroundLocation.current = getPrevLocation();
} else {
backgroundLocation.current = null;
}
console.debug({
backgroundLocation: backgroundLocation.current,
location,
});
2022-12-10 09:14:48 +00:00
return (
2023-10-21 09:40:03 +00:00
<Routes location={backgroundLocation.current || location}>
{isLoggedIn && (
<>
<Route path="/notifications" element={<Notifications />} />
2023-10-21 09:40:03 +00:00
<Route path="/mentions" element={<Mentions />} />
<Route path="/following" element={<Following />} />
<Route path="/b" element={<Bookmarks />} />
<Route path="/f" element={<Favourites />} />
2023-02-10 16:05:18 +00:00
<Route path="/l">
<Route index element={<Lists />} />
<Route path=":id" element={<List />} />
</Route>
2023-10-21 09:40:03 +00:00
<Route path="/ft" element={<FollowedHashtags />} />
<Route
path="/catchup"
element={
<Suspense>
<Catchup />
</Suspense>
}
/>
2023-10-21 09:40:03 +00:00
</>
)}
2023-10-21 09:40:03 +00:00
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
<Route path="/:instance?/a/:id" element={<AccountStatuses />} />
<Route path="/:instance?/p">
<Route index element={<Public />} />
<Route path="l" element={<Public local />} />
</Route>
<Route path="/:instance?/trending" element={<Trending />} />
<Route path="/:instance?/search" element={<Search />} />
{/* <Route path="/:anything" element={<NotFound />} /> */}
</Routes>
2022-12-10 09:14:48 +00:00
);
}
2022-12-31 17:46:08 +00:00
export { App };