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 && ( -
- {/* Home will never be unmounted */} -
- )} - {!isLoggedIn && uiState === 'loading' && } - { - console.debug('ROUTER onChange', e); - // Special handling for Home and Notifications - const { url } = e; - if (/notifications/i.test(url)) { - setCurrentDeck('notifications'); - setCurrentModal(null); - } else if (url === '/') { - setCurrentDeck('home'); - document.title = `Home / ${CLIENT_NAME}`; - setCurrentModal(null); - } else if (/^\/s\//i.test(url)) { - setCurrentModal('status'); - } else { - setCurrentModal(null); - setCurrentDeck(null); + + + ) : uiState === 'loading' ? ( + + ) : ( + + ) } - states.history.push(url); - }} - > - {!isLoggedIn && uiState !== 'loading' && } - - {isLoggedIn && } - - + /> + } /> + } /> + + + {isLoggedIn && ( + } /> + )} + {isLoggedIn && } />} + + + {isLoggedIn && } />} + + {!!snapStates.showCompose && ( { toast.hideToast(); - route(`/s/${newStatus.id}`); + states.prevLocation = location; + navigate(`/s/${newStatus.id}`); }, }); toast.showToast(); diff --git a/src/components/link.jsx b/src/components/link.jsx new file mode 100644 index 00000000..30659f46 --- /dev/null +++ b/src/components/link.jsx @@ -0,0 +1,30 @@ +import { useLocation } from 'react-router-dom'; + +import states from '../utils/states'; + +/* NOTES + ===== + Initially this uses from react-router-dom, but it doesn't work: + 1. It interferes with nested inside and it's difficult to preventDefault/stopPropagation from the nested + 2. isActive doesn't work properly with the weird routes that's set up in this app, due to the faux "location" to make the modals work and prevent unmounting + 3. Not using because it modifies history.state that *persists* across page reloads. I don't need that, so using valtio's states instead. +*/ + +const Link = (props) => { + const routerLocation = useLocation(); + let hash = (location.hash || '').replace(/^#/, '').trim(); + if (hash === '') hash = '/'; + const isActive = hash === props.to; + return ( + { + states.prevLocation = routerLocation; + }} + /> + ); +}; + +export default Link; diff --git a/src/components/status.jsx b/src/components/status.jsx index d66369b4..ac07f0ef 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -29,6 +29,7 @@ import visibilityIconsMap from '../utils/visibility-icons-map'; import Avatar from './avatar'; import Icon from './icon'; +import Link from './link'; import RelativeTime from './relative-time'; function fetchAccount(id) { @@ -251,18 +252,14 @@ function Status({ {/* */}{' '} {size !== 'l' && (uri ? ( - + {' '} - + ) : ( , document.getElementById('app')); +render( + + + , + document.getElementById('app'), +); // Clean up iconify localStorage // TODO: Remove this after few weeks? diff --git a/src/pages/bookmarks.jsx b/src/pages/bookmarks.jsx new file mode 100644 index 00000000..a6ee0ad8 --- /dev/null +++ b/src/pages/bookmarks.jsx @@ -0,0 +1,144 @@ +import { useEffect, useRef, useState } from 'preact/hooks'; + +import Icon from '../components/Icon'; +import Link from '../components/link'; +import Loader from '../components/Loader'; +import Status from '../components/status'; +import { saveStatus } from '../utils/states'; +import useTitle from '../utils/useTitle'; + +const LIMIT = 40; + +function Bookmarks() { + useTitle('Bookmarks'); + const [bookmarks, setBookmarks] = useState([]); + const [uiState, setUIState] = useState('default'); + const [showMore, setShowMore] = useState(false); + + const bookmarksIterator = useRef(masto.v1.bookmarks.list({ limit: LIMIT })); + async function fetchBookmarks(firstLoad) { + console.log('fetchBookmarks', firstLoad); + if (firstLoad) { + bookmarksIterator.current = masto.v1.bookmarks.list({ limit: LIMIT }); + } + const allBookmarks = await bookmarksIterator.current.next(); + if (allBookmarks.value?.length) { + const bookmarksValue = allBookmarks.value.map((status) => { + saveStatus(status, { + skipThreading: true, + override: false, + }); + return status; + }); + if (firstLoad) { + setBookmarks(bookmarksValue); + } else { + setBookmarks([...bookmarks, ...bookmarksValue]); + } + } + return allBookmarks; + } + + const loadBookmarks = (firstLoad) => { + setUIState('loading'); + (async () => { + try { + console.log('loadBookmarks', firstLoad); + const { done } = await fetchBookmarks(firstLoad); + console.log('loadBookmarks', firstLoad); + setShowMore(!done); + setUIState('default'); + } catch (e) { + console.error(e); + setUIState('error'); + } + })(); + }; + + useEffect(() => { + loadBookmarks(true); + }, []); + + const scrollableRef = useRef(null); + + return ( +
+
+
{ + if (e.target === e.currentTarget) { + scrollableRef.current?.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } + }} + > +
+ + + +
+

