v4: the followed feed's two views are pages — "From people" and "From playlists" on a feed's "follow" tab were a state of the page, which a reload, Back or a shared link lost, while a profile's same split is /@name/follow/playlists. They are now /follow and /follow/playlists, and /music/follow/playlists in a category, feedPath taking the view as userPath does (FOLLOW_VIEWS); picking a category keeps it. Their dumps come from GET /api/dumps?follow=users|playlists, signed in, which the feed's word for it names, instead of GET /api/me/feed?from=; an unknown category is a 404 there, as on the rest of the feed, rather than an empty list.

A feed's tab, as a profile's, a search's or the community page's, now takes the place of the current history entry, since it is a view of the same page; picking a category, another page, still adds one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-09 22:09:12 +00:00
parent 53ceb6bcbc
commit 6dfba07341
13 changed files with 244 additions and 147 deletions

View File

@@ -5,8 +5,9 @@
* more segment, and a default view the bare path, with no redirect.
*
* - `/` is the feed of every dump on the viewer's default tab, and `/hot`,
* `/new`, `/journal` and `/follow` its tabs (`Index`). `/?share_url=` opens
* the create form on a shared link.
* `/new`, `/journal` and `/follow` its tabs (`Index`), `/follow/playlists`
* the dumps of the playlists followed. `/?share_url=` opens the create form
* on a shared link.
* - `/search` and `/search/:searchTab` are the search results (`Search`).
* - `/register` and `/login` sign up and sign in (`UserRegister`,
* `UserLogin`), signed out only, `/login?next=` coming back to a page.
@@ -28,7 +29,7 @@
* signed in only), and their playlists at `playlists/:slug`
* (`PlaylistDetail`).
* - `/:categorySlug` and `/:categorySlug/:feedTab` are a dump category's feed
* (`Index`).
* (`Index`), with its `/follow/playlists` too.
* - Any other path is `NotFound`.
*
* The pages not marked are open to anyone. Every page is loaded with its
@@ -49,7 +50,12 @@ import {
import { RestrictedGuest } from "./pages/RestrictedGuest.tsx";
import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx";
import { FEED_TABS, type FeedTab } from "../shared/paths.ts";
import {
FEED_TABS,
type FeedTab,
FOLLOW_VIEWS,
type FollowView,
} from "../shared/paths.ts";
import { AuthProvider } from "./contexts/auth/AuthProvider.tsx";
import { PlayerProvider } from "./contexts/player/PlayerProvider.tsx";
@@ -132,18 +138,21 @@ const NotFound = lazy(() =>
/**
* Renders a feed, of every dump or of `categorySlug`'s, on the tab the path's
* segment after the scope names, or the viewer's default one on the bare
* path. Every feed path renders it at the same place in the tree, and the key
* keeps `Index` mounted on a change of tab, which sorts on the client, and
* remounts it on a change of category, so that it refetches.
* path, and the `follow` tab on the view the next segment names. Every feed
* path renders it at the same place in the tree, and the key keeps `Index`
* mounted on a change of tab, which sorts on the client, and remounts it on a
* change of category, so that it refetches.
*/
function Feed({ categorySlug }: { categorySlug?: string }) {
const { pathname } = useLocation();
const segments = pathname.split("/").filter(Boolean);
const rest = segments.slice(categorySlug ? 1 : 0);
const feedTab = rest[0];
const [feedTab, view, ...more] = segments.slice(categorySlug ? 1 : 0);
if (
rest.length > 1 ||
(feedTab && !(FEED_TABS as readonly string[]).includes(feedTab))
more.length > 0 ||
(feedTab && !(FEED_TABS as readonly string[]).includes(feedTab)) ||
(view !== undefined &&
(feedTab !== "follow" || view === FOLLOW_VIEWS[0] ||
!(FOLLOW_VIEWS as readonly string[]).includes(view)))
) {
return <NotFound />;
}
@@ -152,6 +161,7 @@ function Feed({ categorySlug }: { categorySlug?: string }) {
key={categorySlug ?? ""}
categorySlug={categorySlug}
feedTab={feedTab as FeedTab | undefined}
followView={view as FollowView | undefined}
/>
);
}