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>
This commit is contained in:
khannurien
2026-09-29 21:42:08 +00:00
parent 2af64721c7
commit f3f07ed1e3
105 changed files with 459 additions and 371 deletions

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.

View File

@@ -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
*/

View File

@@ -1,6 +1,6 @@
/**
* Reads a text as the editor's autocompletes and the chat see it: whether the
* caret is inside code, and whether a message is only emoji.
* Tells the editor's autocompletes when the caret is inside code, where they
* stay closed, and the chat when a message is only emoji, which it shows large.
*
* @module
*/

View File

@@ -1,7 +1,7 @@
/**
* Declares a list the API serves a page at a time, and reads its pages. Every
* such list in the app is a {@link PagedList}, whose next page `useLoadMore`
* appends.
* Makes every list the API serves a page at a time work the same way. Each is a
* {@link PagedList}, whose next page `useLoadMore` appends, and this module
* reads its pages.
*
* @module
*/

View File

@@ -1,6 +1,7 @@
/**
* Reads the API's JSON envelope, and turns the errors a request can end in into
* messages. Every component and hook that calls the API goes through it.
* Makes every call to the API succeed or fail the same way. It reads the JSON
* envelope, and turns the errors a request can end in into messages. Every
* component and hook that calls the API goes through it.
*
* @module
*/

View File

@@ -1,6 +1,6 @@
/**
* Declares the state of something a component loads from the API, which
* `ListState` renders while it isn't loaded.
* Gives every component that loads from the API the same states, so that
* `ListState` renders loading, error and empty alike everywhere.
*
* @module
*/

View File

@@ -9,9 +9,9 @@
const STORAGE_KEY = "sounds";
/**
* Holds, in localStorage, the id of the last event that chimed, so that a user
* with the site open in several tabs hears one chime per event, not one per
* tab.
* Makes a user with the site open in several tabs hear one chime per event, not
* one per tab. The key holds, in localStorage, the id of the last event that
* chimed.
*/
const CLAIM_KEY = "sounds-last-claim";
/**