Compare commits

...

2 Commits

Author SHA1 Message Date
khannurien
f3f07ed1e3 v3: every doc opens with the role, then the mechanism, then the details — the docs were written bottom-up: what a thing does first ("Declares the three contexts WSProvider fills…"), details that only make sense once its purpose is known next, and the reason it exists last ("…so that a feed doesn't re-render"), so a reader had nothing to hang the first sentences on. The README's rule caused it, since a module doc "opened with what the module does"; it now opens with the module's role in the app, and the value-side phrasing of a type ("Returned to Y by X") comes once the role is said, with WSContext as the before and after. WSContext.ts is rewritten that way throughout, and so are the docs elsewhere that buried their purpose in a trailing "so that" (the migrations, og, image-proxy, public-address, useFading, useFeedCache, the drafts), opened on a generic verb that hid it ("Holds", "Declares", "Loads", "Defines", "Types": the contexts and providers, the shared types, the models, the hooks), or were label fragments ("The page of a dump…", "A row of tabs."). A function whose verb is its action ("Stores session"), a props or result type, and a protocol message ("Sent by X when Y") already say their role and are left as they are. Only comments changed, and the catalogs only move their references; deno fmt, deno lint and doc:lint are clean.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 49s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:42:08 +00:00
khannurien
2af64721c7 v3: the invite tree spaces every row the same — a child sat 0.2rem under its parent but siblings 0.4rem apart, since each row's spacing was split into top and bottom padding and a last child dropped its bottom half. Every row now has one gap above it (--invite-tree-gap), the connectors run through it from the same variable, and the user link is a block, so a theme's line height can't push the avatar off the connector's center.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:28:43 +00:00
106 changed files with 483 additions and 395 deletions

View File

