From 9bff95bcecbf43d747e8859feed16a00c5552f95 Mon Sep 17 00:00:00 2001
From: Lim Chee Aun
Date: Sat, 21 Jan 2023 00:23:59 +0800
Subject: [PATCH] Replace preact-router with react-router
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Need more routing powers, hopefully things don't break 🤞
---
README.md | 1 +
package-lock.json | 101 +++++---
package.json | 3 +-
src/app.css | 88 +++++--
src/app.jsx | 120 ++++++----
src/components/link.jsx | 30 +++
src/components/status.jsx | 9 +-
src/main.jsx | 8 +-
src/pages/bookmarks.jsx | 144 ++++++++++++
src/pages/home.jsx | 449 ++++++++++++++++++------------------
src/pages/login.jsx | 3 +-
src/pages/notifications.jsx | 34 +--
src/pages/settings.jsx | 5 +-
src/pages/status.jsx | 24 +-
src/pages/welcome.jsx | 5 +-
15 files changed, 662 insertions(+), 362 deletions(-)
create mode 100644 src/components/link.jsx
create mode 100644 src/pages/bookmarks.jsx
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 */}
-
- {/* Notifications can be unmounted */}
- {currentDeck === 'notifications' && }
-
- )}
- {!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 && (
+
loadBookmarks()}
+ style={{ marginBlockEnd: '6em' }}
+ >
+ {uiState === 'loading' ? : <>Show more…>}
+
+ )}
+ >
+ ) : (
+ uiState !== 'loading' && (
+
No bookmarks yet. Go bookmark something!
+ )
+ )}
+ {uiState === 'loading' ? (
+
+
+
+ ) : uiState === 'error' ? (
+
+ Unable to load bookmarks.
+
+
+ loadBookmarks(!bookmarks.length)}
+ >
+ Try again
+
+
+ ) : (
+ 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 (
-
+ <>
+
+
+
{
+ scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ onDblClick={() => {
+ loadStatuses(true);
+ }}
+ >
+
+ Home
+
+
+ {snapStates.homeNew.length > 0 &&
+ scrollDirection === 'start' &&
+ !nearReachStart &&
+ !nearReachEnd && (
+
{
+ if (!snapStates.settings.boostsCarousel) {
+ const uniqueHomeNew = snapStates.homeNew.filter(
+ (status) => !states.home.some((s) => s.id === status.id),
+ );
+ states.home.unshift(...uniqueHomeNew);
+ }
+ loadStatuses(true);
+ states.homeNew = [];
+
+ scrollableRef.current?.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ }}
+ >
+ New posts
+
+ )}
+ {snapStates.home.length ? (
+ <>
+
+ {snapStates.home.map(({ id: statusID, reblog, boosts }) => {
+ const actualStatusID = reblog || statusID;
+ if (boosts) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+
+
+ );
+ })}
+ {showMore && (
+ <>
+ {/* {
+ if (inView) loadStatuses();
+ }}
+ root={scrollableRef.current}
+ rootMargin="100px 0px"
+ > */}
+
+
+
+ {/* */}
+
+
+
+ >
+ )}
+
+ >
+ ) : (
+ <>
+ {uiState === 'loading' && (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+
+
+ ))}
+
+ )}
+ {uiState === 'error' && (
+
+ Unable to load statuses
+
+
+ {
+ loadStatuses(true);
+ }}
+ >
+ Try again
+
+
+ )}
+ >
+ )}
+
+
-
-
{
- scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
- }}
- onDblClick={() => {
- loadStatuses(true);
- }}
- >
-
- Home
-
-
- {snapStates.homeNew.length > 0 &&
- scrollDirection === 'start' &&
- !nearReachStart &&
- !nearReachEnd && (
-
{
- if (!snapStates.settings.boostsCarousel) {
- const uniqueHomeNew = snapStates.homeNew.filter(
- (status) => !states.home.some((s) => s.id === status.id),
- );
- states.home.unshift(...uniqueHomeNew);
- }
- loadStatuses(true);
- states.homeNew = [];
-
- scrollableRef.current?.scrollTo({
- top: 0,
- behavior: 'smooth',
- });
- }}
- >
- New posts
-
- )}
- {snapStates.home.length ? (
- <>
-
- {snapStates.home.map(({ id: statusID, reblog, boosts }) => {
- const actualStatusID = reblog || statusID;
- if (boosts) {
- return (
-
-
-
- );
- }
- return (
-
-
-
-
-
- );
- })}
- {showMore && (
- <>
- {/* {
- if (inView) loadStatuses();
- }}
- root={scrollableRef.current}
- rootMargin="100px 0px"
- > */}
-
-
-
- {/* */}
-
-
-
- >
- )}
-
- >
- ) : (
- <>
- {uiState === 'loading' && (
-
- {Array.from({ length: 5 }).map((_, i) => (
-
-
-
- ))}
-
- )}
- {uiState === 'error' && (
-
- Unable to load statuses
-
-
- {
- loadStatuses(true);
- }}
- >
- Try again
-
-
- )}
- >
- )}
-
-
+ >
);
}
@@ -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