diff --git a/README.md b/README.md index 2fa32093..90717850 100644 --- a/README.md +++ b/README.md @@ -167,8 +167,11 @@ of an exported interface; a comment that describes a declaration is always `/** phrase. Parameters are named in the prose, in backticks. `{@link}` points at symbols the file already has in scope. A function that can throw to its caller says so with `@throws`: for an `APIException`, the statuses and when. A router's -JSDoc lists its endpoints, one line each: method and path, who may call it, what -it does; a page's names its route and who may see it. +module doc lists its endpoints, one line each: method and path, who may call it, +what it does; a page's JSDoc names its route and who may see it. The first +paragraph of a doc is its summary, which the reference shows alone in lists, so +it is a sentence that stands on its own: it never ends in a colon introducing +what follows. In the frontend, every exported function states its return type, so the reference can show it: a component returns `React.JSX.Element` (or `| null`), a @@ -180,12 +183,13 @@ rather than a default import of `react`, which `deno doc` can't follow. `deno task doc` builds the HTML reference into `docs/` from every module of `api/`, `shared/` and `src/` but the tests (and the generated `src/locales/`). Its index page is the module doc of `mod.ts` at the root, which introduces the -repository's three parts and links every documented module (`mod.test.ts` checks -it), and the `docs` workflow publishes it on every push to `main` that touches -them. `deno task doc:lint` checks the rules the linter can see, through -`doc-lint.ts`: `deno doc` doesn't document npm packages, so it takes a type -imported from one (react-hook-form's `FieldValues`, say) for a private one, and -those references alone are let through. +repository's three parts and their layers in prose; `deno doc` builds the list +of every module itself, grouped by path. The `docs` workflow publishes it on +every push to `main` that touches them. `deno task doc:lint` checks the rules +the linter can see, and that no summary ends in a colon, through `doc-lint.ts`. +`deno doc` doesn't document npm packages, so it takes a type imported from one +(react-hook-form's `FieldValues`, say) for a private one, and those references +alone are let through. Each of the frontend's `components/`, `contexts/`, `hooks/` and `utils/` is split into the same domain folders — `auth`, `category`, `chat`, `comment`, diff --git a/api/config.ts b/api/config.ts index c0bb00c3..90541f4b 100644 --- a/api/config.ts +++ b/api/config.ts @@ -139,7 +139,8 @@ export const OG_SITE_NAME: string = Deno.env.get("GERBEUR_SITE_NAME") || export const SITE_EMOJI: string = Deno.env.get("GERBEUR_SITE_EMOJI") || "🚚"; /** - * A Bandcamp playback path: + * A Bandcamp playback path. + * * - `"embed"` — the bandcamp.com iframe (cannot autoplay) * - `"native"` — resolve the page's mp3 streams and play them in the app's own * player, which autoplays and supports albums as a queue diff --git a/api/db/migrations/0016_performance_indexes.ts b/api/db/migrations/0016_performance_indexes.ts index dbffa314..b48e0742 100644 --- a/api/db/migrations/0016_performance_indexes.ts +++ b/api/db/migrations/0016_performance_indexes.ts @@ -1,5 +1,6 @@ /** - * Indexes for the queries that ran as full scans: + * Indexes for the queries that ran as full scans. + * * - feeds page by created_at (newest first), a user's dumps and upvotes by * (user_id, created_at) — these replace the bare user_id indexes; * - dump and playlist pages are looked up by slug; diff --git a/api/routes/attachments.ts b/api/routes/attachments.ts index 0614f9be..a247eeb3 100644 --- a/api/routes/attachments.ts +++ b/api/routes/attachments.ts @@ -1,6 +1,11 @@ /** * The attachment routes, for images pasted into Markdown bodies. * + * - `POST /api/attachments` — signed in. Stores an image (multipart `file`, at + * most 5 MB) and answers its id; it is bound to a resource once a saved body + * links it. + * - `GET /api/attachments/:attachmentId` — public. Serves the image. + * * @module */ @@ -19,14 +24,7 @@ import { validateImageUpload, } from "../lib/upload.ts"; -/** - * The attachment routes, for images pasted into Markdown bodies: - * - * - `POST /api/attachments` — signed in. Stores an image (multipart `file`, at - * most 5 MB) and answers its id; it is bound to a resource once a saved body - * links it. - * - `GET /api/attachments/:attachmentId` — public. Serves the image. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router(); router.post("/api/attachments", authMiddleware, async (ctx) => { diff --git a/api/routes/avatars.ts b/api/routes/avatars.ts index 117d4f21..b8e436df 100644 --- a/api/routes/avatars.ts +++ b/api/routes/avatars.ts @@ -1,6 +1,11 @@ /** * The avatar routes. * + * - `POST /api/avatars/me` — signed in. Replaces the caller's avatar (multipart + * `file`, at most 5 MB), announces it, and answers the caller's user. + * - `GET /api/avatars/:userId` — public. Serves a user's avatar; 404 without + * one. + * * @module */ @@ -21,14 +26,7 @@ import { validateImageUpload, } from "../lib/upload.ts"; -/** - * The avatar routes: - * - * - `POST /api/avatars/me` — signed in. Replaces the caller's avatar (multipart - * `file`, at most 5 MB), announces it, and answers the caller's user. - * - `GET /api/avatars/:userId` — public. Serves a user's avatar; 404 without - * one. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router(); router.post("/api/avatars/me", authMiddleware, async (ctx) => { diff --git a/api/routes/bandcamp.ts b/api/routes/bandcamp.ts index a2f33b2b..fe4e495a 100644 --- a/api/routes/bandcamp.ts +++ b/api/routes/bandcamp.ts @@ -1,5 +1,8 @@ /** - * The Bandcamp route. + * The Bandcamp route, for the native player. + * + * - `GET /api/bandcamp/tracks?url=&force=` — public. Resolves a Bandcamp page + * to its streamable tracks, at play time; `force=1` bypasses the cache. * * @module */ @@ -9,12 +12,7 @@ import type { APIResponse } from "../../shared/api.ts"; import { resolveBandcamp } from "../remote/bandcamp-streams.ts"; import type { Tralbum } from "../../shared/player.ts"; -/** - * The Bandcamp route, for the native player: - * - * - `GET /api/bandcamp/tracks?url=&force=` — public. Resolves a Bandcamp page - * to its streamable tracks, at play time; `force=1` bypasses the cache. - */ +/** The router serving the endpoints the module doc lists. */ const bandcampRouter: Router = new Router(); // Public: it only reads a public page and returns URLs the same page already diff --git a/api/routes/categories.ts b/api/routes/categories.ts index 8d4b1738..bce626f6 100644 --- a/api/routes/categories.ts +++ b/api/routes/categories.ts @@ -1,6 +1,16 @@ /** * The category routes. * + * - `GET /api/categories` — public. Answers every category, in order. + * - `GET /api/categories/:slug` — public. Answers one category. + * - `GET /api/categories/:slug/dumps?page=&limit=` — optional sign-in. Answers + * a page of its dumps, newest first, with the caller's private ones. + * - `POST /api/categories` — permission `category:manage`. Creates a category. + * - `PUT /api/categories/:id` — permission `category:manage`. Updates a + * category. + * - `DELETE /api/categories/:id` — permission `category:manage`. Deletes a + * category; its dumps stay. + * * @module */ @@ -31,19 +41,7 @@ import { updateCategory, } from "../services/category-service.ts"; -/** - * The category routes: - * - * - `GET /api/categories` — public. Answers every category, in order. - * - `GET /api/categories/:slug` — public. Answers one category. - * - `GET /api/categories/:slug/dumps?page=&limit=` — optional sign-in. Answers - * a page of its dumps, newest first, with the caller's private ones. - * - `POST /api/categories` — permission `category:manage`. Creates a category. - * - `PUT /api/categories/:id` — permission `category:manage`. Updates a - * category. - * - `DELETE /api/categories/:id` — permission `category:manage`. Deletes a - * category; its dumps stay. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/categories" }); router.get("/", (ctx) => { diff --git a/api/routes/chat.ts b/api/routes/chat.ts index 7d754474..c05c646c 100644 --- a/api/routes/chat.ts +++ b/api/routes/chat.ts @@ -1,5 +1,13 @@ /** - * The chat history route. + * The chat history route; messages are posted and edited over the socket (see + * routes/ws.ts). + * + * - `GET /api/chat?limit=&before=&beforeId=&after=&afterId=` — signed in. + * Answers messages in ascending order: the most recent by default; with + * `before`, the page of older messages preceding that cursor ("load older"); + * with `after`, the messages following it (resync after a reconnect). A + * cursor is an ISO `created_at` plus the message id to break ties; without + * `afterId` the `after` timestamp itself is included. * * @module */ @@ -13,17 +21,7 @@ import { getRecentChatMessages, } from "../services/chat-service.ts"; -/** - * The chat history route; messages are posted and edited over the socket (see - * routes/ws.ts): - * - * - `GET /api/chat?limit=&before=&beforeId=&after=&afterId=` — signed in. - * Answers messages in ascending order: the most recent by default; with - * `before`, the page of older messages preceding that cursor ("load older"); - * with `after`, the messages following it (resync after a reconnect). A - * cursor is an ISO `created_at` plus the message id to break ties; without - * `afterId` the `after` timestamp itself is included. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/chat" }); router.get("/", authMiddleware, (ctx) => { diff --git a/api/routes/comments.ts b/api/routes/comments.ts index 86c44ea6..c39b2ee9 100644 --- a/api/routes/comments.ts +++ b/api/routes/comments.ts @@ -1,5 +1,17 @@ /** - * The comment routes. + * The comment routes. A private dump's comments exist only for its poster, and + * changes to them aren't broadcast. + * + * - `GET /api/dumps/:dumpId/comments` — optional sign-in. Answers every comment + * on a dump, oldest first. + * - `POST /api/dumps/:dumpId/comments` — signed in. Posts a comment, replying + * to `parentId` if given. + * - `PATCH /api/comments/:commentId` — signed in, as its author or with + * `comment:moderate`. Edits a comment. + * - `GET /api/comments/:commentId/likers?page=&limit=` — optional sign-in. + * Answers a page of the users who like a comment. + * - `DELETE /api/comments/:commentId` — signed in, as its author or with + * `comment:moderate`. Deletes a comment, leaving its place in the thread. * * @module */ @@ -37,21 +49,7 @@ import { broadcastCommentUpdated, } from "../services/ws-service.ts"; -/** - * The comment routes. A private dump's comments exist only for its poster, and - * changes to them aren't broadcast: - * - * - `GET /api/dumps/:dumpId/comments` — optional sign-in. Answers every comment - * on a dump, oldest first. - * - `POST /api/dumps/:dumpId/comments` — signed in. Posts a comment, replying - * to `parentId` if given. - * - `PATCH /api/comments/:commentId` — signed in, as its author or with - * `comment:moderate`. Edits a comment. - * - `GET /api/comments/:commentId/likers?page=&limit=` — optional sign-in. - * Answers a page of the users who like a comment. - * - `DELETE /api/comments/:commentId` — signed in, as its author or with - * `comment:moderate`. Deletes a comment, leaving its place in the thread. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api" }); router.get("/dumps/:dumpId/comments", async (ctx) => { diff --git a/api/routes/dumps.ts b/api/routes/dumps.ts index f537ed71..e3490727 100644 --- a/api/routes/dumps.ts +++ b/api/routes/dumps.ts @@ -1,5 +1,34 @@ /** - * The dump routes. + * The dump routes. `:dumpId` takes an id or a slug; a private dump exists only + * for its poster. + * + * - `POST /api/dumps` — signed in. Posts a link (JSON body) or a file + * (multipart `file`, with `title`, `comment`, `isPrivate` and `categoryIds`). + * - `GET /api/dumps/by-url?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. + * - `GET /api/dumps/:dumpId/voters?page=&limit=` — optional sign-in. Answers a + * page of the users who upvoted it. + * - `GET /api/dumps/:dumpId/related` — optional sign-in. Answers the dumps + * linked to or from it. + * - `GET /api/dumps/:dumpId/playlists` — optional sign-in. Answers the + * playlists it appears in. + * - `GET /api/dumps?page=&limit=` — optional sign-in. Answers a page of the + * dumps, newest first. + * - `PUT /api/dumps/:dumpId/file` — signed in, as its poster or with + * `dump:moderate`. Replaces a file dump's upload (multipart). + * - `PUT /api/dumps/:dumpId` — signed in, as its poster or with + * `dump:moderate`. Updates its title, comment, visibility, categories, and a + * link's URL. + * - `PUT /api/dumps/:dumpId/thumbnail` — signed in, as its poster or with + * `dump:moderate`. Replaces its custom thumbnail (multipart `file`, at most 5 + * MB). + * - `DELETE /api/dumps/:dumpId/thumbnail` — signed in, as its poster or with + * `dump:moderate`. Removes its custom thumbnail. + * - `POST /api/dumps/:dumpId/refresh-metadata` — signed in, as its poster or + * with `dump:moderate`. Fetches a link's preview again. + * - `DELETE /api/dumps/:dumpId` — signed in, as its poster or with + * `dump:moderate`. Deletes it. * * @module */ @@ -44,38 +73,7 @@ import { getDumpVoters } from "../services/vote-service.ts"; import { getRelatedDumps } from "../services/backlink-service.ts"; import { getPlaylistsForDump } from "../services/playlist-service.ts"; -/** - * The dump routes. `:dumpId` takes an id or a slug; a private dump exists only - * for its poster: - * - * - `POST /api/dumps` — signed in. Posts a link (JSON body) or a file - * (multipart `file`, with `title`, `comment`, `isPrivate` and `categoryIds`). - * - `GET /api/dumps/by-url?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. - * - `GET /api/dumps/:dumpId/voters?page=&limit=` — optional sign-in. Answers a - * page of the users who upvoted it. - * - `GET /api/dumps/:dumpId/related` — optional sign-in. Answers the dumps - * linked to or from it. - * - `GET /api/dumps/:dumpId/playlists` — optional sign-in. Answers the - * playlists it appears in. - * - `GET /api/dumps?page=&limit=` — optional sign-in. Answers a page of the - * dumps, newest first. - * - `PUT /api/dumps/:dumpId/file` — signed in, as its poster or with - * `dump:moderate`. Replaces a file dump's upload (multipart). - * - `PUT /api/dumps/:dumpId` — signed in, as its poster or with - * `dump:moderate`. Updates its title, comment, visibility, categories, and a - * link's URL. - * - `PUT /api/dumps/:dumpId/thumbnail` — signed in, as its poster or with - * `dump:moderate`. Replaces its custom thumbnail (multipart `file`, at most 5 - * MB). - * - `DELETE /api/dumps/:dumpId/thumbnail` — signed in, as its poster or with - * `dump:moderate`. Removes its custom thumbnail. - * - `POST /api/dumps/:dumpId/refresh-metadata` — signed in, as its poster or - * with `dump:moderate`. Fetches a link's preview again. - * - `DELETE /api/dumps/:dumpId` — signed in, as its poster or with - * `dump:moderate`. Deletes it. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/dumps" }); // Every route here takes a slug or an id; the services below them key on the diff --git a/api/routes/files.ts b/api/routes/files.ts index 15790c8e..e513a821 100644 --- a/api/routes/files.ts +++ b/api/routes/files.ts @@ -1,6 +1,10 @@ /** * The route serving file dumps' uploads. * + * - `GET /api/files/:dumpId?token=` — public; a private dump's file needs its + * poster's media `token`. Serves the file, inline when that is safe and as a + * download otherwise, with byte ranges. + * * @module */ @@ -12,13 +16,7 @@ import { authenticateMedia } from "../lib/auth.ts"; import { serveFile } from "../lib/serve-file.ts"; import { dumpFileVersion, getDump } from "../services/dump-service.ts"; -/** - * The route serving file dumps' uploads: - * - * - `GET /api/files/:dumpId?token=` — public; a private dump's file needs its - * poster's media `token`. Serves the file, inline when that is safe and as a - * download otherwise, with byte ranges. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/files" }); /** diff --git a/api/routes/follows.ts b/api/routes/follows.ts index fc77a258..c47cf9c0 100644 --- a/api/routes/follows.ts +++ b/api/routes/follows.ts @@ -1,5 +1,20 @@ /** - * The follow routes. + * The follow routes, all for what the caller follows. + * + * - `GET /api/follows/status` — signed in. Answers the ids of the users and + * playlists they follow. + * - `GET /api/follows/feed/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/follows/feed/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. + * - `POST /api/follows/users/:userId` — signed in. Follows a user. + * - `DELETE /api/follows/users/:userId` — signed in. Stops following a user. + * - `POST /api/follows/playlists/:playlistId` — signed in. Follows a public + * playlist. + * - `DELETE /api/follows/playlists/:playlistId` — signed in. Stops following a + * playlist. * * @module */ @@ -38,24 +53,7 @@ function resolveCategoryFilter( /** The feed a filter naming no category yields. */ const EMPTY_FEED: Page = { items: [], hasMore: false }; -/** - * The follow routes, all for what the caller follows: - * - * - `GET /api/follows/status` — signed in. Answers the ids of the users and - * playlists they follow. - * - `GET /api/follows/feed/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/follows/feed/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. - * - `POST /api/follows/users/:userId` — signed in. Follows a user. - * - `DELETE /api/follows/users/:userId` — signed in. Stops following a user. - * - `POST /api/follows/playlists/:playlistId` — signed in. Follows a public - * playlist. - * - `DELETE /api/follows/playlists/:playlistId` — signed in. Stops following a - * playlist. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/follows" }); // Static routes first to prevent Oak matching "status"/"feed" as a :param diff --git a/api/routes/invites.ts b/api/routes/invites.ts index 767ea8f4..749f7baa 100644 --- a/api/routes/invites.ts +++ b/api/routes/invites.ts @@ -1,6 +1,10 @@ /** * The invite routes. * + * - `POST /api/invites` — signed in. Makes an invite and answers its token. + * - `GET /api/invites/:token` — public. Checks an invite is valid, before the + * register page shows its form; 404 when it isn't. + * * @module */ @@ -11,13 +15,7 @@ import type { InviteResponse } from "../../shared/user.ts"; import { type AuthContext, authMiddleware } from "../middleware/auth.ts"; import { createInvite, validateInvite } from "../services/invite-service.ts"; -/** - * The invite routes: - * - * - `POST /api/invites` — signed in. Makes an invite and answers its token. - * - `GET /api/invites/:token` — public. Checks an invite is valid, before the - * register page shows its form; 404 when it isn't. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/invites" }); router.post("/", authMiddleware, (ctx: AuthContext) => { diff --git a/api/routes/notifications.ts b/api/routes/notifications.ts index ce1e2241..91e7f3fc 100644 --- a/api/routes/notifications.ts +++ b/api/routes/notifications.ts @@ -1,5 +1,10 @@ /** - * The notification routes. + * The notification routes, all for the caller's own notifications. + * + * - `GET /api/notifications?page=&limit=` — signed in. Answers a page of them, + * newest first. + * - `POST /api/notifications/read-all` — signed in. Marks them all read. + * - `PATCH /api/notifications/:id/read` — signed in. Marks one read. * * @module */ @@ -20,14 +25,7 @@ import { markOneRead, } from "../services/notification-service.ts"; -/** - * The notification routes, all for the caller's own notifications: - * - * - `GET /api/notifications?page=&limit=` — signed in. Answers a page of them, - * newest first. - * - `POST /api/notifications/read-all` — signed in. Marks them all read. - * - `PATCH /api/notifications/:id/read` — signed in. Marks one read. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/notifications" }); router.get("/", authMiddleware, (ctx: AuthContext) => { diff --git a/api/routes/playlists.ts b/api/routes/playlists.ts index c1777688..cdb2a582 100644 --- a/api/routes/playlists.ts +++ b/api/routes/playlists.ts @@ -1,5 +1,26 @@ /** - * The playlist routes. + * The playlist routes. `:playlistId` takes an id or a slug. + * + * - `GET /api/playlists/by-dump/:dumpId/memberships` — signed in. Answers each + * of the caller's playlists and whether it holds the dump. + * - `POST /api/playlists` — signed in. Creates a playlist. + * - `GET /api/playlists/:playlistId` — optional sign-in. Answers a playlist + * with the dumps the caller may see in it; a private one only to its owner. + * - `PUT /api/playlists/:playlistId` — signed in, as its owner or with + * `playlist:moderate`. Updates its title, description and visibility. + * - `DELETE /api/playlists/:playlistId` — signed in, as its owner or with + * `playlist:moderate`. Deletes it. + * - `POST /api/playlists/:playlistId/dumps/:dumpId` — signed in, as its owner + * or with `playlist:moderate`. Adds a dump at the top. + * - `DELETE /api/playlists/:playlistId/dumps/:dumpId` — signed in, as its owner + * or with `playlist:moderate`. Removes a dump. + * - `POST /api/playlists/:playlistId/image` — signed in, as its owner or with + * `playlist:moderate`. Replaces its cover (multipart `file`, at most 5 MB). + * - `GET /api/playlists/:playlistId/image?token=` — public; a private + * playlist's cover needs its owner's media `token`. Serves its cover; 404 + * without one. + * - `PUT /api/playlists/:playlistId/order` — signed in, as its owner or with + * `playlist:moderate`. Reorders the dumps the caller can see in it. * * @module */ @@ -39,30 +60,7 @@ import { validateImageUpload, } from "../lib/upload.ts"; -/** - * The playlist routes. `:playlistId` takes an id or a slug: - * - * - `GET /api/playlists/by-dump/:dumpId/memberships` — signed in. Answers each - * of the caller's playlists and whether it holds the dump. - * - `POST /api/playlists` — signed in. Creates a playlist. - * - `GET /api/playlists/:playlistId` — optional sign-in. Answers a playlist - * with the dumps the caller may see in it; a private one only to its owner. - * - `PUT /api/playlists/:playlistId` — signed in, as its owner or with - * `playlist:moderate`. Updates its title, description and visibility. - * - `DELETE /api/playlists/:playlistId` — signed in, as its owner or with - * `playlist:moderate`. Deletes it. - * - `POST /api/playlists/:playlistId/dumps/:dumpId` — signed in, as its owner - * or with `playlist:moderate`. Adds a dump at the top. - * - `DELETE /api/playlists/:playlistId/dumps/:dumpId` — signed in, as its owner - * or with `playlist:moderate`. Removes a dump. - * - `POST /api/playlists/:playlistId/image` — signed in, as its owner or with - * `playlist:moderate`. Replaces its cover (multipart `file`, at most 5 MB). - * - `GET /api/playlists/:playlistId/image?token=` — public; a private - * playlist's cover needs its owner's media `token`. Serves its cover; 404 - * without one. - * - `PUT /api/playlists/:playlistId/order` — signed in, as its owner or with - * `playlist:moderate`. Reorders the dumps the caller can see in it. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/playlists", }); diff --git a/api/routes/preview.ts b/api/routes/preview.ts index 65063282..4f377fc9 100644 --- a/api/routes/preview.ts +++ b/api/routes/preview.ts @@ -1,6 +1,11 @@ /** * The link preview routes. * + * - `GET /api/preview?url=` — signed in. Answers the preview of a link, and + * whether its page could be reached. + * - `GET /api/proxy-image?url=` — public. Serves a remote raster image through + * this site; 400 when it isn't one, 502 when its host can't be reached. + * * @module */ @@ -16,14 +21,7 @@ import { import type { LinkPreview } from "../../shared/dump.ts"; import { authMiddleware } from "../middleware/auth.ts"; -/** - * The link preview routes: - * - * - `GET /api/preview?url=` — signed in. Answers the preview of a link, and - * whether its page could be reached. - * - `GET /api/proxy-image?url=` — public. Serves a remote raster image through - * this site; 400 when it isn't one, 502 when its host can't be reached. - */ +/** The router serving the endpoints the module doc lists. */ const previewRouter: Router = new Router(); // Signed-in only: it's the create form's preview, and an open one lets anyone diff --git a/api/routes/search.ts b/api/routes/search.ts index 0638a1eb..a30092fb 100644 --- a/api/routes/search.ts +++ b/api/routes/search.ts @@ -1,6 +1,10 @@ /** * The search route. * + * - `GET /api/search?q=&page=&limit=` — optional sign-in. Answers a page of the + * matching dumps with their total, and up to 20 matching users and playlists; + * private dumps and playlists only the caller's own. + * * @module */ @@ -13,13 +17,7 @@ import { searchPlaylists } from "../services/playlist-service.ts"; import type { APIResponse } from "../../shared/api.ts"; import type { SearchResults } from "../../shared/search.ts"; -/** - * The search route: - * - * - `GET /api/search?q=&page=&limit=` — optional sign-in. Answers a page of the - * matching dumps with their total, and up to 20 matching users and playlists; - * private dumps and playlists only the caller's own. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/search" }); router.get("/", async (ctx) => { diff --git a/api/routes/thumbnails.ts b/api/routes/thumbnails.ts index 9ba0cef4..1498def7 100644 --- a/api/routes/thumbnails.ts +++ b/api/routes/thumbnails.ts @@ -1,6 +1,12 @@ /** * The route serving dumps' thumbnails, generated with ffmpeg where needed. * + * - `GET /api/thumbnails/:dumpId?token=&v=` — public; a private dump's + * thumbnail needs its poster's media `token`. Serves the dump's custom + * thumbnail, else one made from its upload: a still of a video, or a + * scaled-down copy of an image (the image itself when none can be made); 404 + * when there is neither. + * * @module */ @@ -17,15 +23,7 @@ import { serveUploadedFile } from "../lib/upload.ts"; import { DUMPS_DIR, THUMBNAILS_DIR } from "../config.ts"; import { authenticateMedia } from "../lib/auth.ts"; -/** - * The route serving dumps' thumbnails: - * - * - `GET /api/thumbnails/:dumpId?token=&v=` — public; a private dump's - * thumbnail needs its poster's media `token`. Serves the dump's custom - * thumbnail, else one made from its upload: a still of a video, or a - * scaled-down copy of an image (the image itself when none can be made); 404 - * when there is neither. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/thumbnails" }); /** diff --git a/api/routes/users.ts b/api/routes/users.ts index 06cd49ef..755206ed 100644 --- a/api/routes/users.ts +++ b/api/routes/users.ts @@ -1,5 +1,41 @@ /** - * The user and account routes. + * The user and account routes. Public profiles never carry the password hash or + * the email. + * + * - `POST /api/users/register` — public, with a valid invite. Creates an + * account, sends the welcome email, and answers the user and their tokens. + * - `POST /api/users/login` — public. Answers the user and their tokens; + * rate-limited per account. + * - `GET /api/users/me` — signed in. Answers the caller's own user. + * - `GET /api/users/me/media-token` — signed in. Answers a media token, for a + * session that predates them. + * - `POST /api/users/request-password-reset` — public. Emails a reset link to + * the account with that address, if any; always answers 200. + * - `POST /api/users/reset-password` — public, with a reset token. Sets a new + * password. + * - `POST /api/users/me/change-password` — signed in, with the current + * password. Sets a new one, ending every other session, and answers fresh + * tokens. + * - `PATCH /api/users/me` — signed in. Updates the caller's profile fields: + * description and email. + * - `PATCH /api/users/:userId/role` — permission `user:manage`. Sets another + * user's role. + * - `GET /api/users/search?q=` — public. Answers up to 8 users whose name + * starts with `q`, for @mention suggestions. + * - `GET /api/users/by-id/:userId` — public. Answers a user's profile by id. + * - `GET /api/users/:username/followed-users?page=&limit=` — public. Answers a + * page of the users they follow. + * - `GET /api/users/:username/followed-playlists?page=&limit=` — public. + * Answers a page of the public playlists they follow. + * - `GET /api/users/:username/playlists?page=&limit=` — optional sign-in. + * Answers a page of their playlists, the private ones only to themselves. + * - `GET /api/users/:username/invitees` — public. Answers the tree of users + * they invited. + * - `GET /api/users/:username` — public. Answers a user's profile. + * - `GET /api/users/:username/dumps?page=&limit=` — optional sign-in. Answers a + * page of their dumps, the private ones only to themselves. + * - `GET /api/users/:username/votes?page=&limit=` — optional sign-in. Answers a + * page of the dumps they upvoted, private ones only to themselves. * * @module */ @@ -70,45 +106,7 @@ import { getFollowedUsersByUser, } from "../services/follow-service.ts"; -/** - * The user and account routes. Public profiles never carry the password hash or - * the email: - * - * - `POST /api/users/register` — public, with a valid invite. Creates an - * account, sends the welcome email, and answers the user and their tokens. - * - `POST /api/users/login` — public. Answers the user and their tokens; - * rate-limited per account. - * - `GET /api/users/me` — signed in. Answers the caller's own user. - * - `GET /api/users/me/media-token` — signed in. Answers a media token, for a - * session that predates them. - * - `POST /api/users/request-password-reset` — public. Emails a reset link to - * the account with that address, if any; always answers 200. - * - `POST /api/users/reset-password` — public, with a reset token. Sets a new - * password. - * - `POST /api/users/me/change-password` — signed in, with the current - * password. Sets a new one, ending every other session, and answers fresh - * tokens. - * - `PATCH /api/users/me` — signed in. Updates the caller's profile fields: - * description and email. - * - `PATCH /api/users/:userId/role` — permission `user:manage`. Sets another - * user's role. - * - `GET /api/users/search?q=` — public. Answers up to 8 users whose name - * starts with `q`, for @mention suggestions. - * - `GET /api/users/by-id/:userId` — public. Answers a user's profile by id. - * - `GET /api/users/:username/followed-users?page=&limit=` — public. Answers a - * page of the users they follow. - * - `GET /api/users/:username/followed-playlists?page=&limit=` — public. - * Answers a page of the public playlists they follow. - * - `GET /api/users/:username/playlists?page=&limit=` — optional sign-in. - * Answers a page of their playlists, the private ones only to themselves. - * - `GET /api/users/:username/invitees` — public. Answers the tree of users - * they invited. - * - `GET /api/users/:username` — public. Answers a user's profile. - * - `GET /api/users/:username/dumps?page=&limit=` — optional sign-in. Answers a - * page of their dumps, the private ones only to themselves. - * - `GET /api/users/:username/votes?page=&limit=` — optional sign-in. Answers a - * page of the dumps they upvoted, private ones only to themselves. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api/users" }); /** Login attempts per account: a burst of 10, then one every 30s. */ diff --git a/api/routes/ws.ts b/api/routes/ws.ts index 419ba0f9..0dd85c92 100644 --- a/api/routes/ws.ts +++ b/api/routes/ws.ts @@ -1,6 +1,12 @@ /** * The WebSocket route, and the handling of what clients send over it. * + * - `GET /ws?token=` — public; a session `token` signs the socket in, and an + * invalid one leaves it anonymous. Opens the socket that carries presence, + * live updates, votes, comment likes and the chat (see + * `ClientToServerMessage` and `ServerToClientMessage` in shared/ws.ts); 403 + * from an origin not in {@link ALLOWED_ORIGINS}. + * * @module */ @@ -56,15 +62,7 @@ import type { } from "../../shared/ws.ts"; import type { User } from "../model/user.ts"; -/** - * The WebSocket route: - * - * - `GET /ws?token=` — public; a session `token` signs the socket in, and an - * invalid one leaves it anonymous. Opens the socket that carries presence, - * live updates, votes, comment likes and the chat (see - * `ClientToServerMessage` and `ServerToClientMessage` in shared/ws.ts); 403 - * from an origin not in {@link ALLOWED_ORIGINS}. - */ +/** The router serving the endpoints the module doc lists. */ const router: Router = new Router(); /** Checks whether a socket may be opened from `origin`. */ diff --git a/api/routes/youtube.ts b/api/routes/youtube.ts index 34d83c88..33c37842 100644 --- a/api/routes/youtube.ts +++ b/api/routes/youtube.ts @@ -1,5 +1,8 @@ /** - * The YouTube route. + * The YouTube route, for the player's tracklist. + * + * - `GET /api/youtube/playlist?list=` — public. Resolves a YouTube playlist id + * to its tracklist (titles and lengths). * * @module */ @@ -9,12 +12,7 @@ import type { APIResponse } from "../../shared/api.ts"; import { resolveYouTubePlaylist } from "../remote/youtube-playlist.ts"; import type { YouTubePlaylist } from "../../shared/player.ts"; -/** - * The YouTube route, for the player's tracklist: - * - * - `GET /api/youtube/playlist?list=` — public. Resolves a YouTube playlist id - * to its tracklist (titles and lengths). - */ +/** The router serving the endpoints the module doc lists. */ const youtubeRouter: Router = new Router(); // Public, like /api/bandcamp/tracks: it only reads a public page. The client diff --git a/deno.json b/deno.json index 9653d986..230ad01b 100644 --- a/deno.json +++ b/deno.json @@ -8,8 +8,8 @@ "i18n:extract": "deno run -A npm:@lingui/cli extract --clean --overwrite --workers 1", "i18n:compile": "deno run -A npm:@lingui/cli compile --workers 1", "start": "deno task icons dist && deno run --env-file -A api/db/init.ts && deno run --env-file -A api/main.ts", - "test": "deno test -A api/ shared/ mod.test.ts", - "test:coverage": "deno test -A --coverage=coverage/api --clean api/ shared/ mod.test.ts", + "test": "deno test -A api/ shared/", + "test:coverage": "deno test -A --coverage=coverage/api --clean api/ shared/", "test:web": "deno run -A npm:vitest run", "test:web:coverage": "deno run -A npm:vitest run --coverage", "doc": "deno doc --html --name=gerbeur --output=docs mod.ts $(find api shared src \\( -name '*.ts' -o -name '*.tsx' \\) -not -path '*/test/*' -not -path 'src/locales/*')", diff --git a/doc-lint.ts b/doc-lint.ts index 88316d28..a5452112 100644 --- a/doc-lint.ts +++ b/doc-lint.ts @@ -7,6 +7,10 @@ * whose type the module imports from a package is let through, and counted; * every other diagnostic fails the run. * + * It also checks what `deno doc --lint` doesn't: that every summary stands on + * its own. The reference shows a doc's first paragraph alone, in lists and + * above a symbol, so a first paragraph ending in a colon reads cut short. + * * @module */ @@ -62,8 +66,64 @@ function isPackageTypeRef(d: Diagnostic): boolean { importedFromPackage(d.file, type); } +/** Returns the first paragraph of `doc`, which the reference shows alone. */ +function summary(doc: string): string { + return doc.trim().split(/\n\s*\n|\n\s*(?:[-*]|\d+\.)\s|\n\s*```/)[0].trim(); +} + +/** + * Returns a diagnostic for every doc among `modules`, module docs included, + * whose summary ends in a colon. + */ +async function colonSummaries(modules: string[]): Promise { + const { stdout } = await new Deno.Command(Deno.execPath(), { + args: ["doc", "--json", ...modules], + stdout: "piped", + stderr: "null", + }).output(); + const { nodes } = JSON.parse(new TextDecoder().decode(stdout)) as { + nodes: Record; + }; + const found: Diagnostic[] = []; + const cwd = `file://${Deno.cwd()}/`; + function visit(file: string, name: string, value: unknown) { + if (Array.isArray(value)) { + for (const item of value) visit(file, name, item); + return; + } + if (typeof value !== "object" || value === null) return; + const node = value as Record; + const at = typeof node.name === "string" ? node.name : name; + for (const key of ["jsDoc", "module_doc"]) { + const doc = (node[key] as { doc?: string } | undefined)?.doc; + if (doc && summary(doc).endsWith(":")) { + const what = key === "module_doc" ? "module doc" : `doc of ${at}`; + const message = `the ${what} has a summary ending in a colon`; + found.push({ + rule: "colon-summary", + message, + file, + text: `error[colon-summary]: ${message}\n --> ${file}`, + }); + } + } + for (const [key, child] of Object.entries(node)) { + if (key !== "jsDoc" && key !== "module_doc" && key !== "imports") { + visit(file, at, child); + } + } + } + for (const [url, module] of Object.entries(nodes)) { + visit(url.replace(cwd, ""), "", module); + } + return found; +} + if (import.meta.main) { - const diagnostics = await lint(Deno.args); + const diagnostics = [ + ...await lint(Deno.args), + ...await colonSummaries(Deno.args), + ]; const failures = diagnostics.filter((d) => !isPackageTypeRef(d)); for (const d of failures) console.error(d.text.trimEnd() + "\n"); const allowed = diagnostics.length - failures.length; diff --git a/mod.test.ts b/mod.test.ts deleted file mode 100644 index e8c238d6..00000000 --- a/mod.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { assertEquals } from "@std/assert"; - -Deno.test("mod.ts, the docs' index page, links every documented module", async () => { - const doc = await Deno.readTextFile("mod.ts"); - const linked = new Set( - [...doc.matchAll(/\]\(\.\/(\S+?\.tsx?)\/index\.html\)/g)].map((m) => m[1]), - ); - const modules: string[] = []; - async function walk(dir: string) { - for await (const entry of Deno.readDir(dir)) { - const path = `${dir}/${entry.name}`; - if (entry.isDirectory) { - if (entry.name !== "test" && path !== "src/locales") await walk(path); - } else if (/\.tsx?$/.test(entry.name)) modules.push(path); - } - } - await walk("api"); - await walk("shared"); - await walk("src"); - assertEquals(modules.filter((m) => !linked.has(m)), []); - assertEquals([...linked].filter((m) => !modules.includes(m)), []); -}); diff --git a/mod.ts b/mod.ts index 001e4603..1d797e19 100644 --- a/mod.ts +++ b/mod.ts @@ -1,356 +1,61 @@ /** - * # gerbeur - * * A small invite-only social platform for sharing links and files. The * repository holds three parts: the backend in `api/`, the frontend in `src/`, * and in `shared/` whatever crosses the wire between them. This reference - * documents all three. + * documents all three; [every module](./all_symbols.html) is listed by path, + * so they come grouped by part and layer. * * ## Backend — `api/` * * An Oak server over SQLite that answers the API and the WebSocket, and serves - * the built frontend. A request goes through a route, which parses it and - * answers. The route calls services, which hold the logic and every SQL query; - * they read rows through the model, and reach other sites only through - * `remote/`. + * the built frontend. `api/main.ts` builds the application and listens. A + * request goes through a route, which parses it and answers. The route calls + * services, which hold the logic and every SQL query; they read rows through + * the model, and reach other sites only through `remote/`. Each folder holds + * one kind of module, and each file is named after its domain: * - * - **Entry point** — builds the application and listens: - * [main](./api/main.ts/index.html). - * - **Routes** — the HTTP API; each router's page lists its endpoints: - * [attachments](./api/routes/attachments.ts/index.html), - * [avatars](./api/routes/avatars.ts/index.html), - * [bandcamp](./api/routes/bandcamp.ts/index.html), - * [categories](./api/routes/categories.ts/index.html), - * [chat](./api/routes/chat.ts/index.html), - * [comments](./api/routes/comments.ts/index.html), - * [dumps](./api/routes/dumps.ts/index.html), - * [files](./api/routes/files.ts/index.html), - * [follows](./api/routes/follows.ts/index.html), - * [invites](./api/routes/invites.ts/index.html), - * [notifications](./api/routes/notifications.ts/index.html), - * [playlists](./api/routes/playlists.ts/index.html), - * [preview](./api/routes/preview.ts/index.html), - * [search](./api/routes/search.ts/index.html), - * [thumbnails](./api/routes/thumbnails.ts/index.html), - * [users](./api/routes/users.ts/index.html), - * [ws](./api/routes/ws.ts/index.html), - * [youtube](./api/routes/youtube.ts/index.html). - * - **Middleware** — what runs around every route: - * [auth](./api/middleware/auth.ts/index.html), - * [error](./api/middleware/error.ts/index.html), - * [og](./api/middleware/og.ts/index.html). - * - **Services** — the logic and every SQL query, one module per domain: - * [attachment-service](./api/services/attachment-service.ts/index.html), - * [backlink-service](./api/services/backlink-service.ts/index.html), - * [category-service](./api/services/category-service.ts/index.html), - * [chat-service](./api/services/chat-service.ts/index.html), - * [comment-like-service](./api/services/comment-like-service.ts/index.html), - * [comment-service](./api/services/comment-service.ts/index.html), - * [dump-service](./api/services/dump-service.ts/index.html), - * [email-service](./api/services/email-service.ts/index.html), - * [follow-service](./api/services/follow-service.ts/index.html), - * [invite-service](./api/services/invite-service.ts/index.html), - * [notification-service](./api/services/notification-service.ts/index.html), - * [password-reset-service](./api/services/password-reset-service.ts/index.html), - * [playlist-service](./api/services/playlist-service.ts/index.html), - * [rich-content-service](./api/services/rich-content-service.ts/index.html), - * [user-service](./api/services/user-service.ts/index.html), - * [vote-service](./api/services/vote-service.ts/index.html), - * [ws-service](./api/services/ws-service.ts/index.html). - * - **Model** — per table, its row type, guard and mappers: - * [auth](./api/model/auth.ts/index.html), - * [category](./api/model/category.ts/index.html), - * [chat](./api/model/chat.ts/index.html), - * [comment](./api/model/comment.ts/index.html), - * [dump](./api/model/dump.ts/index.html), - * [follow](./api/model/follow.ts/index.html), - * [invite](./api/model/invite.ts/index.html), - * [notification](./api/model/notification.ts/index.html), - * [password-reset](./api/model/password-reset.ts/index.html), - * [playlist](./api/model/playlist.ts/index.html), - * [user](./api/model/user.ts/index.html). - * - **Validators** — the request-body guards: - * [category](./api/validators/category.ts/index.html), - * [comment](./api/validators/comment.ts/index.html), - * [dump](./api/validators/dump.ts/index.html), - * [playlist](./api/validators/playlist.ts/index.html), - * [user](./api/validators/user.ts/index.html). - * - **Remote** — the only place that fetches from other sites: - * [bandcamp-streams](./api/remote/bandcamp-streams.ts/index.html), - * [bandcamp-tralbum](./api/remote/bandcamp-tralbum.ts/index.html), - * [fetch](./api/remote/fetch.ts/index.html), - * [html](./api/remote/html.ts/index.html), - * [image-proxy](./api/remote/image-proxy.ts/index.html), - * [public-address](./api/remote/public-address.ts/index.html), - * [twemoji](./api/remote/twemoji.ts/index.html), - * [youtube-playlist](./api/remote/youtube-playlist.ts/index.html). - * - **Link providers** — the previews of each kind of site: - * [bandcamp](./api/remote/providers/bandcamp.ts/index.html), - * [generic](./api/remote/providers/generic.ts/index.html), - * [provider](./api/remote/providers/provider.ts/index.html), - * [self](./api/remote/providers/self.ts/index.html), - * [soundcloud](./api/remote/providers/soundcloud.ts/index.html), - * [youtube](./api/remote/providers/youtube.ts/index.html). - * - **Lib** — helpers that belong to no layer: - * [api-exception](./api/lib/api-exception.ts/index.html), - * [auth](./api/lib/auth.ts/index.html), - * [canonical-url](./api/lib/canonical-url.ts/index.html), - * [index-html](./api/lib/index-html.ts/index.html), - * [jwt](./api/lib/jwt.ts/index.html), - * [pagination](./api/lib/pagination.ts/index.html), - * [permissions](./api/lib/permissions.ts/index.html), - * [playlist-members](./api/lib/playlist-members.ts/index.html), - * [rate-limit](./api/lib/rate-limit.ts/index.html), - * [reserved-slugs](./api/lib/reserved-slugs.ts/index.html), - * [self-url](./api/lib/self-url.ts/index.html), - * [serve-file](./api/lib/serve-file.ts/index.html), - * [site-icons](./api/lib/site-icons.ts/index.html), - * [slugify](./api/lib/slugify.ts/index.html), - * [static](./api/lib/static.ts/index.html), - * [upload](./api/lib/upload.ts/index.html). - * - **Database** — the handle, its creation, migration and startup - * housekeeping: [connection](./api/db/connection.ts/index.html), - * [init](./api/db/init.ts/index.html), - * [migrate](./api/db/migrate.ts/index.html), - * [startup](./api/db/startup.ts/index.html). - * - **Migrations** — applied in order, each once: - * [0001_comment_likes](./api/db/migrations/0001_comment_likes.ts/index.html), - * [0002_dump_thumbnail](./api/db/migrations/0002_dump_thumbnail.ts/index.html), - * [0003_dump_backlinks](./api/db/migrations/0003_dump_backlinks.ts/index.html), - * [0004_user_roles](./api/db/migrations/0004_user_roles.ts/index.html), - * [0005_drop_is_admin](./api/db/migrations/0005_drop_is_admin.ts/index.html), - * [0006_categories](./api/db/migrations/0006_categories.ts/index.html), - * [0007_password_reset_tokens](./api/db/migrations/0007_password_reset_tokens.ts/index.html), - * [0008_chat_messages](./api/db/migrations/0008_chat_messages.ts/index.html), - * [0009_chat_reply](./api/db/migrations/0009_chat_reply.ts/index.html), - * [0010_youtube_embed_start](./api/db/migrations/0010_youtube_embed_start.ts/index.html), - * [0011_split_favicon_thumbnail](./api/db/migrations/0011_split_favicon_thumbnail.ts/index.html), - * [0012_fix_favicon_reclassification](./api/db/migrations/0012_fix_favicon_reclassification.ts/index.html), - * [0013_dump_url_canonical](./api/db/migrations/0013_dump_url_canonical.ts/index.html), - * [0014_chat_read_state](./api/db/migrations/0014_chat_read_state.ts/index.html), - * [0015_token_revocation](./api/db/migrations/0015_token_revocation.ts/index.html), - * [0016_performance_indexes](./api/db/migrations/0016_performance_indexes.ts/index.html). - * - **Settings and scripts**: [config](./api/config.ts/index.html), - * [generate-icons](./api/scripts/generate-icons.ts/index.html). + * - `routes/` — the HTTP API. Each router's page lists its endpoints: method + * and path, who may call it, what it does. + * - `middleware/` — what runs around every route: auth, errors, OG tags. + * - `services/` — the logic and every SQL query, one module per domain. + * - `model/` — per table, its row type, guard and mappers. + * - `validators/` — the request-body guards. + * - `remote/` — the only place that fetches from other sites; its `providers/` + * build the previews of each kind of site. + * - `lib/` — helpers that belong to no layer. + * - `db/` — the handle, its creation, migration and startup housekeeping; + * `db/migrations/` are applied in order, each once. + * - `config.ts` — the settings read from the environment; `scripts/` — the + * icon generator. * * ## Shared — `shared/` * - * The types and constants both halves use: the API envelope, each domain's - * types and request bodies, the socket protocol, the validation limits and the - * role permissions. It imports nothing from either half. - * - * Modules: [api](./shared/api.ts/index.html), - * [category](./shared/category.ts/index.html), - * [chat](./shared/chat.ts/index.html), - * [comment](./shared/comment.ts/index.html), - * [dump](./shared/dump.ts/index.html), [follow](./shared/follow.ts/index.html), - * [mention](./shared/mention.ts/index.html), - * [notification](./shared/notification.ts/index.html), - * [permissions](./shared/permissions.ts/index.html), - * [player](./shared/player.ts/index.html), - * [playlist](./shared/playlist.ts/index.html), - * [search](./shared/search.ts/index.html), [user](./shared/user.ts/index.html), - * [validation](./shared/validation.ts/index.html), - * [ws](./shared/ws.ts/index.html). + * The types and constants both halves use, one module per domain: the API + * envelope, each domain's types and request bodies, the socket protocol, the + * validation limits and the role permissions. It imports nothing from either + * half. The server sends `Date`s; the client reads the same types through + * `Wire`, with every `Date` an ISO string. * * ## Frontend — `src/` * - * The React app, built with Vite and translated with Lingui. Each layer holds - * one kind of module, and every layer but pages/ is split into the same domains - * (auth, category, chat, comment, dump, editor, feed, form, layout, player, - * playlist, profile, social, theme, ws, and shared for what belongs to no one - * domain). A page reads the API and the socket through contexts and hooks, and + * The React app, built with Vite and translated with Lingui. `main.tsx` mounts + * it, `App.tsx` holds the providers and the route table, `i18n.ts` the + * language. A page reads the API and the socket through contexts and hooks, and * draws with components; utils hold what isn't React at all. * - * - **Entry point and app** — mounting, the providers and the route table, the - * language: [App](./src/App.tsx/index.html), - * [i18n](./src/i18n.ts/index.html), [main](./src/main.tsx/index.html). - * - **Pages** — one component per route, each naming it, and the two guards - * that wrap the routes for the signed in and the signed out: - * [Dump](./src/pages/Dump.tsx/index.html), - * [DumpEdit](./src/pages/DumpEdit.tsx/index.html), - * [Index](./src/pages/Index.tsx/index.html), - * [NotFound](./src/pages/NotFound.tsx/index.html), - * [Notifications](./src/pages/Notifications.tsx/index.html), - * [PlaylistDetail](./src/pages/PlaylistDetail.tsx/index.html), - * [ResetPassword](./src/pages/ResetPassword.tsx/index.html), - * [RestrictedGuest](./src/pages/RestrictedGuest.tsx/index.html), - * [RestrictedLoggedIn](./src/pages/RestrictedLoggedIn.tsx/index.html), - * [Search](./src/pages/Search.tsx/index.html), - * [UserDumps](./src/pages/UserDumps.tsx/index.html), - * [UserLogin](./src/pages/UserLogin.tsx/index.html), - * [UserPlaylists](./src/pages/UserPlaylists.tsx/index.html), - * [UserPublicProfile](./src/pages/UserPublicProfile.tsx/index.html), - * [UserRegister](./src/pages/UserRegister.tsx/index.html), - * [UserUpvoted](./src/pages/UserUpvoted.tsx/index.html). - * - **Config** — settings read at startup: - * [api](./src/config/api.ts/index.html), - * [feedTabs](./src/config/feedTabs.ts/index.html), - * [playerMode](./src/config/playerMode.ts/index.html). - * - **Contexts** — a FooContext holding the state and its FooProvider, by - * domain: - * - auth: [AuthContext](./src/contexts/auth/AuthContext.ts/index.html), - * [AuthProvider](./src/contexts/auth/AuthProvider.tsx/index.html). - * - category: - * [CategoriesContext](./src/contexts/category/CategoriesContext.ts/index.html), - * [CategoriesProvider](./src/contexts/category/CategoriesProvider.tsx/index.html). - * - chat: [ChatContext](./src/contexts/chat/ChatContext.ts/index.html), - * [ChatProvider](./src/contexts/chat/ChatProvider.tsx/index.html). - * - player: - * [PlayerContext](./src/contexts/player/PlayerContext.ts/index.html), - * [PlayerProvider](./src/contexts/player/PlayerProvider.tsx/index.html). - * - social: - * [FollowContext](./src/contexts/social/FollowContext.ts/index.html), - * [FollowProvider](./src/contexts/social/FollowProvider.tsx/index.html). - * - theme: [ThemeContext](./src/contexts/theme/ThemeContext.ts/index.html), - * [ThemeProvider](./src/contexts/theme/ThemeProvider.tsx/index.html). - * - ws: - * [AvatarOverrideContext](./src/contexts/ws/AvatarOverrideContext.ts/index.html), - * [WSContext](./src/contexts/ws/WSContext.ts/index.html), - * [WSProvider](./src/contexts/ws/WSProvider.tsx/index.html). - * - **Hooks** — every hook, by domain: - * - auth: [useAuth](./src/hooks/auth/useAuth.ts/index.html). - * - category: - * [useCategories](./src/hooks/category/useCategories.ts/index.html). - * - chat: [useChat](./src/hooks/chat/useChat.ts/index.html). - * - dump: [useDumpCardLink](./src/hooks/dump/useDumpCardLink.ts/index.html), - * [useDumpListSync](./src/hooks/dump/useDumpListSync.ts/index.html), - * [useUserDumpFeed](./src/hooks/dump/useUserDumpFeed.ts/index.html). - * - editor: - * [useEmojiTrigger](./src/hooks/editor/useEmojiTrigger.ts/index.html), - * [useMentionAutocomplete](./src/hooks/editor/useMentionAutocomplete.ts/index.html). - * - feed: - * [useDefaultFeedTab](./src/hooks/feed/useDefaultFeedTab.ts/index.html), - * [useFading](./src/hooks/feed/useFading.ts/index.html), - * [useFeedCache](./src/hooks/feed/useFeedCache.ts/index.html), - * [useInfiniteScroll](./src/hooks/feed/useInfiniteScroll.ts/index.html), - * [usePositionAwareSync](./src/hooks/feed/usePositionAwareSync.ts/index.html), - * [useScrollSave](./src/hooks/feed/useScrollSave.ts/index.html). - * - form: [useApiForm](./src/hooks/form/useApiForm.ts/index.html). - * - player: - * [usePlayRichContent](./src/hooks/player/usePlayRichContent.ts/index.html), - * [useYouTubePlaylist](./src/hooks/player/useYouTubePlaylist.ts/index.html). - * - playlist: - * [usePlaylistListSync](./src/hooks/playlist/usePlaylistListSync.ts/index.html). - * - shared: - * [useAnchoredFloat](./src/hooks/shared/useAnchoredFloat.ts/index.html), - * [useDocumentTitle](./src/hooks/shared/useDocumentTitle.ts/index.html), - * [useHeightTransition](./src/hooks/shared/useHeightTransition.ts/index.html), - * [useRoomBeside](./src/hooks/shared/useRoomBeside.ts/index.html), - * [useScrollLock](./src/hooks/shared/useScrollLock.ts/index.html), - * [useSoundsEnabled](./src/hooks/shared/useSoundsEnabled.ts/index.html), - * [useTabParam](./src/hooks/shared/useTabParam.ts/index.html). - * - social: [useFollows](./src/hooks/social/useFollows.ts/index.html). - * - theme: [useTheme](./src/hooks/theme/useTheme.ts/index.html). - * - ws: [useWS](./src/hooks/ws/useWS.ts/index.html). - * - **Components** — only components, by domain: - * - category: - * [CategoryManager](./src/components/category/CategoryManager.tsx/index.html), - * [CategorySelect](./src/components/category/CategorySelect.tsx/index.html), - * [CategorySwitcher](./src/components/category/CategorySwitcher.tsx/index.html). - * - chat: [ChatEditBox](./src/components/chat/ChatEditBox.tsx/index.html), - * [ChatMessageItem](./src/components/chat/ChatMessageItem.tsx/index.html), - * [ChatModal](./src/components/chat/ChatModal.tsx/index.html), - * [PendingItem](./src/components/chat/PendingItem.tsx/index.html), - * [ReplyRef](./src/components/chat/ReplyRef.tsx/index.html). - * - comment: - * [CommentForm](./src/components/comment/CommentForm.tsx/index.html), - * [CommentNode](./src/components/comment/CommentNode.tsx/index.html), - * [CommentThread](./src/components/comment/CommentThread.tsx/index.html). - * - dump: [DumpCard](./src/components/dump/DumpCard.tsx/index.html), - * [FilePreview](./src/components/dump/FilePreview.tsx/index.html), - * [JournalCard](./src/components/dump/JournalCard.tsx/index.html), - * [RichContentCard](./src/components/dump/RichContentCard.tsx/index.html), - * [DumpCreateModal](./src/components/dump/create/DumpCreateModal.tsx/index.html), - * [DuplicateNotice](./src/components/dump/create/DuplicateNotice.tsx/index.html), - * [LocalFilePreview](./src/components/dump/create/LocalFilePreview.tsx/index.html), - * [StepIndicator](./src/components/dump/create/StepIndicator.tsx/index.html), - * [TitleField](./src/components/dump/create/TitleField.tsx/index.html). - * - editor: - * [EmojiPickerPopup](./src/components/editor/EmojiPickerPopup.tsx/index.html), - * [Markdown](./src/components/editor/Markdown.tsx/index.html), - * [MentionDropdown](./src/components/editor/MentionDropdown.tsx/index.html), - * [TextEditor](./src/components/editor/TextEditor.tsx/index.html). - * - feed: [DumpFeed](./src/components/feed/DumpFeed.tsx/index.html), - * [FeedTabBar](./src/components/feed/FeedTabBar.tsx/index.html), - * [FollowedFeed](./src/components/feed/FollowedFeed.tsx/index.html), - * [JournalFeed](./src/components/feed/JournalFeed.tsx/index.html), - * [PresenceRow](./src/components/feed/PresenceRow.tsx/index.html). - * - form: [FileDropZone](./src/components/form/FileDropZone.tsx/index.html), - * [FileField](./src/components/form/FileField.tsx/index.html), - * [FormActions](./src/components/form/FormActions.tsx/index.html), - * [FormError](./src/components/form/FormError.tsx/index.html), - * [RichTextField](./src/components/form/RichTextField.tsx/index.html), - * [SegmentedField](./src/components/form/SegmentedField.tsx/index.html), - * [SubmitButton](./src/components/form/SubmitButton.tsx/index.html), - * [TextField](./src/components/form/TextField.tsx/index.html). - * - layout: [AppHeader](./src/components/layout/AppHeader.tsx/index.html), - * [ChatButton](./src/components/layout/ChatButton.tsx/index.html), - * [ChatFab](./src/components/layout/ChatFab.tsx/index.html), - * [DumpFab](./src/components/layout/DumpFab.tsx/index.html), - * [NotificationBell](./src/components/layout/NotificationBell.tsx/index.html), - * [PageError](./src/components/layout/PageError.tsx/index.html), - * [PageShell](./src/components/layout/PageShell.tsx/index.html), - * [SearchBar](./src/components/layout/SearchBar.tsx/index.html), - * [UserMenu](./src/components/layout/UserMenu.tsx/index.html). - * - player: - * [GlobalPlayer](./src/components/player/GlobalPlayer.tsx/index.html), - * [MediaPlayer](./src/components/player/MediaPlayer.tsx/index.html), - * [PlayerIcons](./src/components/player/PlayerIcons.tsx/index.html), - * [SeekBar](./src/components/player/SeekBar.tsx/index.html), - * [Waveform](./src/components/player/Waveform.tsx/index.html). - * - playlist: - * [AddToPlaylistModal](./src/components/playlist/AddToPlaylistModal.tsx/index.html), - * [NewPlaylistButton](./src/components/playlist/NewPlaylistButton.tsx/index.html), - * [PlaylistCard](./src/components/playlist/PlaylistCard.tsx/index.html), - * [PlaylistCreateForm](./src/components/playlist/PlaylistCreateForm.tsx/index.html), - * [PlaylistMembershipPanel](./src/components/playlist/PlaylistMembershipPanel.tsx/index.html). - * - profile: - * [ChangePasswordModal](./src/components/profile/ChangePasswordModal.tsx/index.html), - * [ProfileSubpageHeader](./src/components/profile/ProfileSubpageHeader.tsx/index.html). - * - shared: [Avatar](./src/components/shared/Avatar.tsx/index.html), - * [ConfirmModal](./src/components/shared/ConfirmModal.tsx/index.html), - * [CountedInput](./src/components/shared/CountedInput.tsx/index.html), - * [ErrorCard](./src/components/shared/ErrorCard.tsx/index.html), - * [ImagePicker](./src/components/shared/ImagePicker.tsx/index.html), - * [Modal](./src/components/shared/Modal.tsx/index.html), - * [TabBar](./src/components/shared/TabBar.tsx/index.html), - * [Thumbnail](./src/components/shared/Thumbnail.tsx/index.html), - * [ThumbnailPlaceholder](./src/components/shared/ThumbnailPlaceholder.tsx/index.html), - * [Tooltip](./src/components/shared/Tooltip.tsx/index.html). - * - social: - * [FollowButton](./src/components/social/FollowButton.tsx/index.html), - * [LikeButton](./src/components/social/LikeButton.tsx/index.html), - * [UserListPopover](./src/components/social/UserListPopover.tsx/index.html), - * [VoteButton](./src/components/social/VoteButton.tsx/index.html). - * - **Utils** — every module that isn't React, by domain: - * - auth: [permissions](./src/utils/auth/permissions.ts/index.html), - * [storage](./src/utils/auth/storage.ts/index.html). - * - chat: [draft](./src/utils/chat/draft.ts/index.html), - * [message](./src/utils/chat/message.ts/index.html), - * [text](./src/utils/chat/text.ts/index.html), - * [time](./src/utils/chat/time.ts/index.html). - * - dump: [draft](./src/utils/dump/draft.ts/index.html). - * - editor: [skinTone](./src/utils/editor/skinTone.ts/index.html), - * [text](./src/utils/editor/text.ts/index.html). - * - feed: [hotScore](./src/utils/feed/hotScore.ts/index.html), - * [journalLayout](./src/utils/feed/journalLayout.ts/index.html). - * - player: [bandcamp](./src/utils/player/bandcamp.ts/index.html), - * [duration](./src/utils/player/duration.ts/index.html), - * [streamSources](./src/utils/player/streamSources.ts/index.html), - * [volume](./src/utils/player/volume.ts/index.html), - * [waveform](./src/utils/player/waveform.ts/index.html), - * [youtube](./src/utils/player/youtube.ts/index.html). - * - shared: [api](./src/utils/shared/api.ts/index.html), - * [charCount](./src/utils/shared/charCount.ts/index.html), - * [deserialize](./src/utils/shared/deserialize.ts/index.html), - * [format](./src/utils/shared/format.ts/index.html), - * [proxyImage](./src/utils/shared/proxyImage.ts/index.html), - * [relativeTime](./src/utils/shared/relativeTime.ts/index.html), - * [sound](./src/utils/shared/sound.ts/index.html), - * [thumbnailTint](./src/utils/shared/thumbnailTint.ts/index.html), - * [urls](./src/utils/shared/urls.ts/index.html), - * [visited](./src/utils/shared/visited.ts/index.html). + * - `pages/` — one component per route, each naming it, and the two guards + * that wrap the routes for the signed in and the signed out. + * - `config/` — settings read at startup. + * - `contexts/` — a `FooContext` holding the state and its `FooProvider`. + * - `hooks/` — every hook. + * - `components/` — only components. + * - `utils/` — every module that isn't React. + * + * Every layer but `pages/` and `config/` is split into the same domains (auth, + * category, chat, comment, dump, editor, feed, form, layout, player, playlist, + * profile, social, theme, ws, and shared for what belongs to no one domain), so + * the chat lives in `components/chat/`, `contexts/chat/`, `hooks/chat/` and + * `utils/chat/`. * * @module */ diff --git a/src/hooks/dump/useDumpListSync.ts b/src/hooks/dump/useDumpListSync.ts index bcfe665f..a64960cc 100644 --- a/src/hooks/dump/useDumpListSync.ts +++ b/src/hooks/dump/useDumpListSync.ts @@ -25,7 +25,7 @@ export interface DumpListSyncOptions { } /** - * Keeps the dump list set through `setDumps` in sync with real-time WS events: + * Keeps the dump list set through `setDumps` in sync with real-time WS events. * * - a dump deleted → filtered out * - a dump updated → updated in place, or removed if now private diff --git a/src/hooks/playlist/usePlaylistListSync.ts b/src/hooks/playlist/usePlaylistListSync.ts index 00d38257..ee867821 100644 --- a/src/hooks/playlist/usePlaylistListSync.ts +++ b/src/hooks/playlist/usePlaylistListSync.ts @@ -32,7 +32,7 @@ export interface PlaylistListSyncOptions { /** * Keeps the playlist list set through `setPlaylists` in sync with real-time WS - * events: + * events. * * - a playlist created → prepended (unless `noNewEntries`) * - a playlist updated → updated in place, or removed if now private