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:
@@ -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 },
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Serves what the caller follows, and the index's "follow" tab built from it.
|
||||
* What another user follows is served by `routes/users.ts`.
|
||||
* Serves what the caller follows, and follows or stops following for them.
|
||||
* The dumps of what they follow, the index's "follow" tab, are
|
||||
* `GET /api/dumps?follow=`'s (`routes/dumps.ts`), and what another user
|
||||
* follows is `routes/users.ts`'s.
|
||||
*
|
||||
* - `GET /api/me/follow` — signed in. Answers the ids of the users and
|
||||
* playlists they follow.
|
||||
@@ -10,35 +12,21 @@
|
||||
* playlist.
|
||||
* - `DELETE /api/me/follow/playlists/:playlistId` — signed in. Stops following
|
||||
* a playlist.
|
||||
* - `GET /api/me/feed?from=users&page=&limit=&category=` — signed in. Answers
|
||||
* a page of the public dumps of the users they follow, newest first,
|
||||
* optionally in one category.
|
||||
* - `GET /api/me/feed?from=playlists&page=&limit=&category=` — signed in.
|
||||
* Answers a page of the public dumps in the public playlists they follow,
|
||||
* most recently added first, optionally in one category.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { Router } from "@oak/oak";
|
||||
import { authMiddleware } from "../middleware/auth.ts";
|
||||
import { APIException } from "../lib/api-exception.ts";
|
||||
import type { APIResponse, PaginatedData } from "../../shared/api.ts";
|
||||
import type { Dump } from "../../shared/dump.ts";
|
||||
import type { APIResponse } from "../../shared/api.ts";
|
||||
import type { FollowStatus } from "../../shared/follow.ts";
|
||||
import { type Page, parsePagination } from "../lib/pagination.ts";
|
||||
import {
|
||||
followPlaylist,
|
||||
followUser,
|
||||
getFollowedPlaylistsDumpFeed,
|
||||
getFollowedUsersDumpFeed,
|
||||
getFollowStatus,
|
||||
unfollowPlaylist,
|
||||
unfollowUser,
|
||||
} from "../services/follow-service.ts";
|
||||
import { resolveCategoryFilter } from "../services/category-service.ts";
|
||||
|
||||
const EMPTY_FEED: Page<Dump> = { items: [], hasMore: false };
|
||||
|
||||
/**
|
||||
* Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware.
|
||||
@@ -72,25 +60,4 @@ router.delete("/follow/playlists/:playlistId", authMiddleware, (ctx) => {
|
||||
ctx.response.status = 204;
|
||||
});
|
||||
|
||||
router.get("/feed", authMiddleware, (ctx) => {
|
||||
const params = ctx.request.url.searchParams;
|
||||
const from = params.get("from");
|
||||
if (from !== "users" && from !== "playlists") {
|
||||
throw new APIException(400, "from must be users or playlists");
|
||||
}
|
||||
const { page, limit } = parsePagination(params);
|
||||
const categoryId = resolveCategoryFilter("dump", params);
|
||||
const userId = ctx.state.user.userId;
|
||||
const { items, hasMore } = categoryId === "missing"
|
||||
? EMPTY_FEED
|
||||
: from === "users"
|
||||
? getFollowedUsersDumpFeed(userId, page, limit, categoryId)
|
||||
: getFollowedPlaylistsDumpFeed(userId, page, limit, categoryId);
|
||||
const body: APIResponse<PaginatedData<Dump>> = {
|
||||
success: true,
|
||||
data: { items, hasMore },
|
||||
};
|
||||
ctx.response.body = body;
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user