phanpy/src/app.jsx

505 lines
15 KiB
React
Raw Normal View History

2023-02-11 00:37:42 +00:00
import './app.css';
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
2023-02-09 14:27:49 +00:00
import {
matchPath,
Route,
Routes,
useLocation,
useNavigate,
useParams,
2023-02-09 14:27:49 +00:00
} from 'react-router-dom';
2022-12-10 09:14:48 +00:00
import { useSnapshot } from 'valtio';
import AccountSheet from './components/account-sheet';
2022-12-10 09:14:48 +00:00
import Compose from './components/compose';
import Drafts from './components/drafts';
2022-12-10 09:14:48 +00:00
import Loader from './components/loader';
import MediaModal from './components/media-modal';
2022-12-10 09:14:48 +00:00
import Modal from './components/modal';
2023-02-16 09:51:54 +00:00
import Shortcuts from './components/shortcuts';
import ShortcutsSettings from './components/shortcuts-settings';
import NotFound from './pages/404';
import AccountStatuses from './pages/account-statuses';
import Accounts from './pages/accounts';
import Bookmarks from './pages/bookmarks';
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-04-17 11:00:41 +00:00
import HttpRoute from './pages/HttpRoute';
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';
2022-12-10 09:14:48 +00:00
import Settings from './pages/settings';
import Status from './pages/status';
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 showToast from './utils/show-toast';
import states, { getStatus, saveStatus } from './utils/states';
2022-12-10 09:14:48 +00:00
import store from './utils/store';
import { getCurrentAccount } from './utils/store-utils';
2023-02-28 07:27:42 +00:00
import useInterval from './utils/useInterval';
2023-02-12 09:38:50 +00:00
import usePageVisibility from './utils/usePageVisibility';
2022-12-10 09:14:48 +00:00
window.__STATES__ = states;
2022-12-10 09:14:48 +00:00
2022-12-31 17:46:08 +00:00
function App() {
2022-12-10 09:14:48 +00:00
const snapStates = useSnapshot(states);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [uiState, setUIState] = useState('loading');
const navigate = useNavigate();
2022-12-10 09:14:48 +00:00
useLayoutEffect(() => {
const theme = store.local.get('theme');
if (theme) {
document.documentElement.classList.add(`is-${theme}`);
document
.querySelector('meta[name="color-scheme"]')
.setAttribute('content', theme === 'auto' ? 'dark light' : theme);
2022-12-10 09:14:48 +00:00
}
2023-03-08 09:17:23 +00:00
const textSize = store.local.get('textSize');
if (textSize) {
document.documentElement.style.setProperty(
'--text-size',
`${textSize}px`,
);
}
2022-12-10 09:14:48 +00:00
}, []);
useEffect(() => {
const instanceURL = store.local.get('instanceURL');
const code = (window.location.search.match(/code=([^&]+)/) || [])[1];
if (code) {
console.log({ code });
// Clear the code from the URL
window.history.replaceState({}, document.title, '/');
const clientID = store.session.get('clientID');
const clientSecret = store.session.get('clientSecret');
(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 masto = initClient({ instance: instanceURL, accessToken });
await Promise.allSettled([
initInstance(masto),
initAccount(masto, instanceURL, accessToken),
]);
2023-02-09 15:59:57 +00:00
initPreferences(masto);
2022-12-10 09:14:48 +00:00
setIsLoggedIn(true);
setUIState('default');
})();
} else {
const account = getCurrentAccount();
if (account) {
store.session.set('currentAccount', account.info.id);
const { masto } = api({ account });
2023-02-12 09:38:50 +00:00
console.log('masto', masto);
2023-02-09 15:59:57 +00:00
initPreferences(masto);
setUIState('loading');
2023-02-07 16:31:46 +00:00
(async () => {
try {
await initInstance(masto);
} 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();
states.currentLocation = location.pathname;
2022-12-30 12:37:57 +00:00
const focusDeck = () => {
let timer = setTimeout(() => {
2023-03-01 07:47:19 +00:00
const columns = document.getElementById('columns');
if (columns) {
// Focus first column
// columns.querySelector('.deck-container')?.focus?.();
2023-03-01 07:47:19 +00:00
} else {
const backDrop = document.querySelector('.deck-backdrop');
if (backDrop) return;
2023-03-01 07:47:19 +00:00
// Focus last deck
const pages = document.querySelectorAll('.deck-container');
const page = pages[pages.length - 1]; // last one
if (page && page.tabIndex === -1) {
console.log('FOCUS', page);
page.focus();
}
2022-12-30 12:37:57 +00:00
}
}, 100);
return () => clearTimeout(timer);
};
useEffect(focusDeck, [location]);
2023-02-16 09:51:54 +00:00
const showModal =
snapStates.showCompose ||
snapStates.showSettings ||
snapStates.showAccounts ||
2023-02-16 09:51:54 +00:00
snapStates.showAccount ||
snapStates.showDrafts ||
snapStates.showMediaModal ||
snapStates.showShortcutsSettings;
2022-12-30 12:37:57 +00:00
useEffect(() => {
2023-02-16 09:51:54 +00:00
if (!showModal) focusDeck();
2023-02-13 02:43:12 +00:00
}, [showModal]);
2022-12-10 09:14:48 +00:00
const { prevLocation } = snapStates;
const backgroundLocation = useRef(prevLocation || null);
const isModalPage =
matchPath('/:instance/s/:id', location.pathname) ||
matchPath('/s/:id', location.pathname);
if (isModalPage) {
if (!backgroundLocation.current) backgroundLocation.current = prevLocation;
} else {
backgroundLocation.current = null;
}
console.debug({
backgroundLocation: backgroundLocation.current,
location,
});
2023-04-17 10:56:09 +00:00
if (/\/https?:/.test(location.pathname)) {
return <HttpRoute />;
}
const nonRootLocation = useMemo(() => {
const { pathname } = location;
return !/^\/(login|welcome)/.test(pathname);
}, [location]);
2023-04-18 09:46:40 +00:00
// Change #app dataset based on snapStates.settings.shortcutsViewMode
useEffect(() => {
const $app = document.getElementById('app');
if ($app) {
$app.dataset.shortcutsViewMode = snapStates.settings.shortcutsViewMode;
}
}, [snapStates.settings.shortcutsViewMode]);
2022-12-10 09:14:48 +00:00
return (
<>
<Routes location={nonRootLocation || location}>
<Route
path="/"
element={
isLoggedIn ? (
<Home />
) : uiState === 'loading' ? (
<Loader />
) : (
<Welcome />
)
2022-12-10 09:14:48 +00:00
}
/>
<Route path="/login" element={<Login />} />
<Route path="/welcome" element={<Welcome />} />
</Routes>
<Routes location={backgroundLocation.current || location}>
{isLoggedIn && (
<Route path="/notifications" element={<Notifications />} />
)}
2023-04-06 11:32:26 +00:00
{isLoggedIn && <Route path="/mentions" element={<Mentions />} />}
{isLoggedIn && <Route path="/following" element={<Following />} />}
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
{isLoggedIn && <Route path="/f" element={<Favourites />} />}
2023-02-10 16:05:18 +00:00
{isLoggedIn && (
<Route path="/l">
<Route index element={<Lists />} />
<Route path=":id" element={<List />} />
</Route>
)}
2023-02-11 08:48:47 +00:00
{isLoggedIn && <Route path="/ft" element={<FollowedHashtags />} />}
2023-02-18 12:48:24 +00:00
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
2023-02-06 11:54:18 +00:00
<Route path="/:instance?/a/:id" element={<AccountStatuses />} />
2023-02-06 12:17:07 +00:00
<Route path="/:instance?/p">
<Route index element={<Public />} />
<Route path="l" element={<Public local />} />
</Route>
2023-04-05 17:14:38 +00:00
<Route path="/:instance?/trending" element={<Trending />} />
<Route path="/:instance?/search" element={<Search />} />
{/* <Route path="/:anything" element={<NotFound />} /> */}
</Routes>
{uiState === 'default' && (
<Routes>
<Route path="/:instance?/s/:id" element={<StatusRoute />} />
</Routes>
)}
<div>
{isLoggedIn &&
!snapStates.settings.shortcutsColumnsMode &&
snapStates.settings.shortcutsViewMode !== 'multi-column' && (
<Shortcuts />
)}
</div>
2022-12-10 09:14:48 +00:00
{!!snapStates.showCompose && (
<Modal>
<Compose
replyToStatus={
typeof snapStates.showCompose !== 'boolean'
? snapStates.showCompose.replyToStatus
: window.__COMPOSE__?.replyToStatus || null
}
editStatus={
states.showCompose?.editStatus ||
window.__COMPOSE__?.editStatus ||
null
}
draftStatus={
states.showCompose?.draftStatus ||
window.__COMPOSE__?.draftStatus ||
null
2022-12-10 09:14:48 +00:00
}
onClose={(results) => {
const { newStatus, instance } = results || {};
2022-12-10 09:14:48 +00:00
states.showCompose = false;
window.__COMPOSE__ = null;
if (newStatus) {
2022-12-10 09:14:48 +00:00
states.reloadStatusPage++;
showToast({
text: 'Status posted. Check it out.',
delay: 1000,
duration: 10_000, // 10 seconds
onClick: (toast) => {
toast.hideToast();
states.prevLocation = location;
navigate(
instance
? `/${instance}/s/${newStatus.id}`
: `/s/${newStatus.id}`,
);
},
});
2022-12-10 09:14:48 +00:00
}
}}
/>
</Modal>
)}
{!!snapStates.showSettings && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showSettings = false;
}
}}
>
<Settings
onClose={() => {
states.showSettings = false;
}}
/>
</Modal>
)}
{!!snapStates.showAccounts && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showAccounts = false;
}
}}
>
<Accounts
onClose={() => {
states.showAccounts = false;
}}
/>
</Modal>
)}
2022-12-10 09:14:48 +00:00
{!!snapStates.showAccount && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showAccount = false;
}
}}
>
<AccountSheet
account={snapStates.showAccount?.account || snapStates.showAccount}
instance={snapStates.showAccount?.instance}
onClose={({ destination }) => {
states.showAccount = false;
if (destination) {
states.showAccounts = false;
}
}}
/>
2022-12-10 09:14:48 +00:00
</Modal>
)}
{!!snapStates.showDrafts && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showDrafts = false;
}
}}
>
2023-04-20 08:10:57 +00:00
<Drafts onClose={() => (states.showDrafts = false)} />
</Modal>
)}
{!!snapStates.showMediaModal && (
<Modal
onClick={(e) => {
if (
e.target === e.currentTarget ||
e.target.classList.contains('media')
) {
states.showMediaModal = false;
}
}}
>
<MediaModal
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
instance={snapStates.showMediaModal.instance}
index={snapStates.showMediaModal.index}
statusID={snapStates.showMediaModal.statusID}
onClose={() => {
states.showMediaModal = false;
}}
/>
</Modal>
)}
2023-02-16 09:51:54 +00:00
{!!snapStates.showShortcutsSettings && (
<Modal
2023-04-08 14:16:58 +00:00
class="light"
2023-02-16 09:51:54 +00:00
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showShortcutsSettings = false;
}
}}
>
2023-04-20 08:10:57 +00:00
<ShortcutsSettings
onClose={() => (states.showShortcutsSettings = false)}
/>
2023-02-16 09:51:54 +00:00
</Modal>
)}
<BackgroundService isLoggedIn={isLoggedIn} />
2022-12-10 09:14:48 +00:00
</>
);
}
2022-12-31 17:46:08 +00:00
function BackgroundService({ isLoggedIn }) {
// Notifications service
// - WebSocket to receive notifications when page is visible
const [visible, setVisible] = useState(true);
usePageVisibility(setVisible);
const notificationStream = useRef();
useEffect(() => {
if (isLoggedIn && visible) {
const { masto, instance } = api();
(async () => {
// 1. Get the latest notification
if (states.notificationsLast) {
const notificationsIterator = masto.v1.notifications.list({
limit: 1,
since_id: states.notificationsLast.id,
});
const { value: notifications } = await notificationsIterator.next();
if (notifications?.length) {
states.notificationsShowNew = true;
}
}
// 2. Start streaming
notificationStream.current = await masto.ws.stream(
'/api/v1/streaming',
{
stream: 'user:notification',
},
);
console.log('🎏 Streaming notification', notificationStream.current);
notificationStream.current.on('notification', (notification) => {
console.log('🔔🔔 Notification', notification);
if (notification.status) {
saveStatus(notification.status, instance, {
skipThreading: true,
});
}
states.notificationsShowNew = true;
});
notificationStream.current.ws.onclose = () => {
console.log('🔔🔔 Notification stream closed');
};
})();
}
return () => {
if (notificationStream.current) {
notificationStream.current.ws.close();
notificationStream.current = null;
}
};
}, [visible, isLoggedIn]);
// Check for updates service
const lastCheckDate = useRef();
const checkForUpdates = () => {
lastCheckDate.current = Date.now();
console.log('✨ Check app update');
fetch('./version.json')
.then((r) => r.json())
.then((info) => {
if (info) states.appVersion = info;
})
.catch((e) => {
console.error(e);
});
};
useInterval(() => checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes
usePageVisibility((visible) => {
if (visible) {
if (!lastCheckDate.current) {
checkForUpdates();
} else {
const diff = Date.now() - lastCheckDate.current;
if (diff > 1000 * 60 * 60) {
// 1 hour
checkForUpdates();
}
}
}
});
return null;
}
function StatusRoute() {
const params = useParams();
const { id, instance } = params;
return <Status id={id} instance={instance} />;
}
2022-12-31 17:46:08 +00:00
export { App };