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

@@ -12,6 +12,12 @@
* dumps the caller may see, newest first.
* - `GET /api/dumps?category=&page=&limit=` — optional sign-in. The same, in
* the category with that slug; 404 when there is none.
* - `GET /api/dumps?follow=users&category=&page=&limit=` — signed in. The
* index's "follow" tab: a page of the public dumps of the users the caller
* follows, newest first, optionally in one category.
* - `GET /api/dumps?follow=playlists&category=&page=&limit=` — signed in. Its
* `/follow/playlists` view: a page of the public dumps in the public
* playlists the caller follows, most recently added first.
* - `GET /api/dumps?url=` — optional sign-in. Answers the dumps already
* pointing at the same thing as `url`, for the create form's duplicate hint.
* - `GET /api/dumps/:dumpId` — optional sign-in. Answers a dump.
@@ -50,7 +56,7 @@ import { toPublicUser } from "../model/user.ts";
import { authMiddleware } from "../middleware/auth.ts";
import { can } from "../lib/permissions.ts";
import { parseOptionalAuth } from "../lib/auth.ts";
import { parsePagination } from "../lib/pagination.ts";
import { type Page, parsePagination } from "../lib/pagination.ts";
import {
createFileDump,
createUrlDump,
@@ -61,7 +67,15 @@ import {
refreshDumpMetadata,
updateDump,
} from "../services/dump-service.ts";
import { listDumpsByCategory } from "../services/category-service.ts";
import {
getCategoryBySlug,
listDumpsByCategory,
} from "../services/category-service.ts";
import {
getFollowedPlaylistsDumpFeed,
getFollowedUsersDumpFeed,
} from "../services/follow-service.ts";
import { FOLLOW_VIEWS, type FollowView } from "../../shared/paths.ts";
import { getDumpVoters } from "../services/vote-service.ts";
import { getRelatedDumps } from "../services/backlink-service.ts";
import { getPlaylistsForDump } from "../services/playlist-service.ts";
@@ -174,9 +188,25 @@ router.get("/", async (ctx) => {
}
const { page, limit } = parsePagination(params);
const category = params.get("category");
const { items, hasMore } = category
? listDumpsByCategory(category, page, limit, requestingUserId)
: listDumps(page, limit, requestingUserId);
const follow = params.get("follow");
let feed: Page<Dump>;
if (follow !== null) {
if (!requestingUserId) throw new APIException(401, "Missing token");
if (!(FOLLOW_VIEWS as readonly string[]).includes(follow)) {
throw new APIException(400, "follow must be users or playlists");
}
const categoryId = category
? getCategoryBySlug("dump", category).id
: undefined;
feed = (follow as FollowView) === "users"
? getFollowedUsersDumpFeed(requestingUserId, page, limit, categoryId)
: getFollowedPlaylistsDumpFeed(requestingUserId, page, limit, categoryId);
} else {
feed = category
? listDumpsByCategory(category, page, limit, requestingUserId)
: listDumps(page, limit, requestingUserId);
}
const { items, hasMore } = feed;
const responseBody: APIResponse<PaginatedData<Dump>> = {
success: true,
data: { items, hasMore },