@@ -188,9 +188,20 @@ one of:
Add an example only when the call isn't obvious.
3. **Place in the chain**: where its input comes from and where its output goes.
A type's doc is phrased from the value's side, naming the producer and the
consumer: "Returned to Y by X when Z", "Passed to Y by X". It is never "What X
returns to Y" or "What X passes to Y".
**A doc opens with the role, then the mechanism, then the details.** Its first
sentence says what part of the app the thing serves or what problem it solves,
so a reader who has never seen it knows why it exists. How it does that (what
feeds it, what reads it, how it's split) comes next, and the details last. Each
sentence makes sense given only the ones before it. Not "Declares the three
contexts `WSProvider` fills from the WebSocket. They are split so that a feed
doesn't re-render with every chat message", which leaves the reason for last,
but "Live updates from the server reach the pages through three contexts, split
so that a feed doesn't re-render with every chat message. `WSProvider` fills
them from its WebSocket."
Once the role is said, a type's doc is phrased from the value's side, naming the
producer and the consumer: "Returned to Y by X when Z", "Passed to Y by X". It
is never "What X returns to Y" or "What X passes to Y".
Don't pack a doc into a label followed by a colon and a list ("X: one thing,
plus some other thing"). Write sentences with a subject and a verb.
@@ -209,13 +220,13 @@ and locals. None of these count as a doc:
When a comment is there, it is as short as it can be, often one sentence, and it
is written in full sentences. Labels and fragments ("Upvotes on dumps.") don't
count. A module doc is a few lines at most. It opens with what the module does,
then says who calls it and what it hands back, and it doesn't repeat what its
exports' docs say. `@throws` stays on whatever throws to its caller, with the
statuses of an `APIException` and when each happens. A router's module doc lists
its endpoints (method, path, who may call it), since the code states them
nowhere in one place. A summary never ends in a colon: the reference shows it
alone.
count. A module doc is a few lines at most. It opens with the module's role in
the app, then says how it plays it, who calls it and what it hands back, and it
doesn't repeat what its exports' docs say. `@throws` stays on whatever throws to
its caller, with the statuses of an `APIException` and when each happens. A
router's module doc lists its endpoints (method, path, who may call it), since
the code states them nowhere in one place. A summary never ends in a colon: the
reference shows it alone.
**`//` comments** go wherever the code isn't obvious. They explain why, not
what: a workaround, an ordering constraint, a surprising limitation, a security

View File

@@ -1,7 +1,8 @@
/**
* Reads the server's settings from the `GERBEUR_*` environment variables, once
* at import, and holds the fixed limits the backend applies. The deno tasks
* load `.env` with `--env-file`, so changing a setting needs a restart.
* Lets a deployment configure the server through the `GERBEUR_*` environment
* variables, which it reads once at import, and holds the fixed limits the
* backend applies. The deno tasks load `.env` with `--env-file`, so changing a
* setting needs a restart.
*
* @module
*/
@@ -55,10 +56,10 @@ export const PUBLIC_URL: string =
Deno.env.get("GERBEUR_PUBLIC_URL")?.trim().replace(/\/$/, "") ||
`http://localhost:${PORT}`;
/**
* Holds the URL the frontend is served from (`GERBEUR_FRONTEND_URL`), without
* a trailing slash, when it isn't {@link PUBLIC_URL}, as with a separate Vite
* server. It is an allowed origin, and password reset links and the Open Graph
* tags' page URLs point to it.
* Points password reset links and the Open Graph tags' page URLs at the
* frontend when it isn't served from {@link PUBLIC_URL}, as with a separate
* Vite server, and allows it as an origin. It is `GERBEUR_FRONTEND_URL`,
* without a trailing slash.
*/
export const FRONTEND_URL: string =
Deno.env.get("GERBEUR_FRONTEND_URL")?.trim().replace(/\/$/, "") ||

View File

@@ -1,7 +1,7 @@
/**
* Adds `dumps.custom_thumbnail_mime`, so a poster can upload a thumbnail that
* replaces the fetched one. It holds the uploaded image's type, and NULL means
* the dump has none.
* Lets a poster upload a thumbnail that replaces the fetched one. It adds
* `dumps.custom_thumbnail_mime`, which holds the uploaded image's type; NULL
* means the dump has none.
*
* @module
*/

View File

@@ -1,8 +1,8 @@
/**
* Adds `dump_backlinks`, which records the links from a dump's note or
* comments to other dumps, so a dump page can list the dumps related to it.
* backlink-service fills it, and the index on `dumps.url` lets it find the
* dumps an external link was posted as.
* Lets a dump page list the dumps related to it. It adds `dump_backlinks`,
* which records the links from a dump's note or comments to other dumps and
* which backlink-service fills, and an index on `dumps.url` to find the dumps
* an external link was posted as.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Adds `users.role`, which replaces the `is_admin` flag so that moderators can
* exist between users and admins. 0005 then drops the flag.
* Makes room for moderators between users and admins. It adds `users.role`,
* which replaces the `is_admin` flag, and 0005 then drops the flag.
*
* @module
*/

View File

@@ -1,8 +1,8 @@
/**
* Adds `chat_messages.reply_to_id`, so a message can reply to another. It has
* no foreign key on purpose: when the replied-to message is deleted, the id
* stays and chat-service's join reads null, so the chat shows "deleted
* message" where the quote was.
* Lets a chat message reply to another, through `chat_messages.reply_to_id`.
* The column has no foreign key on purpose: when the replied-to message is
* deleted, the id stays and chat-service's join reads null, so the chat shows
* "deleted message" where the quote was.
*
* @module
*/

View File

@@ -1,8 +1,8 @@
/**
* Adds the `start` parameter to the YouTube embed URLs stored before the
* YouTube provider carried it over. `rich_content.embedUrl` is computed when a
* dump is posted, and used to drop the source URL's `t` (`?t=1265`,
* `?t=1h2m3s`), so those videos played from 0:00.
* Makes the YouTube dumps posted with a start time, before the YouTube provider
* kept it, start there instead of at 0:00. `rich_content.embedUrl` is computed
* when a dump is posted, and it used to drop the source URL's `t` (`?t=1265`,
* `?t=1h2m3s`); this adds the `start` parameter back to the stored embed URLs.
*
* It reads the time from the stored source URL and fetches nothing. It keeps
* its own copy of the parsing, so that it migrates every database alike should

View File

@@ -1,10 +1,11 @@
/**
* Moves site icons out of `rich_content.thumbnailUrl` into the new
* `faviconUrl`. The metadata fetch used to fall back on the page's icon, and
* then on a guessed `/favicon.ico`, so most thumbnails were a 16×16 icon, or a
* 404, that the UI stretched over a 128×72 box. It now stops at real artwork,
* and a missing `thumbnailUrl` means none, which the frontend draws a
* placeholder for. This gives the older rows that meaning.
* Stops older dumps from showing a site's icon as their thumbnail. The metadata
* fetch used to fall back on the page's icon, and then on a guessed
* `/favicon.ico`, so most thumbnails were a 16×16 icon, or a 404, that the UI
* stretched over a 128×72 box. It now stops at real artwork, and a missing
* `thumbnailUrl` means none, which the frontend draws a placeholder for. This
* gives the older rows that meaning, by moving their icons out of
* `rich_content.thumbnailUrl` into the new `faviconUrl`.
*
* It judges the stored URL alone and fetches nothing, so it doesn't fill
* `accentColor`. The frontend derives a hue from the hostname when it is

View File

@@ -1,6 +1,7 @@
/**
* Adds `dumps.url_canonical`, the key the create form's "already dumped" hint
* looks a URL up by, and fills it for every URL dump already posted.
* Lets the create form warn that a link was already dumped. It adds
* `dumps.url_canonical`, the key the hint looks a URL up by, and fills it for
* every URL dump already posted.
*
* It fills the key from each stored URL and fetches nothing. Its helpers are a
* copy of lib/canonical-url.ts as it stood when this shipped: importing the

View File

@@ -1,7 +1,7 @@
/**
* Adds `users.tokens_valid_after`, so that changing a password ends every
* session opened before. Tokens are stateless JWTs, so lib/auth.ts refuses a
* token issued before that time instead.
* Lets a password change end every session opened before it. Tokens are
* stateless JWTs, so this adds `users.tokens_valid_after`, and lib/auth.ts
* refuses a token issued before that time.
*
* @module
*/

View File

@@ -1,8 +1,10 @@
/**
* Adds `attachments.resource_type`, the kind of thing an attachment's
* `resource_id` names. Without it, an attachment couldn't tell whose it was:
* one pasted into a private dump was served to anyone with its URL, and one
* whose dump, comment or message was deleted was kept for good.
* Serves an attachment only to those who may see what it was pasted into, and
* deletes it once that is gone. It adds `attachments.resource_type`, the kind
* of thing an attachment's `resource_id` names. Without it, an attachment
* couldn't tell whose it was: one pasted into a private dump was served to
* anyone with its URL, and one whose dump, comment or message was deleted was
* kept for good.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Defines the exception that carries an HTTP error from any layer up to the
* error middleware.
* Lets any layer fail a request with an HTTP status, which the error middleware
* turns into the response.
*
* @module
*/

View File

@@ -1,9 +1,10 @@
/**
* Reduces a URL to a key shared by every link to the same page or video.
* `dump-service` stores it in `dumps.url_canonical` when a
* dump is posted or edited, and `GET /api/dumps/by-url` matches on it
* to warn the create form of a repost. The key is never fetched or shown, so it
* may be lossy: it forces `https`, drops `www.`, tracking parameters and
* Spots reposts and links to known dumps, by reducing a URL to a key shared by
* every link to the same page or video. `dump-service` stores it in
* `dumps.url_canonical` when a dump is posted or edited, `GET
* /api/dumps/by-url` matches on it to warn the create form of a repost, and
* backlink-service compares links by it. The key is never fetched or shown, so
* it may be lossy: it forces `https`, drops `www.`, tracking parameters and
* timestamps.
*
* Migration 0013 backfilled existing rows with a frozen copy of these rules.

View File

@@ -1,7 +1,7 @@
/**
* Reads the page a list route's query string asks for, and cuts the rows a
* service fetched into that page. The routes send the page as
* `PaginatedData`.
* Pages every list route the same way. It reads the page a list route's query
* string asks for, and cuts the rows a service fetched into that page, which
* the routes send as `PaginatedData`.
*
* @module
*/

View File

@@ -1,8 +1,8 @@
/**
* Answers a link to a dump, a profile or a public playlist with `index.html`
* carrying that page's Open Graph and Twitter card tags, for the link previews
* of chat apps and social sites, which don't run the app. main.ts installs it
* before the routers.
* Gives links to the site rich previews in chat apps and social sites, which
* don't run the app. It answers a link to a dump, a profile or a public
* playlist with `index.html` carrying that page's Open Graph and Twitter card
* tags. main.ts installs it before the routers.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Types the payloads of session and password reset tokens, and guards them.
* `lib/jwt.ts` signs the tokens and runs the guards when it verifies one.
* Checks what a verified token says before it is trusted. It types the payloads
* of session and password reset tokens, and `lib/jwt.ts` runs their guards when
* it verifies one.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Types the rows the category service reads from `categories`, and turns them
* into the {@link Category} the category routes send.
* Turns the rows the category service reads from `categories` into the {@link
* Category} the category routes send.
*
* @module
*/

View File

@@ -1,5 +1,5 @@
/**
* Maps the rows of `chat_messages` to the {@link ChatMessage} the API sends.
* Turns the rows of `chat_messages` into the {@link ChatMessage} the API sends.
* Only `services/chat-service.ts` uses it, on the rows of its one query.
*
* @module

View File

@@ -1,6 +1,6 @@
/**
* Types the rows the comment service reads from `comments`, and turns them into
* the {@link Comment} the comment routes send.
* Turns the rows the comment service reads from `comments` into the {@link
* Comment} the comment routes send.
*
* @module
*/

View File

@@ -1,7 +1,8 @@
/**
* Maps the `dumps` table to the API's {@link Dump}. Every service that reads
* dumps selects them with the column lists here, checks each row with
* {@link isDumpRow} and converts it with {@link dumpRowToApi}.
* Turns the rows of `dumps` into the API's {@link Dump} the same way for every
* service that reads dumps: each selects them with the column lists here,
* checks each row with {@link isDumpRow} and converts it with {@link
* dumpRowToApi}.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Types and guards the rows of `follows`, which `follow-service.ts` reads to
* answer what a user follows.
* Checks the rows `follow-service.ts` reads from `follows` to answer what a
* user follows.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Types and guards the rows of `invites`, which `invite-service.ts` reads to
* check an invite before sign-up.
* Checks the rows `invite-service.ts` reads from `invites` to vet an invite
* before sign-up.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Types and guards the rows of `notifications`, and converts them to the
* {@link Notification} that `GET /api/notifications` answers.
* Turns the rows of `notifications`, once checked, into the {@link
* Notification} that `GET /api/notifications` answers.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Types and guards the rows of `password_reset_tokens`, which
* `password-reset-service.ts` reads to make each reset link work once.
* Checks the rows `password-reset-service.ts` reads from
* `password_reset_tokens` to make each reset link work once.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Types the rows the playlist and follow services read from `playlists`, and
* turns them into the {@link Playlist} the API sends.
* Turns the rows the playlist and follow services read from `playlists` into
* the {@link Playlist} the API sends.
*
* @module
*/

View File

@@ -1,7 +1,6 @@
/**
* Checks the rows of a list query, for the services. Every service answers a
* row that doesn't have its table's shape with a 500 rather than leaving it
* out, so that damage shows instead of making items silently vanish.
* Makes a damaged row fail its query with a 500 rather than silently vanish
* from a list. Every service checks the rows of its list queries with it.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Types and guards the rows of `users`, and converts them to the {@link User}
* that `user-service.ts` returns. A route turns a `User` into what it may send
* with {@link toPublicUser} or {@link toSelfUser}.
* Turns the rows of `users`, once checked, into the {@link User} that
* `user-service.ts` returns. A route turns a `User` into what it may send with
* {@link toPublicUser} or {@link toSelfUser}.
*
* @module
*/

View File

@@ -1,8 +1,9 @@
/**
* Fetches and caches the remote images `GET /api/proxy-image` serves. The
* frontend routes a thumbnail through it when the image is on plain http,
* which an https page would block, or when loading it directly failed, often
* to hotlink protection.
* Shows the thumbnails a browser can't load from their own site: those on plain
* http, which an https page would block, and those behind hotlink protection.
* It fetches and caches the images `GET /api/proxy-image` serves, which the
* frontend routes a thumbnail through when it is on http or when loading it
* directly failed.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Declares what a link provider implements. `rich-content-service.ts` asks its
* providers in turn and fetches a link's preview from the first that matches.
* Lets each kind of site have its own link previews. A provider implements this
* interface, and `rich-content-service.ts` asks its providers in turn and
* fetches a link's preview from the first that matches.
*
* @module
*/

View File

@@ -1,8 +1,8 @@
/**
* Tells private addresses from public ones, so that a user-supplied URL can't
* make the server fetch what only it can reach: loopback, the LAN, container
* networks or the cloud metadata endpoint at 169.254.169.254. `fetch.ts` checks
* every hop with it.
* Keeps a user-supplied URL from making the server fetch what only it can
* reach: loopback, the LAN, container networks or the cloud metadata endpoint
* at 169.254.169.254. It tells private addresses from public ones, and
* `fetch.ts` checks every hop with it.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Resolves a YouTube playlist to its entries' titles and lengths, for
* `GET /api/youtube/playlist`. The embed plays the list but doesn't expose
* them, and the global player needs them to draw a tracklist.
* Gives the global player a tracklist for a YouTube playlist, which the embed
* plays but doesn't list. It resolves the playlist to its entries' titles and
* lengths, for `GET /api/youtube/playlist`.
*
* They are read from the `ytInitialData` JSON of the public playlist page,
* whose undocumented shape changes, so both the current `lockupViewModel` rows

View File

@@ -1,7 +1,7 @@
/**
* Serves the avatar endpoints. The profile page uploads the avatar, and the
* client's `Avatar` component loads it, with the user's `updatedAt` as a
* cache-busting version.
* Stores and serves the users' avatars. The profile page uploads the avatar,
* and the client's `Avatar` component loads it, with the user's `updatedAt` as
* a cache-busting version.
*
* - `POST /api/avatars/me` — signed in. Replaces the caller's avatar (multipart
* `file`, at most 5 MB), sends `user_updated` to everyone, and answers the

View File

@@ -1,5 +1,6 @@
/**
* The category routes.
* Serves the categories dumps are filed under and their feeds, and changes them
* for those with `category:manage`.
*
* - `GET /api/categories` — public. Answers every category, in order.
* - `GET /api/categories/:slug` — public. Answers one category.

View File

@@ -1,6 +1,6 @@
/**
* Serves the invite endpoints. Any signed-in user can invite someone, from
* their profile page, and the invite link opens the `/register` page.
* Lets any signed-in user invite someone, from their profile page. The invite
* link opens the `/register` page.
*
* - `POST /api/invites` — signed in. Makes an invite and answers its token.
* - `GET /api/invites/:token` — public. Checks that an invite still works,

View File

@@ -31,8 +31,8 @@ import { authenticateMedia } from "../lib/auth.ts";
const router: Router = new Router({ prefix: "/api/thumbnails" });
/**
* Holds whether ffmpeg runs, probed once per process: probing on every uncached
* thumbnail request cost a process per card.
* Spares a process per uncached thumbnail request: whether ffmpeg runs is
* probed once per process, and kept here.
*/
let ffmpegProbe: Promise<boolean> | undefined;
@@ -146,8 +146,8 @@ function imagePreview(srcPath: string, outPath: string): Promise<void> {
}
/**
* Holds the generations under way, by output path. A feed full of cards asking
* for the same new still at once then waits on a single ffmpeg run.
* Lets a feed full of cards asking for the same new still at once wait on a
* single ffmpeg run. It holds the generations under way, by output path.
*/
const inFlight = new Map<string, Promise<void>>();

View File

@@ -1,7 +1,8 @@
/**
* Records the images pasted into Markdown bodies, and binds each one to the
* resource whose body links it. `routes/attachments.ts` stores and serves the
* files; the services that save a body call {@link linkAttachments}.
* Ties each image pasted into a Markdown body to the dump, comment, playlist or
* message that links it, so the image is served only to those who may see that,
* and purged at startup once it is gone. `routes/attachments.ts` stores and
* serves the files; the services that save a body call {@link linkAttachments}.
*
* @module
*/

View File

@@ -52,9 +52,9 @@ const RECENT_TTL_MS = 5 * 60 * 1000;
/** How many previews are kept. The oldest written goes first. */
const RECENT_MAX = 200;
/**
* Holds the previews fetched in the last few minutes, by URL. The create form
* previews a link and then posts it, and without this the second fetch of the
* page was the slow part of posting. Only successes are kept.
* Makes posting a link fast after the create form previewed it, by keeping the
* previews fetched in the last few minutes, by URL. Without it, the second
* fetch of the page was the slow part of posting. Only successes are kept.
*/
const recent = new Map<
string,

View File

@@ -1,7 +1,8 @@
/**
* Defines the envelope every JSON endpoint answers with, and {@link Wire}, the
* form a type takes once it has crossed JSON. The routes build the envelope,
* and the frontend's API helpers unwrap it.
* Lets the client tell success from failure the same way for every JSON
* endpoint, through the envelope they all answer with. It also defines {@link
* Wire}, the form a type takes once it has crossed JSON. The routes build the
* envelope, and the frontend's API helpers unwrap it.
*
* @module
*/

View File

@@ -1,7 +1,8 @@
/**
* Declares the categories that dumps are filed under, and the bodies that
* create and change them. Only admins manage categories, through the API, since
* no page does it. The client lists them in its `CategoriesProvider`.
* Categories file dumps by topic, and give each topic its own feed on the
* index. This module types them for the server and the client, with the bodies
* that create and change them. Only admins manage categories, through the API,
* since no page does it. The client lists them in its `CategoriesProvider`.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Defines a message of the site-wide chat, as the server sends it to the
* chatbox.
* The site-wide chat lets signed-in users talk live. This module types one of
* its messages, as the server sends it to the chatbox.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Declares the comments posted under a dump, and the bodies that post and edit
* them. The dump page's `CommentThread` renders them as a tree, by `parentId`.
* Comments let users discuss a dump under it, in threads. This module types
* them, with the bodies that post and edit them. The dump page's
* `CommentThread` renders them as a tree, by `parentId`.
*
* @module
*/

View File

@@ -1,8 +1,8 @@
/**
* Defines the dump, the link or file a user posts, as the API sends it, with
* its link preview and the bodies that create and edit it. The backend's
* `dump-service.ts` produces these types and the frontend's dump pages and
* create form consume them.
* A dump is what users post: a link or a file, with a title and a note. This
* module types it as the API sends it, with its link preview and the bodies
* that create and edit it. The backend's `dump-service.ts` produces these types
* and the frontend's dump pages and create form consume them.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Types what the signed-in user follows, as the client's `FollowProvider`
* loads it to show follow buttons pressed.
* Following a user or a playlist puts their dumps in the index's "followed"
* tab. This module types what the signed-in user follows, as the client's
* `FollowProvider` loads it to show the Follow buttons pressed.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Defines what counts as an @mention, so that the server notifies the same
* users the chat highlights and the editor suggests.
* Makes the server notify the same users the chat highlights and the editor
* suggests, by defining once what counts as an @mention.
*
* @module
*/

View File

@@ -1,7 +1,9 @@
/**
* Types the notifications `api/services/notification-service.ts` makes, and
* the data each kind carries. The client's `/notifications` page lists them
* and turns each into a sentence and a link.
* Notifications tell a user when someone follows them or their playlists,
* upvotes, comments, likes or mentions them, or posts where they follow. This
* module types the ones `api/services/notification-service.ts` makes, and the
* data each kind carries. The client's `/notifications` page lists them and
* turns each into a sentence and a link.
*
* @module
*/

View File

@@ -1,7 +1,8 @@
/**
* Maps each role to what it may do. The server enforces the table
* (`api/lib/permissions.ts`, `api/routes/ws.ts`) and the client reads the same
* one to hide what a role can't do (`src/utils/auth/permissions.ts`).
* Keeps the server and the client agreed on what each role may do. The server
* enforces this table (`api/lib/permissions.ts`, `api/routes/ws.ts`) and the
* client reads the same one to hide what a role can't do
* (`src/utils/auth/permissions.ts`).
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Defines what the Bandcamp and YouTube routes return to the frontend's
* player when it plays a Bandcamp release or a YouTube playlist.
* Lets the global player play a Bandcamp release with its own controls and list
* a YouTube playlist's tracks. This module types what the Bandcamp and YouTube
* routes return to it.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Declares playlists, the ordered collections of dumps their owners curate, and
* the bodies that create, edit and reorder them. The client shows them on
* `/playlists/:playlistId`, on profiles and in search results.
* Playlists are ordered collections of dumps their owners curate. This module
* types them, with the bodies that create, edit and reorder them. The client
* shows them on `/playlists/:playlistId`, on profiles and in search results.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Declares the results of the site search, which the `Search` page shows under
* one tab per kind.
* The site search finds dumps, users and playlists at once. This module types
* its results, which the `Search` page shows under one tab per kind.
*
* @module
*/

View File

@@ -1,7 +1,8 @@
/**
* Types users as the API sends them, their roles, and the bodies and answers
* of the `/api/users` and `/api/invites` endpoints, for the server and the
* client alike.
* A user crosses the wire in several shapes: as anyone sees them, as they see
* themselves, and in the bodies and answers of the `/api/users` and
* `/api/invites` endpoints. This module types them all, with the roles, for the
* server and the client alike.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Holds the limits on what users submit, shared so that the forms check the
* same limits the server enforces.
* Makes the forms check the same limits on what users submit as the server
* enforces, by holding them in one place.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Defines the messages of the WebSocket between `WSProvider` in the client and
* `routes/ws.ts` in the server, as JSON objects told apart by `type`.
* Live updates, votes, likes and the chat travel over one WebSocket between
* `WSProvider` in the client and `routes/ws.ts` in the server. This module
* types its messages, JSON objects told apart by `type`.
*
* The server's messages go out from `services/ws-service.ts`, and `WSProvider`
* handles them. Their Dates arrive as strings, so the client reads them as

View File

@@ -5729,7 +5729,13 @@ body.has-player .chat-fab {
}
/* ── Invite tree ── */
/* Every row but the very first has the same gap above it, a child's as a
sibling's: the gap is the node's top padding, so the connectors can run
through it. --invite-tree-avatar-center is where a row's avatar is centered,
from the top of its node. */
.invite-tree {
--invite-tree-gap: 0.5rem;
--invite-tree-avatar-center: calc(var(--invite-tree-gap) + 12px);
list-style: none;
padding: 0;
margin: 0;
@@ -5737,60 +5743,54 @@ body.has-player .chat-fab {
.invite-tree-node {
position: relative;
padding: 0.2rem 0;
padding-top: var(--invite-tree-gap);
}
.invite-tree-node:last-child {
padding-bottom: 0;
.invite-tree-node:first-child:not(.invite-tree-node .invite-tree-node) {
padding-top: 0;
}
/* Child list: indented so connector lines have room on the left.
The vertical line sits at the list's left:0, centered under the parent
avatar (avatar center 12px - 1px half-line = 11px). */
/* A child list is indented so that the vertical connector, at its left edge,
is centered under the parent's 24px avatar (12px center - 1px half-line). */
.invite-tree-node > .invite-tree {
margin-left: 11px;
padding-left: calc(12px + 0.5rem);
}
/* Connectors are owned per child node (not by the list) so they always anchor
to that child's own avatar center, regardless of how deep its subtree is.
--avatar-center = node top padding (0.2rem) + half the 24px avatar (12px). */
.invite-tree-node > .invite-tree > .invite-tree-node {
--avatar-center: calc(0.2rem + 12px);
}
/* Vertical segment: drops from the top of the child node to its avatar center,
joining the line coming down from the parent / previous sibling. */
.invite-tree-node > .invite-tree > .invite-tree-node::before {
/* Each child draws its own connectors, so they anchor to its avatar however
deep its subtree is. The vertical one drops from the row above to its
avatar center, and on through its subtree when a sibling follows. */
.invite-tree-node .invite-tree-node::before {
content: "";
position: absolute;
left: calc(-12px - 0.5rem);
top: 0;
height: var(--avatar-center);
height: var(--invite-tree-avatar-center);
width: 2px;
background: var(--color-border-subtle);
}
/* Non-last children extend the vertical segment through their full height
(including any subtree) so it reaches the next sibling. */
.invite-tree-node > .invite-tree > .invite-tree-node:not(:last-child)::before {
.invite-tree-node .invite-tree-node:not(:last-child)::before {
height: auto;
bottom: 0;
}
/* Horizontal connector: from the vertical line to the child's avatar. */
.invite-tree-node > .invite-tree > .invite-tree-node::after {
/* The horizontal one runs from the vertical line to the avatar. */
.invite-tree-node .invite-tree-node::after {
content: "";
position: absolute;
left: calc(-12px - 0.5rem);
top: calc(var(--avatar-center) - 1px); /* straddle center with the 2px bar */
top: calc(var(--invite-tree-avatar-center) - 1px);
width: calc(12px + 0.5rem);
height: 2px;
background: var(--color-border-subtle);
}
/* A block, not inline: in a line box, the theme's line height would add space
under the avatar and move it off --invite-tree-avatar-center. */
.invite-tree-user {
display: inline-flex;
display: flex;
width: fit-content;
align-items: center;
gap: 0.5rem;
color: var(--color-text);

View File

@@ -1,6 +1,6 @@
/**
* The modal that posts a dump from a link or a file, then offers to add it to
* the poster's playlists. Every "new dump" button opens it.
* Renders the modal that posts a dump from a link or a file, then offers to add
* it to the poster's playlists. Every "new dump" button opens it.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* The panels of the dump creation modal, and the indicator showing which one
* is open.
* Shows which panel of the dump creation modal is open, and declares the panels
* in the order they are walked.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* The title field of the dump creation modal, read-only until the poster
* chooses to edit it.
* Renders the title field of the dump creation modal, read-only until the
* poster chooses to edit it.
*
* @module
*/

View File

@@ -1,5 +1,6 @@
/**
* The tab bar of the index page, with the category switcher before the tabs.
* Renders the tab bar of the index page, with the category switcher before the
* tabs.
*
* @module
*/

View File

@@ -1,5 +1,5 @@
/**
* A row of tabs.
* Renders the row of tabs of the index, profile and search pages.
*
* @module
*/

View File

@@ -1,5 +1,6 @@
/**
* Reads which Bandcamp player this deployment uses, for `usePlayRichContent`.
* Lets a deployment choose between Bandcamp's embed and the site's own player.
* It reads the choice for `usePlayRichContent`.
*
* The server writes `GERBEUR_BANDCAMP_PLAYER` into index.html's
* `bandcamp-player` `<meta>` tag (`api/lib/index-html.ts`, and the Vite config

View File

@@ -1,6 +1,6 @@
/**
* Declares the session's React context. `AuthProvider` fills it and `useAuth`
* reads it; nothing else should touch it.
* Shares the session with every component. `AuthProvider` fills this context
* and `useAuth` reads it; nothing else should touch it.
*
* @module
*/

View File

@@ -1,5 +1,5 @@
/**
* Holds the session for the whole app and keeps it in step with the other tabs
* Keeps the user signed in across the whole app, in step with the other tabs
* and with the server. `App` mounts it near the root.
*
* @module

View File

@@ -1,6 +1,6 @@
/**
* Declares the categories' React context. `CategoriesProvider` fills it and
* `useCategories` reads it.
* Shares the site's categories with every component. `CategoriesProvider` fills
* this context and `useCategories` reads it.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Loads the site's categories once for the whole app, and again when the
* category manager changes them. `App` mounts it.
* Spares every page fetching the site's categories: it loads them once for the
* whole app, and again when the category manager changes them. `App` mounts it.
*
* @module
*/

View File

@@ -1,7 +1,6 @@
/**
* Declares the context through which the header's chat button and the floating
* chat button open the one chat modal, which `ChatProvider` owns. Read it with
* `useChat`.
* Lets the header's chat button and the floating one open the same chat modal,
* which `ChatProvider` owns. Read it with `useChat`.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Provides `ChatContext` and renders the chat modal while it is open. `App`
* mounts it inside the router, around every page.
* Owns the one chat modal: it renders it while it is open, and provides
* `ChatContext` to the buttons that open it. `App` mounts it inside the router,
* around every page.
*
* @module
*/

View File

@@ -1,8 +1,9 @@
/**
* Defines the player's two contexts, one for the queue and the controls and one
* for the playhead alone. PlayerProvider provides both around the whole app;
* GlobalPlayer draws the player, and the cards and previews that start playback
* read and drive it.
* Shares the player docked at the bottom of every page with the whole app. The
* cards and previews that start playback read and drive it, and GlobalPlayer
* draws it. It is two contexts, one for the queue and the controls and one for
* the playhead alone, which changes several times a second. PlayerProvider
* provides both around the whole app.
*
* @module
*/

View File

@@ -1,7 +1,8 @@
/**
* Holds the player's queue, playback state and controls, saves them across
* reloads and keeps streams' signed URLs fresh. App mounts it around the whole
* app, and GlobalPlayer draws what it holds.
* Keeps playback going from page to page and across reloads. It holds the
* player's queue, playback state and controls, saves them as they change and
* keeps streams' signed URLs fresh. App mounts it around the whole app, and
* GlobalPlayer draws what it holds.
*
* @module
*/

View File

@@ -1,7 +1,6 @@
/**
* Declares the context that holds the users and playlists the signed-in user
* follows, for the Follow buttons. `FollowProvider` provides it and
* `useFollows` reads it.
* Tells the Follow buttons what the signed-in user follows. `FollowProvider`
* provides this context and `useFollows` reads it.
*
* @module
*/
@@ -42,7 +41,10 @@ export const defaultFollowValue: FollowContextValue = {
isLoaded: false,
};
/** Holds the signed-in user's follows. Read it with `useFollows`. */
/**
* Tells the Follow buttons what the signed-in user follows. Read it with
* `useFollows`.
*/
export const FollowContext: React.Context<FollowContextValue> = createContext<
FollowContextValue
>(

View File

@@ -1,6 +1,7 @@
/**
* Provides `FollowContext`, from `GET /api/follows/status`. `App` mounts it
* around every page.
* Keeps what the signed-in user follows for the Follow buttons, loaded from
* `GET /api/follows/status` and again when another tab changes it. `App` mounts
* it around every page.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Declares the theme's React context. `ThemeProvider` fills it and `useTheme`
* reads it.
* Shares the chosen theme with every component. `ThemeProvider` fills this
* context and `useTheme` reads it.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Holds the chosen style and color scheme, stores them in localStorage and
* applies them to the document. `App` mounts it outermost.
* Lets each browser pick the site's style and color scheme. It holds the
* choice, stores it in localStorage and applies it to the document. `App`
* mounts it outermost.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Declares the context through which `Avatar` learns of avatars changed since
* the page loaded. `WSProvider` fills it from `user_updated` messages.
* Shows a user's new avatar everywhere as soon as they change it, without a
* reload. `WSProvider` fills this context from `user_updated` messages, and
* `Avatar` reads it.
*
* @module
*/
@@ -21,8 +22,9 @@ export interface AvatarOverride {
}
/**
* Holds an {@link AvatarOverride} by user id. It is apart from `WSContext`, so
* the many avatars of a page don't re-render on every vote or comment.
* Shows changed avatars without a reload, as an {@link AvatarOverride} by user
* id. It is apart from `WSContext`, so the many avatars of a page don't
* re-render on every vote or comment.
*/
export const AvatarOverrideContext: React.Context<
Record<string, AvatarOverride>

View File

@@ -1,7 +1,10 @@
/**
* Declares the three contexts `WSProvider` fills from the WebSocket, and the
* types they carry. They are split by how often each part changes, so that a
* feed doesn't re-render with every chat message or presence change.
* Live updates from the server reach the pages through the three contexts
* declared here: votes and likes, dumps, playlists, comments, profiles and
* notifications in one, the chat in another, who is online in the third. They
* are split so that a feed doesn't re-render with every chat message or
* presence change. `WSProvider` fills them from its WebSocket, and components
* read them with `useWS`, `useWSChat` and `useWSPresence`.
*
* @module
*/
@@ -16,8 +19,9 @@ import type { Playlist } from "../../../shared/playlist.ts";
import type { PublicUser } from "../../../shared/user.ts";
/**
* Set as `lastVoteEvent` by `WSProvider` on a `votes_update`. The profile
* pages read it to add or drop a dump from a user's upvoted list.
* Keeps a user's upvoted list live, on their profile and on `UserUpvoted`.
* `WSProvider` sets it as `lastVoteEvent` on each `votes_update`, and
* `useUpvotedList` adds or drops the dump.
*/
export interface VoteEvent {
dumpId: string;
@@ -26,9 +30,11 @@ export interface VoteEvent {
}
/**
* Set as `lastPlaylistEvent` by `WSProvider` on each `playlist_*` message.
* The pages that show playlists apply it, some through `usePlaylistListSync`.
* Which fields are set depends on `type`.
* Keeps the playlists on screen up to date, in lists, on a playlist's page and
* on a dump's page. `WSProvider` sets it as `lastPlaylistEvent` on each
* `playlist_*` message; lists apply it through `usePlaylistListSync`, a
* playlist's page through `usePlaylist`. Which fields are set depends on
* `type`.
*/
export interface PlaylistEvent {
type: "created" | "updated" | "deleted" | "dumps_updated";
@@ -47,9 +53,9 @@ export interface PlaylistEvent {
}
/**
* Set as `lastCommentEvent` by `WSProvider` on each `comment_*` message. The
* `Dump` page applies it to its thread, and `useDumpListSync` to its comment
* counts.
* Keeps comment threads and comment counts live. `WSProvider` sets it as
* `lastCommentEvent` on each `comment_*` message; the `Dump` page applies it
* to its thread, and `useDumpListSync` to the counts on dump cards.
*/
export interface CommentEvent {
type: "created" | "deleted" | "updated";
@@ -61,16 +67,18 @@ export interface CommentEvent {
}
/**
* Set as `lastUserEvent` by `WSProvider` on a `user_updated`, which the
* server sends on any change to a profile. `useProfile` applies it.
* Keeps a profile page live while its user edits their profile. `WSProvider`
* sets it as `lastUserEvent` on each `user_updated`, which the server sends on
* any change to a profile, and `useProfile` applies it.
*/
export interface UserEvent {
user: PublicUser;
}
/**
* Chosen in `ChatModal` for the reply bar, and kept on the pending message so
* it shows the reply before the server's echo. Only `id` goes to the server.
* Quotes the message a chat message replies to. `ChatModal` shows it in the
* reply bar once the user picks it, and it stays on the pending message so
* the quote shows before the server's echo. Only `id` goes to the server.
*/
export interface ChatReplyTarget {
id: string;
@@ -79,9 +87,9 @@ export interface ChatReplyTarget {
}
/**
* Held by `WSProvider` for a chat message this client sent and the server
* hasn't echoed back yet. `ChatModal` shows it, as `PendingItem`, after the
* messages the server has.
* Shows a chat message the moment the user sends it, and lets them retry it if
* it fails. `WSProvider` holds it until the server echoes the message back, and
* `ChatModal` shows it, as `PendingItem`, after the messages the server has.
*/
export interface PendingChatMessage {
/**
@@ -102,8 +110,8 @@ export interface PendingChatMessage {
}
/**
* Set as `chatError` by `WSProvider` when the server refuses a chat edit or
* delete. `ChatModal` shows it for a few seconds.
* Tells the user the server refused their chat edit or delete. `WSProvider`
* sets it as `chatError`, and `ChatModal` shows it for a few seconds.
*/
export interface ChatError {
/** Tells apart two errors with the same message. */
@@ -112,60 +120,75 @@ export interface ChatError {
}
/**
* Provided by `WSProvider` through {@link WSContext}, read with `useWS`. The
* `last*Event` fields hold only the latest event of their kind: a consumer
* reacts in an effect when one changes, and an event that another replaces
* within the same render is missed. After a reconnect, `connectionEpoch` tells
* pages to fetch what they missed.
* Carries everything live but the chat and presence, for pages to lay over
* what they fetched. `WSProvider` provides it through {@link WSContext}, and
* components read it with `useWS`. Each `last*Event` field holds only the
* latest event of its kind: a consumer reacts in an effect when one changes,
* and an event that another replaces within the same render is missed. After a
* reconnect, `connectionEpoch` tells pages to fetch what they missed.
*/
export interface WSContextValue {
wsStatus: "connecting" | "connected" | "disconnected";
/** Set while the socket is down, for the banner that says so. */
wsErrorMessage: string | null;
/**
* Counts known to the socket, by dump id: pushed by the server, or changed
* optimistically. A missing dump shows its fetched `voteCount`.
* Shows vote counts live. It holds, by dump id, the counts the server pushed
* or this tab changed optimistically; a dump missing from it shows its
* fetched `voteCount`.
*/
voteCounts: Record<string, number>;
/** Reset from the server's `welcome` on every connection. */
/**
* Marks the dumps this user upvoted, for their vote buttons. The server's
* `welcome` resets it on every connection.
*/
myVotes: Set<string>;
/** As `voteCounts`, for comments. */
likeCounts: Record<string, number>;
/** As `myVotes`, for comments. */
myLikes: Set<string>;
/**
* Dumps that arrived this session, newest first and at most 100: the ones
* the server announced, which are public, and the ones this user posted,
* which may be private. `Index` puts them above the feed it fetched.
* Shows new dumps on the index as they are posted, above the feed `Index`
* fetched. It holds, newest first and at most 100, the ones the server
* announced this session, which are public, and the ones this user posted,
* which may be private.
*/
recentDumps: Dump[];
/**
* It holds the dumps deleted this session, or made private by someone else,
* for the lists to filter out. A dump made public again leaves it.
* Takes deleted dumps off every list at once. It holds the dumps deleted this
* session, or made private by someone else; a dump made public again leaves
* it.
*/
deletedDumpIds: Set<string>;
lastVoteEvent: VoteEvent | null;
/**
* It holds the dump as its latest `dump_created` or `dump_updated` has it,
* for the lists to update it in place, or put it back where it was.
* Keeps a dump up to date in every list that shows it. It holds the dump as
* its latest `dump_created` or `dump_updated` has it, and a list updates it
* in place, or puts it back where it was.
*/
lastDumpEvent: Dump | null;
lastPlaylistEvent: PlaylistEvent | null;
deletedPlaylistIds: Set<string>;
lastCommentEvent: CommentEvent | null;
lastUserEvent: UserEvent | null;
/** Read from the `welcome`, then counted up by each new notification. */
/**
* Drives the bell's badge. It comes from the `welcome`, then each new
* notification counts it up.
*/
unreadNotificationCount: number;
/** Read by the bell, which rings, and by the notifications page. */
/**
* Makes the bell ring, and puts the notification on the notifications page
* as it arrives.
*/
lastNotification: Notification | null;
/**
* Incremented on each reconnect, never on the first connection, so pages
* fetch again what they missed while the socket was down.
* Tells pages to fetch again what they missed while the socket was down. It
* goes up on each reconnect, never on the first connection.
*/
connectionEpoch: number;
/**
* Incremented when the user follows or unfollows something in another tab
* (or this one), so `FollowProvider` fetches the follow status again.
* Keeps the Follow buttons right when the user follows or unfollows in
* another tab. It goes up on every follow change, in any tab, and
* `FollowProvider` fetches the follow status again.
*/
followsVersion: number;
/**
@@ -182,8 +205,9 @@ export interface WSContextValue {
/** Withdraws a like, as {@link WSContextValue.castVote} casts a vote. */
removeCommentLike: (commentId: string, shownCount?: number) => void;
/**
* Adds a dump this user just posted to `recentDumps`, without waiting for
* its broadcast, which a private dump never gets. `DumpCreateModal` calls it.
* Shows a dump this user just posted on the index without waiting for its
* broadcast, which a private dump never gets. `DumpCreateModal` calls it,
* and it adds the dump to `recentDumps`.
*/
injectDump: (dump: Dump) => void;
/**
@@ -194,36 +218,43 @@ export interface WSContextValue {
}
/**
* Provided by `WSProvider` through {@link WSChatContext}, read with
* `useWSChat`. The socket sends chat messages only to signed-in users, so for
* a visitor it stays empty.
* Carries the chat's live side, which `ChatModal` lays over the history it
* fetches and the chat buttons show as a badge. `WSProvider` provides it
* through {@link WSChatContext}, and components read it with `useWSChat`. The
* socket sends chat only to signed-in users, so for a visitor it stays empty.
*/
export interface WSChatContextValue {
/**
* The messages received this session, at most 200, for `ChatModal` to merge
* with the history it fetches. A reconnect empties it while the chat is
* closed, since `ChatModal` fetches everything again on open.
* Shows chat messages as they arrive. It holds the ones received this
* session, at most 200, which `ChatModal` merges with the history it
* fetches. A reconnect empties it while the chat is closed, since `ChatModal`
* fetches everything again on open.
*/
chatMessages: ChatMessage[];
/**
* Messages deleted this session, which `ChatModal` filters out of its
* fetched history.
* Takes the messages deleted this session out of the history `ChatModal`
* fetched.
*/
deletedChatIds: Set<string>;
/**
* Messages from others after the read marker. The server keeps the marker,
* so the count agrees across tabs and devices.
* Drives the chat buttons' unread badge. It counts the messages from others
* after the read marker, which the server keeps, so the count agrees across
* tabs and devices.
*/
unreadChatCount: number;
/**
* The time of the newest message the user has seen, which the server keeps.
* Null until the `welcome`, and for a visitor.
* Places the "new" divider when the chat opens. It holds the time of the
* newest message the user has seen, which the server keeps, and is null
* until the `welcome`, and for a visitor.
*/
chatReadAt: Date | null;
pendingChatMessages: PendingChatMessage[];
/** Only for edits and deletes. A failed send shows on its pending message. */
chatError: ChatError | null;
/** Includes the user when they are typing: `ChatModal` filters them out. */
/**
* Shows who is typing. It includes the user themselves, whom `ChatModal`
* filters out.
*/
typingUsers: OnlineUser[];
/** Trims `body` and sends it as a pending message. Ignores a blank body. */
sendChatMessage: (body: string, replyTo?: ChatReplyTarget) => void;
@@ -244,10 +275,10 @@ export interface WSChatContextValue {
*/
markChatRead: (at: Date) => void;
/**
* Replaces the live buffer's messages between `from` and `to` with
* `messages`, which `ChatModal` fetched after a reconnect. A message missing
* from them was deleted while the socket was down. A failed send found among
* them did reach the server, and leaves the pending list.
* Repairs the live buffer after a reconnect. It replaces its messages between
* `from` and `to` with `messages`, which `ChatModal` fetched. A message
* missing from them was deleted while the socket was down. A failed send
* found among them did reach the server, and leaves the pending list.
*/
reconcileChat: (messages: ChatMessage[], from: Date, to: Date) => void;
/**
@@ -257,23 +288,27 @@ export interface WSChatContextValue {
*/
setChatTyping: (typing: boolean) => void;
/**
* Tells the server whether the user is reading the chat: open, in a visible
* tab. The server sends no notification for a mention the user reads live,
* and this client doesn't count the message unread.
* Spares the user a notification for a mention they read live. It tells the
* server whether the chat is open in a visible tab; while it is, the server
* sends no notification for a mention, and this client doesn't count the
* message unread.
*/
setChatOpen: (open: boolean) => void;
}
/**
* Provided by `WSProvider` through {@link WSPresenceContext}, read with
* `useWSPresence`.
* Carries who is online, for `PresenceRow`. `WSProvider` provides it through
* {@link WSPresenceContext}, and components read it with `useWSPresence`.
*/
export interface WSPresenceContextValue {
/** The signed-in users with a socket open, each once. */
onlineUsers: OnlineUser[];
}
/** Given to a consumer outside `WSProvider`, as if it were still connecting. */
/**
* Lets a component render outside `WSProvider`, as if the socket were
* connecting.
*/
export const defaultWSValue: WSContextValue = {
wsStatus: "connecting",
wsErrorMessage: null,
@@ -301,7 +336,7 @@ export const defaultWSValue: WSContextValue = {
clearUnreadNotifications: () => {},
};
/** Given to a consumer outside `WSProvider`: an empty chat. */
/** Lets a component render outside `WSProvider`, with an empty chat. */
export const defaultWSChatValue: WSChatContextValue = {
chatMessages: [],
deletedChatIds: new Set(),
@@ -322,25 +357,25 @@ export const defaultWSChatValue: WSChatContextValue = {
setChatOpen: () => {},
};
/** Given to a consumer outside `WSProvider`: nobody is online. */
/** Lets a component render outside `WSProvider`, with nobody online. */
export const defaultWSPresenceValue: WSPresenceContextValue = {
onlineUsers: [],
};
/**
* Holds the connection, the votes and likes, and the latest live event of
* each kind. Read it with `useWS`.
* Carries the live state pages lay over what they fetched: the connection, the
* votes and likes, and the latest event of each kind. Read it with `useWS`.
*/
export const WSContext: React.Context<WSContextValue> = createContext<
WSContextValue
>(defaultWSValue);
/** Holds the chat's live stream and read state. Read it with `useWSChat`. */
/** Carries the chat's live stream and read state. Read it with `useWSChat`. */
export const WSChatContext: React.Context<WSChatContextValue> = createContext<
WSChatContextValue
>(
defaultWSChatValue,
);
/** Holds who is online. Read it with `useWSPresence`. */
/** Carries who is online. Read it with `useWSPresence`. */
export const WSPresenceContext: React.Context<WSPresenceContextValue> =
createContext<
WSPresenceContextValue

View File

@@ -1,8 +1,9 @@
/**
* Holds the app's one WebSocket to `/ws` and turns its messages into the state
* of `WSContext`, `WSChatContext`, `WSPresenceContext` and
* `AvatarOverrideContext`. `App` mounts it around every page. The protocol is
* `shared/ws.ts`, and the server's side is `api/routes/ws.ts`.
* Brings the server's live updates into the app. It holds the one WebSocket to
* `/ws` and turns its messages into the state of `WSContext`, `WSChatContext`,
* `WSPresenceContext` and `AvatarOverrideContext`. `App` mounts it around every
* page. The protocol is `shared/ws.ts`, and the server's side is
* `api/routes/ws.ts`.
*
* @module
*/
@@ -94,9 +95,9 @@ const CHAT_READ_THROTTLE = 1_000;
const CHAT_ECHO_WINDOW = 60_000;
/**
* Held by {@link useOptimisticToggles} for an item whose toggles the server
* hasn't all acknowledged yet. `count` and `on` are as before the first of
* them, for a failure to restore.
* Lets a failed toggle restore what was shown before the first click the server
* hasn't acknowledged. {@link useOptimisticToggles} holds one for each such
* item; `count` and `on` are as they were before that click.
*/
interface PendingToggle {
timeout: ReturnType<typeof setTimeout>;

View File

@@ -1,5 +1,5 @@
/**
* Reads `ChatContext`, for the buttons that open the chat.
* Lets a button open the chat, by reading `ChatContext`.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Loads a user's dumps or upvoted dumps, a page at a time, for the profile
* subpages that list them in full.
* Feeds the profile subpages that list all of a user's dumps, or all they
* upvoted, loading them a page at a time.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Keeps the state of `TextEditor`'s emoji picker, which opens on a typed
* `:query` or from the editor's button. `TextEditor` renders the picker and
* handles its keys.
* Lets a user insert an emoji by typing `:query` or from the editor's button.
* It keeps the state of `TextEditor`'s emoji picker, and `TextEditor` renders
* the picker and handles its keys.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Keeps the state of `TextEditor`'s `@mention` suggestions, which it fetches
* from `GET /api/users/search`. `TextEditor` renders them in a
* `MentionDropdown`.
* Suggests users to @mention as the user types in `TextEditor`. It keeps the
* state of the suggestions, fetched from `GET /api/users/search`, and
* `TextEditor` renders them in a `MentionDropdown`.
*
* @module
*/

View File

@@ -1,5 +1,6 @@
/**
* Stores, in this browser, which feed tab the index opens on.
* Lets a user choose which feed tab the index opens on, remembered in this
* browser.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Times the fade-out of items leaving a list, so that a removal made by
* mistake can be undone before the item is gone.
* Lets a removal made by mistake be undone before the item leaves the list, by
* timing the fade-out of items leaving a list.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Keeps a feed's loaded pages and scroll position in sessionStorage, so that
* going back to a feed page shows it as it was left instead of refetching it.
* Shows a feed as it was left when the user comes back to it, instead of
* fetching it again. It keeps the feed's loaded pages and scroll position in
* sessionStorage.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Loads a list a page at a time. Every list the API pages goes through
* {@link useLoadMore} for its next pages, and a list that loads on its own
* through {@link usePagedList}, from its first page on.
* Gives every paged list of the app the same loading. Every list the API pages
* goes through {@link useLoadMore} for its next pages, and a list that loads on
* its own through {@link usePagedList}, from its first page on.
*
* @module
*/

View File

@@ -1,5 +1,7 @@
/**
* react-hook-form, with the shared lifecycle of a form that posts to the API.
* Gives every form that posts to the API the same submit, which shows a failed
* request as the form's root error. It is react-hook-form's `useForm` plus that
* `submit`.
*
* @module
*/
@@ -15,7 +17,7 @@ import {
import { friendlyFetchError } from "../../utils/shared/api.ts";
/** What {@link useApiForm} returns: the form, and `submit`. */
/** Returned by {@link useApiForm}, the form with `submit` added. */
export interface UseApiFormResult<T extends FieldValues>
extends UseFormReturn<T> {
/**

View File

@@ -1,6 +1,7 @@
/**
* Loads a playlist with its dumps for its page, keeps them live, and makes the
* changes its owner or a moderator makes there.
* Everything a playlist's page shows and changes goes through this hook. It
* loads the playlist with its dumps, keeps them live, and makes the changes its
* owner or a moderator makes there.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Loads which of the signed-in user's playlists hold a dump, and adds the dump
* to them or takes it out.
* Lets the user add a dump to their playlists or take it out, in
* `AddToPlaylistModal` and the dump form's last step. It loads which of their
* playlists hold the dump.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Loads a profile's user with the first page of their dumps, upvotes and
* playlists, and keeps them live.
* Everything a profile page shows comes through this hook. It loads the user
* with the first page of their dumps, upvotes and playlists, and keeps them
* live.
*
* @module
*/

View File

@@ -1,5 +1,6 @@
/**
* Loads one thing from the API, once per key, when first asked to.
* Lets a component load one thing from the API the first time it's needed, and
* keep it for as long as its key holds, as the profile's lazy tabs do.
*
* @module
*/

View File

@@ -1,5 +1,6 @@
/**
* Reads `FollowContext`, for the Follow buttons.
* Tells a Follow button what the user follows, and follows or unfollows, by
* reading `FollowContext`.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Reads the live state that `WSProvider` keeps from the WebSocket. Each hook
* reads one of its three contexts, so that a component re-renders only with
* the part it uses. Outside `WSProvider`, they return inert defaults.
* Gives components the server's live updates. Each hook reads one of the three
* contexts `WSProvider` fills, so that a component re-renders only with the
* part it uses. Outside `WSProvider`, they return inert defaults.
*
* @module
*/

View File

@@ -200,7 +200,7 @@ msgstr "Already dumped by {0} {1}"
msgid "Already have an account? <0>Log in</0>"
msgstr "Already have an account? <0>Log in</0>"
#: src/utils/shared/api.ts:34
#: src/utils/shared/api.ts:35
msgid "An unexpected error occurred."
msgstr "An unexpected error occurred."
@@ -228,8 +228,8 @@ msgstr "Auto"
msgid "Back to login"
msgstr "Back to login"
#: src/contexts/ws/WSProvider.tsx:394
#: src/contexts/ws/WSProvider.tsx:760
#: src/contexts/ws/WSProvider.tsx:395
#: src/contexts/ws/WSProvider.tsx:761
msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
msgstr "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
@@ -341,7 +341,7 @@ msgstr "Color scheme"
msgid "Confirm new password"
msgstr "Confirm new password"
#: src/contexts/ws/WSProvider.tsx:753
#: src/contexts/ws/WSProvider.tsx:754
msgid "Connection lost — this message may not have been sent"
msgstr "Connection lost — this message may not have been sent"
@@ -357,7 +357,7 @@ msgstr "Copy"
msgid "Could not change password"
msgstr "Could not change password"
#: src/utils/shared/api.ts:32
#: src/utils/shared/api.ts:33
msgid "Could not connect to the server. Check your connection and try again."
msgstr "Could not connect to the server. Check your connection and try again."
@@ -699,7 +699,7 @@ msgstr "Follow some public playlists to see their dumps here."
msgid "Follow some users to see their dumps here."
msgstr "Follow some users to see their dumps here."
#: src/components/feed/FeedTabBar.tsx:48
#: src/components/feed/FeedTabBar.tsx:49
#: src/components/profile/ProfileSettingsTab.tsx:44
#: src/pages/UserPublicProfile.tsx:191
msgid "Followed"
@@ -745,7 +745,7 @@ msgstr "Go home"
msgid "Go to login"
msgstr "Go to login"
#: src/components/feed/FeedTabBar.tsx:44
#: src/components/feed/FeedTabBar.tsx:45
#: src/components/profile/ProfileSettingsTab.tsx:41
msgid "Hot"
msgstr "Hot"
@@ -789,7 +789,7 @@ msgstr "invited by"
msgid "Invitees"
msgstr "Invitees"
#: src/components/feed/FeedTabBar.tsx:46
#: src/components/feed/FeedTabBar.tsx:47
#: src/components/profile/ProfileSettingsTab.tsx:43
msgid "Journal"
msgstr "Journal"
@@ -814,7 +814,7 @@ msgstr "Link"
msgid "Link or file"
msgstr "Link or file"
#: src/contexts/ws/WSProvider.tsx:759
#: src/contexts/ws/WSProvider.tsx:760
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
@@ -919,7 +919,7 @@ msgid "new"
msgstr "new"
#: src/components/chat/ChatModal.tsx:669
#: src/components/feed/FeedTabBar.tsx:45
#: src/components/feed/FeedTabBar.tsx:46
#: src/components/profile/ProfileSettingsTab.tsx:42
msgid "New"
msgstr "New"
@@ -993,7 +993,7 @@ msgstr "No playlists match \"{q}\"."
msgid "No playlists yet."
msgstr "No playlists yet."
#: src/contexts/ws/WSProvider.tsx:928
#: src/contexts/ws/WSProvider.tsx:929
msgid "No reply from the server — this message may not have been sent"
msgstr "No reply from the server — this message may not have been sent"
@@ -1351,7 +1351,7 @@ msgstr "Some images couldn't be uploaded."
#: src/components/form/FormError.tsx:31
#: src/components/layout/PageError.tsx:29
#: src/contexts/ws/WSProvider.tsx:728
#: src/contexts/ws/WSProvider.tsx:729
#: src/pages/PlaylistDetail.tsx:119
msgid "Something went wrong"
msgstr "Something went wrong"
@@ -1380,7 +1380,7 @@ msgstr "That file is over {limit}."
msgid "the chat"
msgstr "the chat"
#: src/hooks/playlist/usePlaylist.ts:160
#: src/hooks/playlist/usePlaylist.ts:161
msgid "The dump couldn't be removed. Try again."
msgstr "The dump couldn't be removed. Try again."
@@ -1388,7 +1388,7 @@ msgstr "The dump couldn't be removed. Try again."
msgid "The image couldn't be uploaded."
msgstr "The image couldn't be uploaded."
#: src/hooks/playlist/usePlaylist.ts:306
#: src/hooks/playlist/usePlaylist.ts:307
msgid "The new order couldn't be saved. Try again."
msgstr "The new order couldn't be saved. Try again."
@@ -1448,7 +1448,7 @@ msgstr "Type a message…"
msgid "Undo"
msgstr "Undo"
#: src/utils/shared/api.ts:21
#: src/utils/shared/api.ts:22
msgid "Unexpected response format"
msgstr "Unexpected response format"
@@ -1580,7 +1580,7 @@ msgstr "You can post it anyway."
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
#: src/contexts/ws/WSProvider.tsx:909
#: src/contexts/ws/WSProvider.tsx:910
msgid "You're offline — not sent"
msgstr "You're offline — not sent"

View File

@@ -200,7 +200,7 @@ msgstr "Déjà recommandé par {0} {1}"
msgid "Already have an account? <0>Log in</0>"
msgstr "Vous avez déjà un compte ? <0>Se connecter</0>"
#: src/utils/shared/api.ts:34
#: src/utils/shared/api.ts:35
msgid "An unexpected error occurred."
msgstr "Une erreur inattendue s'est produite."
@@ -228,8 +228,8 @@ msgstr "Auto"
msgid "Back to login"
msgstr "Retour à la connexion"
#: src/contexts/ws/WSProvider.tsx:394
#: src/contexts/ws/WSProvider.tsx:760
#: src/contexts/ws/WSProvider.tsx:395
#: src/contexts/ws/WSProvider.tsx:761
msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion."
@@ -341,7 +341,7 @@ msgstr "Thème de couleur"
msgid "Confirm new password"
msgstr "Confirmer le nouveau mot de passe"
#: src/contexts/ws/WSProvider.tsx:753
#: src/contexts/ws/WSProvider.tsx:754
msgid "Connection lost — this message may not have been sent"
msgstr "Connexion perdue — ce message n'a peut-être pas été envoyé"
@@ -357,7 +357,7 @@ msgstr "Copier"
msgid "Could not change password"
msgstr "Impossible de changer le mot de passe"
#: src/utils/shared/api.ts:32
#: src/utils/shared/api.ts:33
msgid "Could not connect to the server. Check your connection and try again."
msgstr "Impossible de joindre le serveur. Vérifiez votre connexion et réessayez."
@@ -699,7 +699,7 @@ msgstr "Suivez des collections publiques pour voir leurs recos ici."
msgid "Follow some users to see their dumps here."
msgstr "Suivez des utilisateurs pour voir leurs recos ici."
#: src/components/feed/FeedTabBar.tsx:48
#: src/components/feed/FeedTabBar.tsx:49
#: src/components/profile/ProfileSettingsTab.tsx:44
#: src/pages/UserPublicProfile.tsx:191
msgid "Followed"
@@ -745,7 +745,7 @@ msgstr "Accueil"
msgid "Go to login"
msgstr "Aller à la connexion"
#: src/components/feed/FeedTabBar.tsx:44
#: src/components/feed/FeedTabBar.tsx:45
#: src/components/profile/ProfileSettingsTab.tsx:41
msgid "Hot"
msgstr "Tendances"
@@ -789,7 +789,7 @@ msgstr "invité par"
msgid "Invitees"
msgstr "Invités"
#: src/components/feed/FeedTabBar.tsx:46
#: src/components/feed/FeedTabBar.tsx:47
#: src/components/profile/ProfileSettingsTab.tsx:43
msgid "Journal"
msgstr "Journal"
@@ -814,7 +814,7 @@ msgstr "Lien"
msgid "Link or file"
msgstr "Lien ou fichier"
#: src/contexts/ws/WSProvider.tsx:759
#: src/contexts/ws/WSProvider.tsx:760
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
@@ -919,7 +919,7 @@ msgid "new"
msgstr "nouveau"
#: src/components/chat/ChatModal.tsx:669
#: src/components/feed/FeedTabBar.tsx:45
#: src/components/feed/FeedTabBar.tsx:46
#: src/components/profile/ProfileSettingsTab.tsx:42
msgid "New"
msgstr "Nouveau"
@@ -993,7 +993,7 @@ msgstr "Aucune collection ne correspond à « {q} »."
msgid "No playlists yet."
msgstr "Pas encore de collections."
#: src/contexts/ws/WSProvider.tsx:928
#: src/contexts/ws/WSProvider.tsx:929
msgid "No reply from the server — this message may not have been sent"
msgstr "Pas de réponse du serveur — ce message n'a peut-être pas été envoyé"
@@ -1351,7 +1351,7 @@ msgstr "Certaines images n'ont pas pu être envoyées."
#: src/components/form/FormError.tsx:31
#: src/components/layout/PageError.tsx:29
#: src/contexts/ws/WSProvider.tsx:728
#: src/contexts/ws/WSProvider.tsx:729
#: src/pages/PlaylistDetail.tsx:119
msgid "Something went wrong"
msgstr "Une erreur est survenue"
@@ -1380,7 +1380,7 @@ msgstr "Ce fichier dépasse {limit}."
msgid "the chat"
msgstr "la tribune"
#: src/hooks/playlist/usePlaylist.ts:160
#: src/hooks/playlist/usePlaylist.ts:161
msgid "The dump couldn't be removed. Try again."
msgstr "Le dump n'a pas pu être retiré. Réessayez."
@@ -1388,7 +1388,7 @@ msgstr "Le dump n'a pas pu être retiré. Réessayez."
msgid "The image couldn't be uploaded."
msgstr "L'image n'a pas pu être envoyée."
#: src/hooks/playlist/usePlaylist.ts:306
#: src/hooks/playlist/usePlaylist.ts:307
msgid "The new order couldn't be saved. Try again."
msgstr "Le nouvel ordre n'a pas pu être enregistré. Réessayez."
@@ -1448,7 +1448,7 @@ msgstr "Écrivez un message…"
msgid "Undo"
msgstr "Annuler"
#: src/utils/shared/api.ts:21
#: src/utils/shared/api.ts:22
msgid "Unexpected response format"
msgstr "Format de réponse inattendu"
@@ -1580,7 +1580,7 @@ msgstr "Vous pouvez la publier quand même."
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu."
#: src/contexts/ws/WSProvider.tsx:909
#: src/contexts/ws/WSProvider.tsx:910
msgid "You're offline — not sent"
msgstr "Hors ligne — message non envoyé"

View File

@@ -1,5 +1,5 @@
/**
* The page of a dump, with its preview, comments, related dumps and the
* Renders a dump's page, with its preview, comments, related dumps and the
* playlists holding it.
*
* @module

View File

@@ -1,5 +1,5 @@
/**
* The page where a dump's poster, or a moderator, edits or deletes it.
* Renders the page where a dump's poster, or a moderator, edits or deletes it.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* The page that shows the results of a search, in a tab for dumps, one for
* users and one for playlists.
* Renders the results of a search, in a tab for dumps, one for users and one
* for playlists.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* The page listing all of a user's dumps, which the "dumps" section of their
* profile links to.
* Renders the page listing all of a user's dumps, which the "dumps" section of
* their profile links to.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* The page listing all the dumps a user upvoted, which the "upvoted" section of
* their profile links to.
* Renders the page listing all the dumps a user upvoted, which the "upvoted"
* section of their profile links to.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Reads and writes the session in localStorage, where every tab shares one
* entry. `AuthProvider` and `useAuth` are its only users.
* Keeps the user signed in across reloads and tabs, in one localStorage entry
* every tab shares. `AuthProvider` and `useAuth` are its only users.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Keeps the chat composer's unsent message in sessionStorage, per user, so
* closing the chat modal or reloading the tab doesn't lose it. `ChatModal`
* reads it on open and writes it on every change.
* Keeps the chat composer's unsent message when the chat modal closes or the
* tab reloads. It lives in sessionStorage, per user; `ChatModal` reads it on
* open and writes it on every change.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Reads the Markdown of chat messages, to find mentions and to flatten it into
* reply previews.
* Finds the mentions in a chat message, and flattens its Markdown into reply
* previews.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Keeps the half-written dump of the dump creation modal in localStorage, so
* that closing the modal by mistake, with Escape or a click outside, loses
* nothing but an attached file, which can't be stored.
* Gives back the half-written dump when the dump creation modal is closed by
* mistake, with Escape or a click outside. It is kept in localStorage, all but
* an attached file, which can't be stored.
*
* There is one draft, not one per tab: whoever closed the modal by mistake
* wants back what they were just writing, wherever they reopen it.

Some files were not shown because too many files have changed in this diff Show More