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.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
31
README.md
31
README.md
@@ -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
|
||||
|
||||
@@ -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(/\/$/, "") ||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>();
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* A row of tabs.
|
||||
* Renders the row of tabs of the index, profile and search pages.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
>(
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Reads `ChatContext`, for the buttons that open the chat.
|
||||
* Lets a button open the chat, by reading `ChatContext`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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> {
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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é"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Stores the emoji picker's skin tone in localStorage, so that it outlasts the
|
||||
* picker and the page. `EmojiPickerPopup` reads and writes it.
|
||||
* Remembers the emoji picker's skin tone across pickers and pages. It lives in
|
||||
* localStorage, and `EmojiPickerPopup` reads and writes it.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user