diff --git a/README.md b/README.md index e24016ee..925bdafb 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Prerequisites: Node.js 18+ - [Vite](https://vitejs.dev/) - Build tool - [Preact](https://preactjs.com/) - UI library - [Valtio](https://valtio.pmnd.rs/) - State management +- [React Router](https://reactrouter.com/) - Routing - [masto.js](https://github.com/neet/masto.js/) - Mastodon API client - [Iconify](https://iconify.design/) - Icon library - Vanilla CSS - *Yes, I'm old school.* diff --git a/package-lock.json b/package-lock.json index 7606458b..bae57c1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,15 +14,14 @@ "dayjs-twitter": "~0.5.0", "fast-blurhash": "~1.1.2", "fast-deep-equal": "~3.1.3", - "history": "~5.3.0", "idb-keyval": "~6.2.0", "just-debounce-it": "~3.2.0", "masto": "~5.5.0", "mem": "~9.0.2", "preact": "~10.11.3", - "preact-router": "~4.1.0", "react-hotkeys-hook": "~4.3.2", "react-intersection-observer": "~9.4.1", + "react-router-dom": "~6.7.0", "string-length": "~5.0.1", "swiped-events": "~1.1.7", "toastify-js": "~1.12.0", @@ -1653,6 +1652,7 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -2276,6 +2276,14 @@ "vite": ">=2.0.0-beta.3" } }, + "node_modules/@remix-run/router": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.0.tgz", + "integrity": "sha512-nwQoYb3m4DDpHTeOwpJEuDt8lWVcujhYYSFGLluC+9es2PyLjm+jjq3IeRBQbwBtPLJE/lkuHuGHr8uQLgmJRA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/plugin-replace": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz", @@ -3607,14 +3615,6 @@ "tslib": "^2.0.3" } }, - "node_modules/history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "dependencies": { - "@babel/runtime": "^7.7.6" - } - }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -4537,14 +4537,6 @@ "url": "https://opencollective.com/preact" } }, - "node_modules/preact-router": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/preact-router/-/preact-router-4.1.0.tgz", - "integrity": "sha512-y1w2YvVpKAju9FMV+fAVR1NpH4MW5q07BZrziMZeg6F/rGJ9KvLUZtjOqsy2I8fDYiX36AM1AQTXIIK3jigBhA==", - "peerDependencies": { - "preact": ">=10" - } - }, "node_modules/prettier": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz", @@ -4672,6 +4664,36 @@ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-router": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.7.0.tgz", + "integrity": "sha512-KNWlG622ddq29MAM159uUsNMdbX8USruoKnwMMQcs/QWZgFUayICSn2oB7reHce1zPj6CG18kfkZIunSSRyGHg==", + "dependencies": { + "@remix-run/router": "1.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.7.0.tgz", + "integrity": "sha512-jQtXUJyhso3kFw430+0SPCbmCmY1/kJv8iRffGHwHy3CkoomGxeYzMkmeSPYo6Egzh3FKJZRAL22yg5p2tXtfg==", + "dependencies": { + "@remix-run/router": "1.3.0", + "react-router": "6.7.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -4693,7 +4715,8 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true }, "node_modules/regenerator-transform": { "version": "0.15.1", @@ -7013,6 +7036,7 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "dev": true, "requires": { "regenerator-runtime": "^0.13.11" } @@ -7394,6 +7418,11 @@ "@rollup/pluginutils": "^4.1.0" } }, + "@remix-run/router": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.0.tgz", + "integrity": "sha512-nwQoYb3m4DDpHTeOwpJEuDt8lWVcujhYYSFGLluC+9es2PyLjm+jjq3IeRBQbwBtPLJE/lkuHuGHr8uQLgmJRA==" + }, "@rollup/plugin-replace": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz", @@ -8413,14 +8442,6 @@ "tslib": "^2.0.3" } }, - "history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "requires": { - "@babel/runtime": "^7.7.6" - } - }, "idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -9097,12 +9118,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==" }, - "preact-router": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/preact-router/-/preact-router-4.1.0.tgz", - "integrity": "sha512-y1w2YvVpKAju9FMV+fAVR1NpH4MW5q07BZrziMZeg6F/rGJ9KvLUZtjOqsy2I8fDYiX36AM1AQTXIIK3jigBhA==", - "requires": {} - }, "prettier": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz", @@ -9181,6 +9196,23 @@ "integrity": "sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==", "requires": {} }, + "react-router": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.7.0.tgz", + "integrity": "sha512-KNWlG622ddq29MAM159uUsNMdbX8USruoKnwMMQcs/QWZgFUayICSn2oB7reHce1zPj6CG18kfkZIunSSRyGHg==", + "requires": { + "@remix-run/router": "1.3.0" + } + }, + "react-router-dom": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.7.0.tgz", + "integrity": "sha512-jQtXUJyhso3kFw430+0SPCbmCmY1/kJv8iRffGHwHy3CkoomGxeYzMkmeSPYo6Egzh3FKJZRAL22yg5p2tXtfg==", + "requires": { + "@remix-run/router": "1.3.0", + "react-router": "6.7.0" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -9199,7 +9231,8 @@ "regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true }, "regenerator-transform": { "version": "0.15.1", diff --git a/package.json b/package.json index 6a45033c..0444dada 100644 --- a/package.json +++ b/package.json @@ -16,15 +16,14 @@ "dayjs-twitter": "~0.5.0", "fast-blurhash": "~1.1.2", "fast-deep-equal": "~3.1.3", - "history": "~5.3.0", "idb-keyval": "~6.2.0", "just-debounce-it": "~3.2.0", "masto": "~5.5.0", "mem": "~9.0.2", "preact": "~10.11.3", - "preact-router": "~4.1.0", "react-hotkeys-hook": "~4.3.2", "react-intersection-observer": "~9.4.1", + "react-router-dom": "~6.7.0", "string-length": "~5.0.1", "swiped-events": "~1.1.7", "toastify-js": "~1.12.0", diff --git a/src/app.css b/src/app.css index b7eb202b..690905a1 100644 --- a/src/app.css +++ b/src/app.css @@ -46,6 +46,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { transition: opacity 0.1s ease-in-out; overscroll-behavior: contain; scroll-behavior: smooth; + background-color: var(--bg-color); } .deck-container[hidden] { display: block; @@ -61,6 +62,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { scroll-padding-top: 3em; } +.deck-container { + transition: transform 0.4s var(--timing-function); +} +.deck-container:has(~ .deck-backdrop) { + transition: transform 0.4s ease-out; + transform: translate3d(-5vw, 0, 0); +} + .deck { min-height: 100vh; min-height: 100dvh; @@ -364,7 +373,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { -webkit-tap-highlight-color: transparent; animation: appear 0.2s ease-out; } -.status-link:is(:hover, :focus) { +.status-link:is(:hover, :focus, .is-active) { background-color: var(--link-bg-hover-color); outline-offset: -2px; } @@ -508,11 +517,6 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { max-width: 40em; } -.decks { - flex-grow: 1; - width: 100%; -} - .deck-close { color: var(--text-insignificant-color) !important; } @@ -944,21 +948,63 @@ meter.donut:is(.danger, .explode):after { gap: 4px; } +.deck-container { + width: 100%; + flex-grow: 1; +} +#home-page ~ .deck-container { + z-index: 10; + position: fixed; + inset: 0; +} +#home-page:has(~ .deck-container) { + display: block; + position: absolute; + user-select: none; + pointer-events: none; + opacity: 0; + content-visibility: hidden; +} + +/* TAB BAR */ + +#tab-bar:not([hidden]) { + position: fixed; + bottom: 16px; + bottom: max(16px, env(safe-area-inset-bottom)); + width: calc(100% - 32px); + max-width: calc(40em - 32px); + z-index: 100; + display: flex; + background-color: var(--bg-blur-color); + backdrop-filter: blur(16px) saturate(3); + border: var(--hairline-width) solid var(--outline-color); + border-radius: 16px; + box-shadow: 0 8px 32px var(--outline-color); +} +#tab-bar li { + flex-grow: 1; + margin: 0; + padding: 0; + list-style: none; +} +#tab-bar li a { + text-align: center; + padding: 16px 0; + display: block; +} + @media (min-width: 40em) { html, body { background-color: var(--bg-faded-color); } + .deck-container { + background-color: var(--bg-faded-color); + } #app { display: flex; } - .decks { - transition: transform 0.4s var(--timing-function); - } - .decks:has(~ .deck-backdrop) { - transition: transform 0.4s ease-out; - transform: translate3d(-5vw, 0, 0); - } .deck-backdrop .deck { width: 50%; min-width: 40em; @@ -995,6 +1041,22 @@ meter.donut:is(.danger, .explode):after { border-radius: 16px; overflow: hidden; box-shadow: 0px 1px var(--bg-blur-color); + transition: transform 0.4s var(--timing-function); + --back-transition: transform 0.4s ease-out; + } + .timeline-deck .timeline:not(.flat) > li:has(.status-link.is-active) { + transition: var(--back-transition); + transform: translate3d(-2.5vw, 0, 0); + } + .timeline-deck + .timeline:not(.flat) + > li:not(:has(.boost-carousel)):has(+ li .status-link.is-active), + .timeline-deck + .timeline:not(.flat) + > li:not(:has(.boost-carousel)):has(.status-link.is-active) + + li { + transition: var(--back-transition); + transform: translate3d(-1.25vw, 0, 0); } .box { padding: 32px; diff --git a/src/app.jsx b/src/app.jsx index bd4f553c..340a1a45 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -1,19 +1,21 @@ import './app.css'; import 'toastify-js/src/toastify.css'; -import { createHashHistory } from 'history'; import debounce from 'just-debounce-it'; import { login } from 'masto'; -import Router, { route } from 'preact-router'; -import { useEffect, useLayoutEffect, useState } from 'preact/hooks'; +import { useEffect, useLayoutEffect, useMemo, useState } from 'preact/hooks'; +import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import Toastify from 'toastify-js'; import { useSnapshot } from 'valtio'; import Account from './components/account'; import Compose from './components/compose'; import Drafts from './components/drafts'; +import Icon from './components/icon'; +import Link from './components/link'; import Loader from './components/loader'; import Modal from './components/modal'; +import Bookmarks from './pages/bookmarks'; import Home from './pages/home'; import Login from './pages/login'; import Notifications from './pages/notifications'; @@ -24,14 +26,13 @@ import { getAccessToken } from './utils/auth'; import states, { saveStatus } from './utils/states'; import store from './utils/store'; -const { VITE_CLIENT_NAME: CLIENT_NAME } = import.meta.env; - window.__STATES__ = states; function App() { const snapStates = useSnapshot(states); const [isLoggedIn, setIsLoggedIn] = useState(false); const [uiState, setUIState] = useState('loading'); + const navigate = useNavigate(); useLayoutEffect(() => { const theme = store.local.get('theme'); @@ -126,20 +127,22 @@ function App() { } }, []); - const [currentDeck, setCurrentDeck] = useState('home'); - const [currentModal, setCurrentModal] = useState(null); + let location = useLocation(); + const locationDeckMap = { + '/': 'home-page', + '/notifications': 'notifications-page', + }; const focusDeck = () => { - if (currentModal) return; let timer = setTimeout(() => { - const page = document.getElementById(`${currentDeck}-page`); - console.debug('FOCUS', currentDeck, page); + const page = document.getElementById(locationDeckMap[location.pathname]); + console.debug('FOCUS', location.pathname, page); if (page) { page.focus(); } }, 100); return () => clearTimeout(timer); }; - useEffect(focusDeck, [currentDeck, currentModal]); + useEffect(focusDeck, [location]); useEffect(() => { if ( !snapStates.showCompose && @@ -173,44 +176,66 @@ function App() { } }, [isLoggedIn]); + const backgroundLocation = useMemo(() => { + const { prevLocation } = snapStates; + + console.debug({ location, prevLocation }); + const { pathname } = location; + const { pathname: prevPathname } = prevLocation || {}; + console.debug({ prevPathname, pathname }); + const isModalPage = /^\/s\//i.test(pathname); + return isModalPage ? prevLocation : null; + }, [location]); + + const nonRootLocation = useMemo(() => { + const { pathname } = location; + return !/\/(login|welcome)$/.test(pathname); + }, [location]); + return ( <> - {isLoggedIn && currentDeck && ( -
No bookmarks yet. Go bookmark something!
+ ) + )} + {uiState === 'loading' ? ( +
+ Unable to load bookmarks.
+
+
+
+
The end.
+ )} +
+ Unable to load statuses
+
+
+
+
- Unable to load statuses
-
-
-
-
- Go home + Go home
diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index 738f6971..b90d6d45 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -1,17 +1,17 @@ import './notifications.css'; -import { Link } from 'preact-router/match'; import { memo } from 'preact/compat'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useSnapshot } from 'valtio'; import Avatar from '../components/avatar'; import Icon from '../components/icon'; +import Link from '../components/link'; import Loader from '../components/loader'; import NameText from '../components/name-text'; import RelativeTime from '../components/relative-time'; import Status from '../components/status'; -import states from '../utils/states'; +import states, { saveStatus } from '../utils/states'; import store from '../utils/store'; import useTitle from '../utils/useTitle'; @@ -156,7 +156,7 @@ function Notification({ notification }) { {status && (- + Log in - +