Bookmarks

+
{' '} +
+ {!!bookmarks.length ? ( + <> +
    + {bookmarks.map((status) => ( +
  • + + + +
  • + ))} +
+ + {showMore && ( + + )} + + ) : ( + uiState !== 'loading' && ( +

No bookmarks yet. Go bookmark something!

+ ) + )} + {uiState === 'loading' ? ( +
+ +
+ ) : uiState === 'error' ? ( +

+ Unable to load bookmarks. +
+
+ +

+ ) : ( + bookmarks.length && + !showMore &&

The end.

+ )} +
+
+ ); +} + +export default Bookmarks; diff --git a/src/pages/home.jsx b/src/pages/home.jsx index e4c333e8..f1bfafbf 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -1,10 +1,10 @@ -import { Link } from 'preact-router/match'; import { memo } from 'preact/compat'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; import { useSnapshot } from 'valtio'; import Icon from '../components/icon'; +import Link from '../components/link'; import Loader from '../components/loader'; import Status from '../components/status'; import db from '../utils/db'; @@ -36,82 +36,79 @@ function Home({ hidden }) { states.homeNew = []; } const allStatuses = await homeIterator.current.next(); - if (allStatuses.value <= 0) { - return { done: true }; - } - const homeValues = allStatuses.value.map((status) => { - saveStatus(status); - return { - id: status.id, - reblog: status.reblog?.id, - reply: !!status.inReplyToAccountId, - }; - }); + if (allStatuses.value?.length) { + const homeValues = allStatuses.value.map((status) => { + saveStatus(status); + return { + id: status.id, + reblog: status.reblog?.id, + reply: !!status.inReplyToAccountId, + }; + }); - // BOOSTS CAROUSEL - if (snapStates.settings.boostsCarousel) { - let specialHome = []; - let boostStash = []; - let serialBoosts = 0; - for (let i = 0; i < homeValues.length; i++) { - const status = homeValues[i]; - if (status.reblog) { - boostStash.push(status); - serialBoosts++; - } else { - specialHome.push(status); - if (serialBoosts < 3) { - serialBoosts = 0; + // BOOSTS CAROUSEL + if (snapStates.settings.boostsCarousel) { + let specialHome = []; + let boostStash = []; + let serialBoosts = 0; + for (let i = 0; i < homeValues.length; i++) { + const status = homeValues[i]; + if (status.reblog) { + boostStash.push(status); + serialBoosts++; + } else { + specialHome.push(status); + if (serialBoosts < 3) { + serialBoosts = 0; + } } } - } - // if boostStash is more than quarter of homeValues - // or if there are 3 or more boosts in a row - if (boostStash.length > homeValues.length / 4 || serialBoosts >= 3) { - // if boostStash is more than 3 quarter of homeValues - const boostStashID = boostStash.map((status) => status.id); - if (boostStash.length > (homeValues.length * 3) / 4) { - // insert boost array at the end of specialHome list - specialHome = [ - ...specialHome, - { id: boostStashID, boosts: boostStash }, - ]; + // if boostStash is more than quarter of homeValues + // or if there are 3 or more boosts in a row + if (boostStash.length > homeValues.length / 4 || serialBoosts >= 3) { + // if boostStash is more than 3 quarter of homeValues + const boostStashID = boostStash.map((status) => status.id); + if (boostStash.length > (homeValues.length * 3) / 4) { + // insert boost array at the end of specialHome list + specialHome = [ + ...specialHome, + { id: boostStashID, boosts: boostStash }, + ]; + } else { + // insert boosts array in the middle of specialHome list + const half = Math.floor(specialHome.length / 2); + specialHome = [ + ...specialHome.slice(0, half), + { + id: boostStashID, + boosts: boostStash, + }, + ...specialHome.slice(half), + ]; + } } else { - // insert boosts array in the middle of specialHome list - const half = Math.floor(specialHome.length / 2); - specialHome = [ - ...specialHome.slice(0, half), - { - id: boostStashID, - boosts: boostStash, - }, - ...specialHome.slice(half), - ]; + // Untouched, this is fine + specialHome = homeValues; + } + console.log({ + specialHome, + }); + if (firstLoad) { + states.home = specialHome; + } else { + states.home.push(...specialHome); } } else { - // Untouched, this is fine - specialHome = homeValues; - } - console.log({ - specialHome, - }); - if (firstLoad) { - states.home = specialHome; - } else { - states.home.push(...specialHome); - } - } else { - if (firstLoad) { - states.home = homeValues; - } else { - states.home.push(...homeValues); + if (firstLoad) { + states.home = homeValues; + } else { + states.home.push(...homeValues); + } } } states.homeLastFetchTime = Date.now(); - return { - done: false, - }; + return allStatuses; } const loadingStatuses = useRef(false); @@ -276,13 +273,161 @@ function Home({ hidden }) { }, []); return ( - + ); } @@ -504,9 +499,9 @@ function BoostsCarousel({ boosts }) { const actualStatusID = reblog || statusID; return (
  • - + - +
  • ); })} diff --git a/src/pages/login.jsx b/src/pages/login.jsx index d184ceaa..5bcc0ce6 100644 --- a/src/pages/login.jsx +++ b/src/pages/login.jsx @@ -2,6 +2,7 @@ import './login.css'; import { useEffect, useRef, useState } from 'preact/hooks'; +import Link from '../components/link'; import Loader from '../components/loader'; import instancesListURL from '../data/instances.json?url'; import { getAuthorizationURL, registerApplication } from '../utils/auth'; @@ -111,7 +112,7 @@ function Login() {

    - 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 && ( @@ -232,19 +232,19 @@ function Notifications() { states.notificationsNew = []; } const allNotifications = await notificationsIterator.current.next(); - if (allNotifications.value <= 0) { - return { done: true }; - } - const notificationsValues = allNotifications.value.map((notification) => { - if (notification.status) { - states.statuses[notification.status.id] = notification.status; + if (allNotifications.value?.length) { + const notificationsValues = allNotifications.value.map((notification) => { + saveStatus(notification.status, { + skipThreading: true, + override: false, + }); + return notification; + }); + if (firstLoad) { + states.notifications = notificationsValues; + } else { + states.notifications.push(...notificationsValues); } - return notification; - }); - if (firstLoad) { - states.notifications = notificationsValues; - } else { - states.notifications.push(...notificationsValues); } states.notificationsLastFetchTime = Date.now(); return allNotifications; @@ -310,9 +310,9 @@ function Notifications() { }} >
    - + - +

    Notifications

    diff --git a/src/pages/settings.jsx b/src/pages/settings.jsx index 60b55d38..b9aa6987 100644 --- a/src/pages/settings.jsx +++ b/src/pages/settings.jsx @@ -5,6 +5,7 @@ import { useSnapshot } from 'valtio'; import Avatar from '../components/avatar'; import Icon from '../components/icon'; +import Link from '../components/link'; import NameText from '../components/name-text'; import RelativeTime from '../components/relative-time'; import states from '../utils/states'; @@ -124,9 +125,9 @@ function Settings({ onClose }) {

    )}

    - + Add new account - +

    Settings

    diff --git a/src/pages/status.jsx b/src/pages/status.jsx index c0ce9e73..1c1719bb 100644 --- a/src/pages/status.jsx +++ b/src/pages/status.jsx @@ -1,13 +1,14 @@ import './status.css'; import debounce from 'just-debounce-it'; -import { Link } from 'preact-router/match'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; import { InView } from 'react-intersection-observer'; +import { useLocation, useParams } from 'react-router-dom'; import { useSnapshot } from 'valtio'; 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'; @@ -23,7 +24,9 @@ import useTitle from '../utils/useTitle'; const LIMIT = 40; -function StatusPage({ id }) { +function StatusPage() { + const { id } = useParams(); + const location = useLocation(); const snapStates = useSnapshot(states); const [statuses, setStatuses] = useState([]); const [uiState, setUIState] = useState('default'); @@ -270,10 +273,11 @@ function StatusPage({ id }) { : 'Status', ); - const prevRoute = states.history.findLast((h) => { - return h === '/' || /notifications/i.test(h); - }); - const closeLink = `#${prevRoute || '/'}`; + const closeLink = useMemo(() => { + const pathname = snapStates.prevLocation?.pathname; + if (!pathname || pathname.startsWith('/s/')) return '/'; + return pathname; + }, []); const [limit, setLimit] = useState(LIMIT); const showMore = useMemo(() => { @@ -305,7 +309,7 @@ function StatusPage({ id }) { return (
    - +
    @@ -420,7 +424,7 @@ function StatusPage({ id }) { class=" status-link " - href={`#/s/${statusID}`} + to={`/s/${statusID}`} > diff --git a/src/pages/welcome.jsx b/src/pages/welcome.jsx index 4ba7e7c2..0f417a62 100644 --- a/src/pages/welcome.jsx +++ b/src/pages/welcome.jsx @@ -1,6 +1,7 @@ import './welcome.css'; import logo from '../assets/logo.svg'; +import Link from '../components/link'; import useTitle from '../utils/useTitle'; function Welcome() { @@ -28,9 +29,9 @@ function Welcome() {

    - + Log in - +