default

Documents gerbeur, a small invite-only social platform for sharing links and files. The repository holds three parts: the backend in api/, the frontend in src/, and in shared/ whatever crosses the wire between them. Every module is listed by path, so the list groups them by part and layer.

api/scripts/generate-icons.ts

Draws the favicons and writes the manifest from SITE_EMOJI and OG_SITE_NAME, so changing them needs a restart and no rebuild. deno task icons <dir> runs it before the server starts, into the directory the frontend is served from, relative to the working directory. That is dist/ for start and serve, and public/, which Vite serves, for dev. It does nothing when the directory doesn't exist.

api/middleware/auth.ts

Gates routes on a session token, and on a permission after it. The routers put these middleware before a handler; a route open to anonymous callers calls parseOptionalAuth (lib/auth.ts) instead.

Functions

f
authMiddleware<R extends string>(
ctx: AuthContext<R>,
next: Next
): Promise<void>

Requires a valid session token in the Authorization header, and puts its payload in ctx.state.user for the rest of the route.

f
requirePermission(permission: Permission): <R extends string>(
ctx: AuthContext<R>,
next: Next
) => Promise<void>

Returns a middleware that requires the caller to hold permission. It must run after authMiddleware, as in router.patch(path, authMiddleware, requirePermission("user:manage"), …). The returned middleware throws an APIException (403) when the caller lacks the permission.

Interfaces

I
AuthState

Set on ctx.state by authMiddleware for the handlers after it. user carries the caller's live role, not the one in their token.

Type Aliases

T
AuthContext<
R extends string = string,
P extends RouteParams<R> = RouteParams<R>
>
= RouterContext<R, P, AuthState>

The context type of a handler behind authMiddleware. Pass the route path, as in AuthContext<"/:key/role">, to keep ctx.params typed.

api/middleware/error.ts

Turns the errors a request's handling throws into APIFailure responses. main.ts installs it first, so it catches what every later middleware and route throws.

Functions

f
errorMiddleware(
ctx: Context,
next: Next
): Promise<void>

Answers an APIException with its status, code and message. Anything else is logged with its stack and answered with a 500 SERVER_ERROR whose message gives nothing away.

api/middleware/compress.ts

Compresses what the routes answer in JSON, and the HTML page, for the browsers that accept it: a page of the feed is some twenty kilobytes as it is, and a fifth of that compressed. main.ts installs it after the error middleware, around everything that sets a body. The built frontend's files aren't its concern: lib/static.ts sends the copies compressed at build time.

Functions

f
compressMiddleware(
ctx: Context,
next: Next
): Promise<void>

Sends the response's JSON or text compressed, in brotli or gzip as the request accepts, when it is at least MIN_BYTES long. A response already encoded, a HEAD, and one marked no-transform go out as they are.

api/middleware/og.ts

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, and a link by a former name with a 301 to the current one. main.ts installs it before the routers.

Functions

f
ogMiddleware(
ctx: Context,
next: Next
): Promise<unknown>

Serves index.html with the tags of the dump, user or public playlist a path names (parsePagePath), and passes the request on when it names none that exists or the visitor may see. A path naming one by a former name, a user's or a slug, or in another case, gets a 301 to its current path. The page's URL is on FRONTEND_URL and the images' on PUBLIC_URL, not on the request's origin, which a reverse proxy may have rewritten to an internal host or plain http.

api/validators/settings.ts

Checks the body of PATCH /api/me/settings before routes/me.ts trusts it.

Functions

f
isUpdateSettingsRequest(obj: unknown): obj is UpdateSettingsRequest

Checks whether obj is an UpdateSettingsRequest: an object whose every key is a setting, each with a value that setting takes. An unknown key fails, so a typo doesn't pass for a change.

api/validators/federation.ts

Checks the JSON bodies that the federation routes (routes/federation.ts, routes/oauth.ts) receive before they read them. They check the shapes only: whether an instance answers, or a code or an invite works, is the services' check.

Functions

f
isAuthorizeRequest(obj: unknown): obj is AuthorizeRequest

Checks whether obj is an AuthorizeRequest.

f
isFederationLoginRequest(obj: unknown): obj is FederationLoginRequest

Checks whether obj is a FederationLoginRequest.

f
isFederationSessionRequest(obj: unknown): obj is FederationSessionRequest

Checks whether obj is a FederationSessionRequest.

f
isSetPeerRequest(obj: unknown): obj is SetPeerRequest

Checks whether obj is a SetPeerRequest.

api/validators/user.ts

Checks the JSON bodies that the account routes (routes/users.ts, routes/me.ts, routes/sessions.ts) receive before they read them.

Functions

f
fieldOf(
obj: unknown,
key: string
): unknown

Returns obj[key] when obj is an object that has it, else undefined. The users routes whose body has no guard of its own read their fields with it.

f
isLoginUserRequest(obj: unknown): obj is LoginUserRequest

Checks whether obj is a LoginUserRequest. It checks the types only: a wrong name or password is the login route's answer to give.

f
isRegisterUserRequest(obj: unknown): obj is RegisterUserRequest

Checks whether obj is a valid RegisterUserRequest. When it isn't, validateRegisterUserRequest says why.

f
isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest

Checks whether obj is a valid UpdateUserRequest, for PATCH /api/me. Every field is optional, but each one present must be well-formed and within the VALIDATION limits.

f
validateRegisterUserRequest(obj: unknown): string | null

Returns what is wrong with a sign-up body, as the message the register page shows, or null when it is valid. It checks the shape, the email's form, and the username and password against the VALIDATION limits. Whether the name, the email or the invite is taken is createUser's and claimInvite's check.

api/validators/category.ts

Checks the shape of the category request bodies, for the category routes to answer a 400. The category service checks the slug and name themselves.

Functions

f
isCreateCategoryRequest(obj: unknown): obj is CreateCategoryRequest

Checks whether obj has the fields of a CreateCategoryRequest.

f
isUpdateCategoryRequest(obj: unknown): obj is UpdateCategoryRequest

Checks whether obj has the fields of an UpdateCategoryRequest.

api/validators/dump.ts

Checks the JSON bodies that routes/dumps.ts takes to post a link and to edit a dump. A file dump arrives as a multipart form instead, and dump-service.ts checks its title and comment.

Functions

f
isCreateUrlDumpRequest(obj: unknown): obj is CreateUrlDumpRequest

Checks whether obj is a CreateUrlDumpRequest whose title and comment fit the VALIDATION limits. It leaves the URL to createUrlDump, which rejects anything but a public http(s) URL.

f
isUpdateDumpRequest(obj: unknown): obj is UpdateDumpRequest

Checks whether obj is an UpdateDumpRequest whose title and comment fit the VALIDATION limits. Unlike on creation, a title that is given must not be blank.

f
parseCategoryIds(value: FormDataEntryValue | null): string[]

Parses a multipart form's categoryIds, a JSON array sent as a string. A missing or malformed value counts as no category. The dumps and files routes read the forms that post and replace a file with it.

api/validators/playlist.ts

Checks the playlist request bodies, for the playlist routes to answer a 400.

Functions

f
isCreatePlaylistRequest(obj: unknown): obj is CreatePlaylistRequest

Checks whether obj is a CreatePlaylistRequest with a title that isn't blank, and a title and description within the VALIDATION limits. A null description passes. Whether its category ids name playlist categories is the playlist service's to check.

f
isReorderPlaylistRequest(obj: unknown): obj is ReorderPlaylistRequest

Checks whether obj is a ReorderPlaylistRequest. Whether its ids match the playlist is reorderPlaylist's to check.

f
isUpdatePlaylistRequest(obj: unknown): obj is UpdatePlaylistRequest

Checks whether obj is an UpdatePlaylistRequest whose title, if given, isn't blank, with a title and description within the VALIDATION limits.

api/validators/comment.ts

Checks the comment request bodies, for the comment routes to answer a 400.

Functions

f
isCreateCommentRequest(obj: unknown): obj is CreateCommentRequest

Checks whether obj is a CreateCommentRequest whose body isn't blank and fits VALIDATION.COMMENT_BODY_MAX. A null parentId passes, as a top-level comment.

f
isUpdateCommentRequest(obj: unknown): obj is UpdateCommentRequest

Checks whether obj is an UpdateCommentRequest whose body isn't blank and fits VALIDATION.COMMENT_BODY_MAX.

api/main.ts

Builds the Oak application and, when run directly, starts the recurring jobs and listens on LISTEN_HOST:PORT, having first signed the users out of other instances if they may no longer sign in there (FEDERATION.outgoing). A request goes through the error middleware, the compression of what is answered, the OG tags and CORS, then the routers, one per resource of the API (API.md lists them), and last the built frontend, which answers every path no router did.

Variables

v
app: Application

The whole server, exported so the tests' helpers can send it requests without listening.

api/model/auth.ts

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.

Functions

f
isAuthPayload(obj: unknown): obj is AuthPayload

Checks whether obj is an AuthPayload. A media or reset token's payload, which has a purpose, never is.

f
isPasswordResetPayload(obj: unknown): obj is PasswordResetPayload

Checks whether obj is a PasswordResetPayload. It doesn't check exp, which jwtVerify has already enforced.

Interfaces

I
AuthPayload

Signed into a session token by createJWT, and returned by authenticate to the auth middleware as ctx.state.user. By then role is the user's live role, while username is still the one at issue time.

I
PasswordResetPayload

Signed into a reset link's token by createPasswordResetToken, and returned by verifyPasswordResetToken to resetPassword.

api/model/chat.ts

Turns the rows of chat_messages into the ChatMessage the API sends. Only services/chat-service.ts uses it, on the rows of its one query.

Functions

f
chatMessageRowToApi(row: ChatMessageRow): ChatMessage

Converts a row to the ChatMessage that chat-service returns, turning its timestamps into Dates and its nulls into absent fields.

f
isChatMessageRow(obj: unknown): obj is ChatMessageRow

Checks whether obj, a row read by chat-service, is a ChatMessageRow.

Interfaces

I
ChatMessageRow

Returned by chat-service's query, which joins each message to its author and to the message it replies to. reply_to_id is kept when the message it points to is deleted, while reply_to_author and reply_to_body then read null.

api/model/settings.ts

Checks the rows settings-service.ts reads from user_settings, and turns them into the UserSettings the settings endpoints answer.

Functions

f
isUserSettingsRow(obj: unknown): obj is UserSettingsRow

Checks whether obj, a row getSettings selected, is a UserSettingsRow.

f
settingsRowToApi(row: UserSettingsRow): UserSettings

Converts a row to the UserSettings the client gets. A style or scheme the code no longer knows, as after one is removed, reads as unset.

Interfaces

I
UserSettingsRow

Stored by updateSettings and read back by getSettings. A user without a row has the defaults.

Variables

v
SETTINGS_COLUMNS: Record<keyof UserSettings, string>

Names the column of each setting, for the queries that pick users by one, as digest-service.ts does by its digest's switch.

api/model/user.ts

Turns the rows of users, once checked, into the User that user-service.ts returns. A route turns a User into what it may send with toPublicUser or toSelfUser.

Functions

f
isRemoteUser(user: User): boolean

Checks whether user's account lives on another instance, whose user signs in here through federation (federation-service.ts). Their password, email, avatar and name are that instance's to change, so the routes that change them here refuse.

f
isUserRow(obj: unknown): obj is UserRow

Checks whether obj, a row selected with the columns of UserRow, is one.

f
toPublicUser(unnamed 0: User): PublicUser

Returns what anyone may see of user, without the hash and the email. Every route that sends another user's profile goes through it.

f
toSelfUser(unnamed 0: User): SelfUser

Returns what user sees of themselves, without the hash, for the routes that answer the caller's own account.

f
userApiToRow(user: User): UserRow

Converts a User back to a row, for updateUser. A User has no inviter id, so invited_by comes out null: don't write it back.

f
userRowToApi(row: UserRow): User

Converts a row to a User, turning nulls into undefined.

Interfaces

I
User

Returned by the lookups of user-service.ts. It must never be sent as is: it carries the password hash.

  • passwordHash: string | null

    From hashPassword, as hash.salt, or null for an account from another instance (isRemoteUser), which signs in there and never with a password here. The row stores that as ''.

I
UserRow

Selected by user-service.ts and follow-service.ts, and converted by userRowToApi. It leaves out chat_read_at and tokens_valid_after, which only the chat's read state and getSessionState query.

api/model/invite.ts

Checks the rows invite-service.ts reads from invites to vet an invite before sign-up.

Functions

f
isInviteRow(obj: unknown): obj is InviteRow

Checks whether obj, a row validateInvite selected, is an InviteRow.

Interfaces

I
InviteRow

Stored by createInvite, and read back by validateInvite when the invite link is opened or used to sign up.

api/model/category.ts

Turns the rows the category service reads from categories into the Category the category routes send.

Functions

f
categoryRowToApi(row: CategoryRow): Category

Converts row to the Category the category routes send.

f
isCategoryRow(obj: unknown): obj is CategoryRow

Checks whether a row node:sqlite returned is a CategoryRow.

Interfaces

I
CategoryRow

Returned by the category service's SELECTs on categories.

api/model/password-reset.ts

Checks the rows password-reset-service.ts reads from password_reset_tokens to make each reset link work once.

Functions

f
isPasswordResetTokenRow(obj: unknown): obj is PasswordResetTokenRow

Checks whether obj, a row resetPassword selected, is a PasswordResetTokenRow.

Interfaces

I
PasswordResetTokenRow

Stored by requestPasswordReset when it emails a reset link, and read back by resetPassword when the link is used.

api/model/dump.ts

Turns the rows of dumps into the API's Dump the same way for every service that reads dumps: each selects them with the column lists here, checks each row with isDumpRow and converts it with dumpRowToApi.

Functions

f
dumpApiToRow(dump: Dump): DumpRow

Converts a Dump back to a row, for a service that writes an edited dump's columns.

f
dumpRowToApi(row: DumpRow): Dump

Converts a row to the API's Dump, parsing its preview's JSON. It leaves categoryIds out: the services add them with attachCategoryIds.

f
isDumpRow(obj: unknown): obj is DumpRow

Checks whether obj, a row from a query, is a DumpRow. It checks each column's type, not that kind is url or file.

Interfaces

I
DumpRow

Read from the dumps table with DUMP_SELECT_COLUMNS, or written back from a Dump by dumpApiToRow. It lacks url_canonical, which only the duplicate lookup reads.

Variables

v
DUMP_COLUMNS: string

Lists the stored columns of DumpRow. A column added to the table must be added here, to DumpRow and to isDumpRow.

v
DUMP_SELECT_COLUMNS: string

Selects a whole DumpRow, the stored columns and the computed comment_count and owner_username.

v
DUMP_SELECT_COLUMNS_ALIASED: string

Selects a whole DumpRow in a query that aliases dumps as d to join it with other tables.

api/model/follow.ts

Checks the rows follow-service.ts reads from follows to answer what a user follows.

Functions

f
isFollowRow(obj: unknown): obj is FollowRow

Checks whether obj, a row getFollowStatus selected, is a FollowRow.

Interfaces

I
FollowRow

Read by getFollowStatus. Exactly one of followed_user_id and followed_playlist_id is set, as the table's CHECK constraint requires.

api/model/playlist.ts

Turns the rows the playlist and follow services read from playlists into the Playlist the API sends.

Functions

f
isPlaylistRow(obj: unknown): obj is PlaylistRow

Checks whether a row node:sqlite returned is a PlaylistRow.

f
playlistRowToApi(row: PlaylistRow): Playlist

Converts row to the Playlist the playlist routes send.

Interfaces

I
PlaylistRow

Returned by the playlist and follow services' SELECTs on playlists, which join the owner's name as owner_username. A query may add the dump_count and last_dump_at columns (VISIBLE_DUMP_FIGURES), which playlistRowToApi carries over when present.

api/model/comment.ts

Turns the rows the comment service reads from comments into the Comment the comment routes send.

Functions

f
commentRowToApi(row: CommentRow): Comment

Converts row to the Comment the comment routes send and the WebSocket broadcasts.

f
isCommentRow(obj: unknown): obj is CommentRow

Checks whether a row node:sqlite returned is a CommentRow.

Interfaces

I
CommentRow

Returned by the comment service's SELECTs on comments, joined with users for the author_ columns.

api/model/notification.ts

Turns the rows of notifications, once checked, into the Notification that GET /api/me/notifications answers.

Functions

f
isNotificationRow(obj: unknown): obj is NotificationRow

Checks whether obj, a row getNotificationsForUser selected, is a NotificationRow.

f
notificationRowToApi(row: NotificationRow): Notification

Converts a row to the Notification the client gets.

Interfaces

I
NotificationRow

Stored by the triggers of notification-service.ts, and read back by getNotificationsForUser.

api/model/rows.ts

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.

Functions

api/mail/layout.ts

Every email the site sends shares one layout: the site's emoji and name, then a card holding the content, then a footer. The templates beside this module (welcome, password reset, digest) build their content with an EmailKit, which draws each block in a style from styles.ts, and email-service.ts sends what they return.

Classes

c
EmailKit(look: EmailLook)

Draws an email in one EmailLook. A template writes the email as Markdown and hands it to EmailKit.render, which draws each block in the style. The block methods return HTML: text passed to them must already be escaped with esc.

Functions

f
esc(value: string): string

Escapes value for HTML text and attributes. Everything users wrote (names, titles, quotes) goes through it, since none of it may become markup.

f
escMd(value: string): string

Escapes value for Markdown, so that user text in a plain-text part (a title with * or [, say) stays text.

f
lookOf(settings?: Pick<UserSettings, "style" | "colorScheme"> | null): EmailLook

Returns the look of a reader's settings. An unset style is the instance's, as on the site, and an unset scheme the system's; with no settings at all, as for the welcome and reset emails, both are.

Interfaces

I
EmailLook

Passed to EmailKit, made by lookOf: the instance's style for the emails that aren't a user's own, their settings for a digest.

I
RenderedEmail

Returned by each template to its caller, which hands it to sendEmail with a recipient.

Type Aliases

T
PaletteRole = Exclude<keyof EmailPalette, "gradient">

Names a color of an EmailPalette, as EmailKit.paint takes it, so that the dark block can recolor the element by the same name.

api/mail/welcome.ts

Renders the email that greets a new account. sendWelcomeEmail (user-service.ts) sends it after sign-up, in the instance's style and language. It opens with the instance's GERBEUR_WELCOME_EMAIL_BODY, or the built-in body, then says the digests are on, since they are by default, and where to turn them off.

Functions

f
renderWelcomeEmail(username: string): RenderedEmail

Returns the welcome email of username. The instance's body has {{username}} and {{site_name}} filled in; raw HTML in it passes through, since it comes from whoever runs the instance, never from a user.

api/mail/styles.ts

Carries each site style into email, so a digest looks like the site its reader chose and the other emails like the instance's style. A mail client takes no stylesheet of ours, so each style is resolved here into plain values, which layout.ts inlines.

Interfaces

I
EmailPalette

The colors of one style in one scheme, by role, each read from a token of its theme. The optional ones carry a style's particular touches.

I
EmailStyle

How a style draws an email. layout.ts builds every block from it, so adding a style to shared/theme.ts means adding its Recipe here.

Variables

v
EMAIL_STYLES: Record<StyleName, EmailStyle>

Every style's email look, keyed by its name, read once at load.

api/mail/i18n.ts

Lets the emails speak the instance's language from the same catalogs as the interface. A template passes each message as a descriptor holding its id and its English, as i18n._({ id: "email.reset.title", message: "Reset your password" }), or one of shared/digest-messages.ts, which a page says too. lingui extract reads them into locales/<lang>.po beside the interface's messages, and lingui compile turns the catalogs into the locales/<lang>.mjs imported here, failing when a language lacks one. The ids are named rather than hashed from the English, since the server runs no Lingui macro.

Variables

v
i18n: I18n

Translates the emails' messages, in EMAIL_LOCALE. Every catalog is loaded, so a test can activate another language.

api/mail/theme-tokens.ts

Lets the emails draw with the site's own values. It reads a style's design tokens out of the stylesheets the client loads, src/tokens.css and then src/themes/<style>.css, so styles.ts names a token wherever it needs a color, a gradient, a font or a radius, and a change to a theme reaches its emails with nothing copied to keep in step.

Functions

f
readTokens(
style: StyleName,
scheme: Scheme
): ThemeTokens

Returns the tokens of style in scheme, read from the site's stylesheets at the time of the call. styles.ts reads each style once, at load.

Interfaces

I
ThemeTokens

Returned by readTokens: the tokens of one style in one scheme, read as the kind of value each is.

Type Aliases

T
Scheme = "light" | "dark"

A scheme a token is resolved for. "auto" is drawn from both.

api/mail/password-reset.ts

Renders the email carrying a password reset link. requestPasswordReset (password-reset-service.ts) sends it, in the instance's style and language: whoever asks isn't signed in, so their own settings don't apply.

Functions

f
renderPasswordResetEmail(
username: string,
resetUrl: string,
hours: number
): RenderedEmail

Returns the reset email of username, whose button opens resetUrl. The link stays usable for hours, which the email states.

api/mail/digest.ts

Renders the weekly digest and the monthly recap, as the drafts approved them: the weekly one has four sections (what happened to the reader, what the people and playlists they follow posted, the period's top dumps, and the community), the monthly one only the last two. digest-service.ts gathers a DigestContent and sends what this returns, drawn in the reader's own style and scheme.

Functions

f
renderDigestEmail(
content: DigestContent,
look: EmailLook
): RenderedEmail

Returns the digest content describes, written as Markdown and drawn in look, with its subject and hidden preheader.

Interfaces

I
DigestDump

A dump in a digest's lists, with its links made absolute.

I
DigestHighlight

One line of the weekly digest's "For you", made from a notification.

Type Aliases

api/lib/auth.ts

Checks session and media tokens against the live user row, not just their signature. The auth middleware, the WebSocket route, the media routes and the routes open to anonymous callers all authenticate through it.

Functions

f
authenticate(token: string): Promise<AuthPayload | null>

Returns the payload of a valid session token, or null. Besides the signature and expiry, it refuses a token whose user is gone or which predates a password change. The returned role comes from the database, not the token, since a token lasts 7 days and a role can change under it.

f
authenticateMedia(token: string): Promise<string | null>

Returns the user id behind the ?token= of a file, thumbnail or playlist cover URL, or null when it isn't valid. A session token is accepted too, for clients from before media tokens.

f
parseOptionalAuth(ctx: Context): Promise<string | null>

Returns the caller's user id on a route open to anonymous callers, or null when the request has no token. A placeholder such as Bearer undefined counts as no token. An invalid token throws instead of reading as anonymous, so that a stale session gets a 401 the client signs out on, rather than a misleading 404 on a private resource.

api/lib/upload.ts

Checks the images users upload and serves them back, scaled down to the width a request names. The avatar, playlist cover, dump thumbnail and attachment routes call it.

Functions

f
detectImageMime(data: Uint8Array): string | null

Returns the MIME type of a JPEG, PNG, GIF or WebP image from its magic bytes, or null for anything else. These four are the only image types accepted anywhere. The type the browser declared is never trusted, since the file is later served with the type found here.

f
replaceUploadedFile(
filePath: string,
data: Uint8Array
): Promise<void>

Writes data to filePath, replacing whatever is there only once the whole file is written, so a failed write leaves the old file whole. The upload routes call it before recording the new file, so that clients told of it never fetch it early.

f
scalableImage(head: Uint8Array): ScalableImage | null

Returns how to scale the image whose first IMAGE_HEAD_BYTES are head, or null when it is to be served as it is: an animated WebP, which ffmpeg can't read, or anything but a JPEG, PNG, GIF, WebP or BMP.

f
serveImage(
ctx: Context,
path: string,
mime: string,
cacheControl: string,
defaultWidth?: ImageWidth
): Promise<boolean>

Sends the image at path, of type mime, with cacheControl: a WebP copy at most as wide as the request's ?w= names (shared/image.ts), or as defaultWidth without one, and the image as it is when the request names no width or the copy can't be made. With ?f=compat, the copy is a JPEG, or a PNG for an image that may be transparent, which the digest's emails ask for, since not every mail client reads WebP. Every route that answers one of the site's own images ends here, so each takes both. It resolves false, having sent nothing, when the file is missing.

f
serveUploadedFile(
ctx: Context,
filePath: string,
mime: string,
cacheMaxAge?: number,
isPrivate?: boolean,
defaultWidth?: ImageWidth
): Promise<boolean>

Serves an uploaded image inline as mime, or scaled down as serveImage does, cached for cacheMaxAge seconds, and only by the browser when isPrivate. It answers 404 and resolves false when the file is missing. The caller checks first that the viewer may see it.

f
validateImageUpload(data: Uint8Array): string

Checks an uploaded image's size and content, and returns its MIME type, which the route stores to serve the image with.

Interfaces

I
ScalableImage

Returned by scalableImage, for scaleImage.

  • compat: "jpg" | "png"

    The format of its copy for a reader without WebP: a JPEG, or a PNG for an image that may be transparent, which a JPEG would fill with black.

api/lib/static.ts

Serves the built frontend, as its static files and, for every client-side route, index.html with its placeholders filled. main.ts installs it after the routers.

Functions

f
routeStaticFilesFrom(staticPaths: string[]): (
context: Context<Record<string, object>>,
next: Next
) => Promise<void>

Returns the middleware that serves a static file from the first of staticPaths that has it, and index.html of the first for any other path. Hashed build output is cached for a year, and a missing chunk is a 404. main.ts passes dist/ then public/.

api/lib/permissions.ts

Checks an authenticated caller's permissions. Routes call it where the answer depends on more than the permission, such as "the owner, or a moderator"; requirePermission gates a whole route instead.

Functions

f
can(
payload: AuthPayload,
permission: Permission
): boolean

Checks whether the caller's role grants permission. The role is the live one authenticate put in the payload, not the token's.

api/lib/ttl-cache.ts

Keeps what the server fetched from another site for a while, by key, so a page or a feed asking again doesn't refetch it: Bandcamp tracklists, YouTube playlists, and link previews. Each holder sets how long an entry lives and how many are kept; past that count, the oldest written goes first.

Classes

c
TtlCache<V>(
ttlMs: number,
maxEntries: number
)

A map whose entries expire ttlMs after they are written, holding maxEntries at most.

api/lib/api-exception.ts

Lets any layer fail a request with an HTTP status, which the error middleware turns into the response.

Classes

c
APIException(
status: APIStatus,
message: string
)

Thrown by a route, a service or a remote fetch to answer the request with an error. The error middleware sends it as an APIFailure, message included, so the message must be fit for the user. Use 401 for a caller who isn't signed in, 403 for one who is but may not, and 404 rather than 403 when the caller mustn't learn that the thing exists.

Type Aliases

T
APIStatus = 400 | 401 | 403 | 404 | 409 | 429 | 500 | 502

Passed to an APIException; any other status is a type error.

api/lib/css-color.ts

Reads the CSS colors the server meets: a dumped page's theme-color, which the generic link preview keeps as its tint (normalizeCssColor), and the theme stylesheets' tokens, which the emails resolve (theme-tokens.ts).

Functions

f
hexOf(unnamed 0: Rgba): string

Writes color as a lowercase #rrggbb, its alpha dropped.

f
parseCssColor(text: string): Rgba | undefined

Returns the color text writes as a hex (3, 4, 6 or 8 digits), an rgb() or rgba() of numbers or percentages, or one of the common names; undefined for any other form. Channels out of range are clamped.

Interfaces

I
Rgba

An sRGB color with its alpha, each channel 0–255 and alpha 0–1.

api/lib/canonical-url.ts

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?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.

Functions

f
canonicalizeUrl(raw: string): string | null

Returns the canonical key of raw, or null when it isn't an http(s) URL. A YouTube video or playlist becomes one fixed URL whatever shape it came in.

f
youtubeHost(u: URL): string | null

Returns u's host in lower case and without "www.", when it is one of YouTube's (youtu.be aside), and null otherwise. The YouTube preview provider uses it too, so both agree on what a YouTube link is.

f
youtubeVideoId(
host: string,
pathname: string,
params: URLSearchParams
): string | null

Returns the id of the video a YouTube URL points at, in any of the shapes people paste (youtu.be/ID, /watch?v=ID, /embed/ID, /shorts/ID, /live/ID), and null for any other URL. host is lower case, without "www.". The canonical key drops the timestamp, since the same video linked at 2:30 is still a repost; the YouTube provider reads it separately.

api/lib/site-icons.ts

Renders the site's favicons and web manifest from its emoji. The icons task (api/scripts/generate-icons.ts) runs it before the server starts, so the emoji can change without a rebuild.

Functions

f
emojiToCodepoint(emoji: string): string

Returns the name Twemoji files emoji under: its codepoints in hex, joined with "-", without variation selectors (FE0F) unless the emoji is a ZWJ sequence. lib/index-html.ts also uses it as the icon URLs' cache-busting version.

f
generateSiteIcons(opts: GenerateSiteIconsOptions): Promise<void>

Writes favicon.svg, favicon.ico, favicon-<size>x<size>.png (16 to 512), apple-touch-icon.png and manifest.webmanifest into opts.outDir, the directory the site's static files are served from.

Interfaces

I
GenerateSiteIconsOptions

Passed to generateSiteIcons by api/scripts/generate-icons.ts, from the site's config.

api/lib/playlist-members.ts

Decides which of a playlist's dumps a viewer may see. The playlist service filters its listings with it, the WebSocket service filters each client's playlist_dumps_updated message the same way, and the queries that list playlists count their dumps by the same rule.

Functions

f
dumpFiguresFor(
members: DatedPlaylistMember[],
playlist: Pick<Playlist, "userId">,
viewerId: string | null | undefined
):
Required<Pick<Playlist, "dumpCount">>
& Pick<Playlist, "lastDumpAt">

Returns what playlist says of its dumps to the viewer, members being all of them: the dumpCount and lastDumpAt that VISIBLE_DUMP_FIGURES reads in a query. The WebSocket service puts them on the playlist each socket is sent.

f
isMemberVisibleTo(
member: PlaylistMember,
playlist: Pick<Playlist, "userId">,
viewerId: string | null | undefined
): boolean

Checks whether viewerId may see member in playlist. A public dump is visible to all. A private dump is visible only to the playlist's owner, and only if they posted it: moderators don't see it, and neither does the owner once someone else's dump in their playlist goes private. A null or undefined viewerId is a signed-out viewer.

Interfaces

I
DatedPlaylistMember

The fields of a playlist's dump that dumpFiguresFor needs: those that tell who may see it, and when it was added. The playlist service reads them for its broadcasts.

I
PlaylistMember

The fields of a playlist's dump that isMemberVisibleTo needs. A Dump has them. For its broadcasts, the playlist service reads only these.

Variables

v
VISIBLE_DUMP_FIGURES: string

The dump_count and last_dump_at columns of a query on playlists p: how many of the playlist's dumps the viewer may see, by isMemberVisibleTo's rule, and when the latest of them was added, null without any. Both read the viewer's id, or "" for a signed-out one, as ?1: it is the query's first parameter, and each ? after it takes the next.

api/lib/serve-file.ts

Sends files from disk with ETags and byte ranges, streamed a chunk at a time. The files and thumbnails routes call it, and lib/upload.ts for the other uploads.

Functions

f
serveFile(
ctx: Context,
path: string,
unnamed 2: ServeFileOptions
): Promise<boolean>

Sends a file from disk with an ETag, answering 304 when the client already has it, and single byte ranges when ranges is set. It returns false, having sent nothing, when the file doesn't exist, and leaves the 404 to the caller.

Interfaces

I
ServeFileOptions

Passed to serveFile by its caller, which knows the file's type and how long it may be cached.

api/lib/ffmpeg.ts

Makes what the server derives from an upload with ffmpeg: a video's still, which the thumbnails route serves, an audio file's waveform, which the waveform route serves, and the scaled-down copies of any image, which serveImage (lib/upload.ts) serves for every route that answers one. Each is made on its first request and kept under a name this module gives, a dump's in THUMBNAILS_DIR and the scaled copies in SCALED_DIR, until dump-service replaces or deletes the upload and removeGeneratedMedia drops them. generatedMediaSweepJob takes what that leaves behind.

Functions

f
audioWaveform(
srcPath: string,
outPath: string
): Promise<void>

Writes the waveform of the audio file srcPath as the waveform route's whole response, so the route serves it as a file, with its ETag.

f
ensureGenerated(
outPath: string,
generate: () => Promise<void>,
srcPath?: string
): Promise<boolean>

Makes outPath with generate unless it exists. Resolves false when it can't be made, because ffmpeg is missing or failed.

f
generatedPaths(dumpId: string): { still: string; waveform: string; }

Returns where the still and the waveform generated from dump dumpId's upload are kept.

f
removeGeneratedMedia(dumpId: string): Promise<void>

Deletes what was generated from dump dumpId's upload, once dump-service has replaced or deleted it. The routes make them again from a new one.

f
removeScaledImages(srcPath: string): Promise<void>

Deletes the scaled copies of the upload at srcPath, once it is deleted. One that is replaced needs no call: its copies are made again, being older than it (ensureGenerated).

f
scaledImagePath(
srcPath: string,
width: ImageWidth,
format?: ScaledFormat
): string

Returns where the copy of the upload at srcPath scaled to width is kept, in format: under SCALED_DIR, at the upload's own path below UPLOADS_DIR.

f
scaleImage(
srcPath: string,
outPath: string,
width: ImageWidth,
unnamed 3: ImageTraits,
format?: ScaledFormat
): Promise<void>

Writes a copy of an image at most width wide, never wider than the image itself, as a WebP unless format names another. WebP keeps a PNG's transparency and a GIF's animation, at a fraction of either's bytes.

f
scaleImageBytes(
bytes: Uint8Array,
width: ImageWidth,
traits: ImageTraits
): Promise<Uint8Array | null>

Returns bytes, an image held in memory, scaled as scaleImage does, or null when it can't be, because ffmpeg is missing or failed. The image proxy scales what it fetched with it, and keeps the result itself.

f
sweepGeneratedMedia(): Promise<number>

Deletes the generated files nothing serves any more, and returns how many: the scaled copies of an upload that is gone, which the services' own removals missed or never made, as for a user's avatar; the previews of image dumps under their former name, <id>.preview.jpg or .png, which the scaled copies replaced; and the temporary files a generation left unfinished. Nothing else of THUMBNAILS_DIR is touched, the custom thumbnails kept there being uploads.

f
videoStill(
srcPath: string,
outPath: string
): Promise<void>

Writes a 320-pixel-wide JPEG still of a video, one second in.

Interfaces

I
ImageTraits

What scaleImage is told of the image it scales, which scalableImage (lib/upload.ts) reads from its first bytes.

  • animated: boolean

    Whether its frames are an animation, as a GIF's are. A still may hold more than one image too, a phone's JPEG carrying a gain map or a depth map after the photo, and ffmpeg reads each as a frame.

  • orientation: number

    The EXIF orientation to turn the image by, 1 when it is upright.

Type Aliases

T
ScaledFormat = "webp" | "jpg" | "png"

The formats a scaled copy comes in: WebP for a browser, and a JPEG or a PNG for what doesn't read it, as some mail clients don't.

Variables

v
generatedMediaSweepJob: Job

Runs sweepGeneratedMedia a minute after the server starts, which clears what a new version no longer uses as soon as it is deployed, then once a day. main.ts starts it.

api/lib/jwt.ts

Signs and verifies the server's four kinds of token (session, media, password reset and unsubscribe), all HS256 with the one key. Only a token's signature and expiry are checked here: lib/auth.ts checks a session or media token against its user.

Functions

f
createJWT(payload: Omit<AuthPayload, "exp">): Promise<string>

Signs a session token carrying payload, valid for 7 days. issueTokens issues it on sign-up, sign-in and password change, and the client sends it as a bearer token.

f
createMediaToken(userId: string): Promise<string>

Signs a media token for userId, valid for 7 days. Media elements can't send an Authorization header, so the URLs of private files, thumbnails and playlist covers carry this token as ?token=, where it lands in logs and browser history. It only reads media, so a leaked URL doesn't expose the account.

f
createPasswordResetToken(userId: string): Promise<string>

Signs a password reset token for userId, valid for an hour. The token isn't single-use by itself: requestPasswordReset stores it, and resetPassword marks it used.

f
createUnsubscribeToken(
userId: string,
cadence: DigestCadence
): Promise<string>

Signs the token of a digest's unsubscribe link, which turns off cadence for userId without signing in. It never expires, so the link in an old email still works; turning the digest back on doesn't revoke it, which is harmless since all it can do is turn it off again.

f
generateInstanceKey(): Promise<{ kid: string; privateJwk: JWK; publicJwk: JWK; }>

Returns a new Ed25519 key pair as JWKs, with a random kid, for instance-key-service.ts to store when the instance has none. The public JWK carries the kid, alg and use a JWKS entry needs.

f
importInstanceKey(
kid: string,
privateJwk: JWK
): Promise<InstanceSigningKey>

Returns the stored private JWK privateJwk as a key to sign with.

f
issueTokens(user: { id: string; username: string; role: Role; }): Promise<AuthTokens>

Returns a new session token and media token for user, as sign-up (POST /api/users), sign-in (POST /api/sessions) and a password change (PUT /api/me/password) answer them.

f
signInstanceToken(
payload: JWTPayload,
key: InstanceSigningKey,
claims: { issuer: string; audience: string; subject: string; },
expiresIn: string
): Promise<string>

Signs payload with the instance's key, from issuer (this instance) to audience (the instance it is for), valid for expiresIn, with a random jti. The identity tokens of POST /api/oauth/tokens and the events of the outbox are signed with it.

f
verifyInstanceToken(
token: string,
jwks: JSONWebKeySet,
claims: { issuer: string; audience: string; }
): Promise<JWTPayload>

Returns the payload of token once its signature checks against jwks, the keys of issuer, and it is addressed to audience and unexpired.

f
verifyJWT(token: string): Promise<AuthPayload | null>

Returns the payload of a session token whose signature and expiry are valid, or null. Call authenticate (lib/auth.ts) instead, which also checks the token against its user.

f
verifyMediaToken(token: string): Promise<{ userId: string; iat?: number; } | null>

Returns the user id and issue time of a media token, or null when it is invalid, expired or another kind of token.

f
verifyPasswordResetToken(token: string): Promise<PasswordResetPayload | null>

Returns the payload of a password reset token, or null when it is invalid, expired or another kind of token. Whether it was used is the caller's check.

f
verifyUnsubscribeToken(token: string): Promise<{ userId: string; cadence: DigestCadence; } | null>

Returns the user and cadence of an unsubscribe token, or null when it is invalid or another kind of token.

Interfaces

I
InstanceSigningKey

An instance's key, ready to sign, from importInstanceKey. Its kid names the public half in the instance's JWKS.

api/lib/index-html.ts

Fills the placeholders of the frontend's index.html with settings read at runtime, so they change with a restart and no rebuild. The static and OG middlewares serve what it returns.

Functions

f
loadIndexHtml(root: string): Promise<string>

Returns ${root}/index.html with the site name, emoji, icon version, Bandcamp player mode, default style, and whether email and each way of federated sign-in are on filled in.

api/lib/self-url.ts

Recognises links to this site's own pages. The self provider previews them from the database, remote/fetch.ts accepts them as dump links even on a private host such as localhost, and migration 0021 rewrote the v3 ones in stored text.

Functions

f
isSelfHost(host: string): boolean

Checks whether host (with its port, as URL.host gives it) is where this site is served, at its public URL or its frontend's.

f
isSelfUrl(url: URL): boolean

Checks whether url links to one of this site's own dump, user or playlist pages, by parsePagePath; an editor doesn't count. Matched on the host as well as the path: /@bob on some other site is someone else's page, not ours.

api/lib/rate-limit.ts

Limits how often something may happen, with token buckets kept in memory, which a restart empties. Logins, password reset requests and the chat's typing messages use it.

Functions

f
takeToken(
key: string,
limit: RateLimit
): boolean

Takes one token from key's bucket, or returns false when it is empty. The caller picks the key, such as typing:<userId>, and keys by user or account so that more tabs or parallel requests don't buy more tokens.

Interfaces

I
RateLimit

Passed to takeToken by each limited action, which declares its own as a constant.

Variables

v
rateLimitSweepJob: Job

Drops the buckets untouched for an hour, every minute. Every limit in use refills within the hour, so such a bucket is full and dropping it changes nothing. main.ts starts it.

api/lib/like.ts

Builds the patterns of the search queries' LIKE, for the dump, playlist and user searches. A query's % and _ are matched as themselves rather than as wildcards: usernames hold underscores, and "a_b" must not find "axb".

Functions

f
likePattern(
query: string,
where?: "anywhere" | "prefix"
): string

Returns the pattern matching text that contains query, or that starts with it when where is "prefix". The SQL must read LIKE ? ESCAPE '\', which the LIKE fragment spells.

Variables

v
LIKE: "LIKE ? ESCAPE '\\'"

Spells LIKE ? ESCAPE '\', for a likePattern parameter.

api/lib/slugify.ts

Picks and checks the slugs of dumps and playlists, which name them in their page URLs under their owner: /@bob/dumps/<slug>, /@bob/playlists/<slug>. Each kind has its own slug space per owner, shared by the items' current slugs and their aliases (the slugs they had before). services/slug-service.ts changes and resolves slugs with these, and migration 0021 gave every item one; both pass the database, since this module doesn't open it.

Functions

f
slugHolder(
db: DatabaseSync,
kind: SlugKind,
userId: string,
slug: string
): string | undefined

Returns the id of the item whose current slug or alias is slug in userId's space for kind, or undefined when it is free.

f
uniqueSlug(
db: DatabaseSync,
kind: SlugKind,
userId: string,
base: string
): string

Returns base, or base-2, base-3… cut to fit, whichever is free in userId's space for kind. base must be a valid slug, as slugify makes. The services take it on creating an item, the migration on re-slugging one, inside the transaction that writes it.

f
validateSlug(
slug: string,
max?: number
): void

Throws a 400 unless slug is a valid slug, lowercase words of letters and digits joined by hyphens, at most max long. slug-service checks a dump's or a playlist's with it, and category-service a category's, with that kind's own limit.

Type Aliases

T
SlugKind = "dump" | "playlist"

A kind of item that has a slug under its owner.

Variables

api/lib/password.ts

Hashes and checks passwords, for sign-up, sign-in, password changes and the admin that db/startup.ts seeds. It reads no config, so the startup tasks can use it before the server's settings are checked.

Functions

f
hashPassword(password: string): Promise<string>

Hashes password with scrypt and a random salt, as hash.salt in hex, for users.password_hash.

f
verifyPassword(
password: string,
storedHash: string
): Promise<boolean>

Checks password against a hash made by hashPassword.

api/lib/reserved-slugs.ts

Checks which slugs a dump category may take. A dump category's feed lives at the URL root (/music), so its slug must not shadow another top-level page. The category service runs this check when a dump category is created or renamed, and the migrations that reserve more words rename the categories that had one with renameReservedCategories. A playlist category's page is /playlists/<slug>, under a reserved word, so it reserves nothing.

Functions

f
isReservedSlug(slug: string): boolean

Checks whether slug is one of the RESERVED_SLUGS.

f
renameReservedCategories(db: DatabaseSync): void

Renames each category whose slug is now reserved, such as hot, to the first of <slug>-2, <slug>-3… that is free, and logs it. Migration 0021 runs it for v4's words, 0022 for community, 0024 for authorize and federation, and 0025 for playlists.

Variables

v
RESERVED_SLUGS: readonly string[]

Lists the top-level path segments a category slug may not take: the feed tabs (/hot), the SPA's own pages (src/App.tsx), and api, ws, assets and emojibase, which the server answers before the SPA. Keep it in step with the route table and the build's top-level folders.

api/lib/scheduler.ts

Runs the server's recurring tasks. Each module that needs one exports a Job, and main.ts passes them all to startJobs when it runs directly, so nothing recurs in the tests, which call a job's run when they need it.

Functions

f
startJobs(jobs: readonly Job[]): void

Starts running each of jobs on its own timer. A run still going when the next is due makes that one skip, and a run that throws or rejects is logged and the next one runs anyway.

Interfaces

I
Job

A recurring task, exported by its module for main.ts to start.

api/lib/dump-media.ts

Resolves the dump whose media a route under /api/dumps/:dumpId/ serves: its file (routes/files.ts), its thumbnail (routes/thumbnails.ts) or its waveform (routes/waveforms.ts). Media elements and <img>s can't send an Authorization header, so these routes take the viewer's media token from the query string, and are cached by the file's version.

Functions

f
resolveMediaDump(
ctx: Context,
dumpId: string
): Promise<{ dump: Dump; cacheControl: string; }>

Returns dump dumpId as the viewer of ctx's ?token= may see it, and the Cache-Control its media get: kept for good under a URL naming the file's current version (?v=, as dumpFileVersion gives), revalidated under any other, and never in a shared cache for a private dump. It also keeps the token out of the Referer of whatever the response links to.

api/lib/pagination.ts

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.

Functions

f
pageOf<T>(
rows: T[],
limit: number
): Page<T>

Turns rows fetched with LIMIT limit + 1 into a page. The extra row, when it comes back, is dropped and tells that another page follows, which spares a COUNT(*) over the whole list.

f
parsePagination(
params: URLSearchParams,
defaultLimit?
): { page: number; limit: number; }

Reads the page and limit query parameters of a list route. It never fails: a missing or invalid page reads as 1, and limit is kept between 1 and PAGINATION_MAX_LIMIT.

Interfaces

I
Page

Returned to the routes by the services' list functions, and sent as PaginatedData.

api/routes/follows.ts

Serves what the caller follows, and follows or stops following for them. The dumps of what they follow, the index's "follow" tab, are GET /api/dumps?follow='s (routes/dumps.ts), and what another user follows is routes/users.ts's.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/chat.ts

Serves the chat's history to the chatbox (ChatModal). Messages are sent, edited and deleted over the socket instead (routes/ws.ts).

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/invites.ts

Lets any signed-in user invite someone, from their profile page. The invite link opens the /register page.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/well-known.ts

Serves what other gerbeur instances read about this one before federated sign-in, at the root paths OpenID Connect fixes, which are the one exception to the API's /api/ rule. remote/gerbeur-instance.ts reads them on the other side.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/users.ts

Serves sign-up and user profiles, with what each user posted, upvoted, made, follows and invited, and the dumps and playlists under them. A user's :key is their id or @name, a current or former name in any case (resolveUserKey). A profile sent to anyone goes through toPublicUser, which drops the email. The caller's own account is routes/me.ts's.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/federation.ts

Lets users of other gerbeur instances sign in here with their account there, as federation-service.ts describes, and lets admins pick the instances to trust or block. The login and register pages start a sign-in, the /federation/callback page finishes it, /manage edits the peers, and the other instances' servers send their events.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/search.ts

Searches dumps, users and playlists at once, for the client's Search page. Only the dumps are paged. Private dumps and playlists are found only by their owner.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/notifications.ts

Serves the caller's own notifications to the client's /notifications page, which marks them all read when it opens. New ones arrive over the WebSocket instead, as notification_created.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/avatars.ts

Stores and serves the users' avatars. The profile page uploads the avatar, and the client's Avatar component loads it at the size it shows, with the user's updatedAt as a cache-busting version.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/files.ts

Serves and replaces file dumps' uploads, a sub-resource of their dump. The players, viewers and download links of the frontend build the URL with dumpFileUrl, and its edit page replaces the file.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/playlists.ts

Serves playlists, their dumps and their covers. :playlistId is a playlist's id; its page reads it by its path, through GET /api/users/:key/playlists/:slug.

Variables

v
default: Router<AuthState>

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/categories.ts

Serves the categories dumps and playlists are filed under, a set for each kind, and changes them for those with category:manage. A dump category's feed is GET /api/dumps?category=:slug, a playlist category's GET /api/playlists?category=:slug.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/waveforms.ts

Serves an audio file dump's waveform, a sub-resource of its dump, for the player to seek on. The frontend builds the URL with dumpWaveformUrl. lib/ffmpeg.ts makes it from the upload on first request and keeps it, so a listener downloads a few hundred bytes of peaks rather than the file a second time to decode it.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/attachments.ts

Stores and serves the images pasted or dropped into the text editor, which puts each one in the body as a link to its GET URL.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/oauth.ts

Lets this instance's users sign in to other gerbeur instances with their account here, as identity-provider-service.ts describes. The client's /authorize page calls the first two; the other instance's server calls the last.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/comments.ts

Serves the comments on dumps. Comments are broadcast over the WebSocket as they are posted, edited and deleted, to everyone, or to its poster alone for a private dump, whose comments 404 for anyone else.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/community.ts

Serves the figures of the client's /community page, which community-service.ts recomputes on a timer, and the community feed.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/thumbnails.ts

Serves and changes dumps' thumbnails, a sub-resource of their dump. The cards and pages that show a file dump build the URL with dumpThumbnailUrl, and the edit page uploads a custom one. Thumbnails made from an upload are generated on first request by lib/ffmpeg.ts, which keeps them.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/ws.ts

Opens the WebSocket that WSProvider keeps for each tab, and handles what the client sends over it. Voting, comment likes and every chat write go through here, not through HTTP. The messages are typed in shared/ws.ts, and services/ws-service.ts sends the broadcasts.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/remote.ts

Reads other sites for the client, as api/remote/ does for the server: the previews of links on the create form, the images its thumbnails can't load directly, and the tracks the players list. On any failure of the last two, the player falls back to the embed.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/dumps.ts

Posts, reads, edits and deletes dumps, for the feeds, the dump page, its edit page and the create form. :dumpId is a dump's id; the dump page reads a dump by its path, through GET /api/users/:key/dumps/:slug. A private dump answers 404 to anyone but its poster, moderators included. Its file and thumbnail are served by routes/files.ts and routes/thumbnails.ts, its comments by routes/comments.ts.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/sessions.ts

Signs users in, from the client's /login page. A session is a token the client keeps; signing out forgets it, with no request.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/unsubscribe.ts

Turns a digest off from the link in its email, without signing in. Mail clients post here for a one-click unsubscribe (RFC 8058), and so does the client's /unsubscribe page.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/me.ts

Serves the caller's own account: their user, settings, password and media token, and which of their playlists hold a dump. What they follow, their feed and their notifications are under /api/me too, served by routes/follows.ts and routes/notifications.ts, and their avatar by routes/avatars.ts.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/routes/password-resets.ts

Resets forgotten passwords. The client's /login page asks for a reset link, which the email takes to the /reset-password page, whose token names the reset.

Variables

v
default: Router

Mounted by api/main.ts behind the error, Open Graph and CORS middleware. An APIException thrown by a handler becomes the JSON error response there.

api/services/attachment-service.ts

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 linkAttachments.

Functions

f
createAttachment(
id: string,
mime: string
): void

Records attachment id once POST /api/attachments has written its file. Until a saved body links it, the startup purge deletes it after ORPHANED_ATTACHMENTS_RETENTION_HOURS.

f
getAttachmentAccess(attachmentId: string): AttachmentAccess

Returns attachment attachmentId's type and who may see it. An attachment nothing links yet is public, so the editor can show it before the text is saved; its id is unguessable. One linked by a private dump or playlist, or by a comment on a private dump, is its owner's.

f
linkAttachments(
text: string,
resourceType: AttachmentResourceType,
resourceId: string
): void

Binds the attachments whose URLs appear in text to the resource that saved it, so the startup purge keeps them as long as that resource exists, and they are seen by whoever may see it. It is called whenever a dump, comment, chat message, playlist or profile description is saved. An attachment already bound stays with its resource, so pasting another's URL claims nothing, and removing the link from a body never unbinds it.

Interfaces

I
AttachmentAccess

Returned to GET /api/attachments/:attachmentId by getAttachmentAccess. ownerId is set when only that user may see the image, because the resource linking it is private.

Type Aliases

T
AttachmentResourceType = "dump" | "comment" | "playlist" | "chat" | "user"

Stored in attachments.resource_type by bindAttachments, it names the kind of resource whose saved text links the attachment.

api/services/invite-service.ts

Makes invites, and checks and claims them at sign-up. An invite works once, for UNUSED_INVITES_RETENTION_DAYS days. routes/invites.ts makes and checks them, and POST /api/users claims one.

Functions

f
claimInvite(token: string): string

Marks invite token used and returns the inviter's id. Call it before the async account creation, and releaseInvite if that fails: checking first and marking after would let parallel sign-ups share one invite.

f
createInvite(inviterId: string): string

Makes an invite from inviterId and returns its token, which the client puts in the invite link.

f
releaseInvite(token: string): void

Makes a claimed invite usable again, when the sign-up that claimed it failed.

f
validateInvite(token: string): string

Returns the id of the inviter when invite token is unused and still in its window, which is the one after which db/startup.ts purges it. It doesn't claim the invite: sign-up goes through claimInvite.

api/services/follow-service.ts

Stores who follows which users and playlists, and builds the feeds and lists made from it. routes/follows.ts serves the caller's own, and routes/users.ts what any user follows.

Functions

f
followPlaylist(
followerId: string,
playlistId: string
): void

Makes followerId follow a public playlist, and notifies its owner. Following again changes nothing. A follow outlives the playlist going private, but the feeds and lists skip it while it is.

f
followUser(
followerId: string,
followedUserId: string
): void

Makes followerId follow followedUserId, and notifies the followed user. Following again changes nothing. The follower's other tabs are told.

f
getFollowedPlaylistsByUser(
userId: string,
requestingUserId: string | null,
page: number,
limit: number
): Page<Playlist>

Returns a page of the public playlists userId follows, most recently followed first, for their profile and playlists pages. Their dumps are counted for requestingUserId, null being a signed-out viewer.

f
getFollowedPlaylistsDumpFeed(
followerId: string,
page: number,
limit: number,
categoryId?: string
): Page<Dump>

Returns a page of the public dumps in the public playlists followerId follows, optionally only those in categoryId. It feeds the "playlists" half of the index's "followed" tab. A dump in several of them shows once, dated by its latest addition.

f
getFollowedUsersByUser(
userId: string,
page: number,
limit: number
): Page<User>

Returns a page of the users userId follows, most recently followed first, for their profile page. The users carry their password hash and email: pass them through toPublicUser before sending them.

f
getFollowedUsersDumpFeed(
followerId: string,
page: number,
limit: number,
categoryId?: string
): Page<Dump>

Returns a page of the public dumps posted by the users followerId follows, newest first, optionally only those in categoryId. It feeds the "users" half of the index's "followed" tab.

f
getFollowStatus(followerId: string): FollowStatus

Returns the ids of every user and playlist followerId follows, private playlists included, for GET /api/me/follow.

f
unfollowPlaylist(
followerId: string,
playlistId: string
): void

Makes followerId stop following a playlist, if they did.

f
unfollowUser(
followerId: string,
followedUserId: string
): void

Makes followerId stop following followedUserId, if they did.

api/services/notification-service.ts

Stores notifications, pushes each new one to its user's sockets as notification_created, and reads them back for routes/notifications.ts. The services of follows, votes, likes, comments, dumps, playlists and the chat call its notify… triggers. Every trigger passes a source key, so the same event never notifies a user twice. What a notification names by id is joined each time it is sent (withCurrentRefs), so its names and links follow renames.

Functions

f
getNotificationsForUser(
userId: string,
page: number,
limit: number
): Page<Notification>

Returns a page of userId's notifications, newest first, with their refs joined, for GET /api/me/notifications.

f
getUnreadCount(userId: string): number

Returns how many of userId's notifications are unread. The WebSocket route sends it in its welcome message, for the bell's count.

f
markAllRead(userId: string): void

Marks every notification of userId read, when they open the /notifications page, and tells all their sockets, so that the bell clears in their other tabs and devices too.

f
notifyCommentOwnerLike(
likerId: string,
commentId: string
): void

Notifies a comment's author that likerId likes it, unless they are the author, and once however often the like is withdrawn and given again.

f
notifyDumpOwnerNewComment(
commenterId: string,
commentId: string,
dumpId: string
): void

Notifies a dump's poster that commenterId commented on it, unless they are the poster. createComment calls it for replies too, but not on an edit.

f
notifyDumpOwnerUpvote(
voterId: string,
dumpId: string
): void

Notifies a dump's poster that voterId upvoted it, unless they are the poster, and once however often the vote is withdrawn and cast again. castVote calls it.

f
notifyEveryone(
mentionerUserId: string,
messageId: string,
skipUserId?: (userId: string) => boolean
): void

Notifies every user but its author of chat message messageId, which says @everyone. The WebSocket route calls it, after notifyMentions so that a user also named is told by name, when the message is sent flagged mentionsEveryone, or when an edit flags it. Each user is notified once per message, however often it is edited. skipUserId spares the users who have the chat open, as for a mention.

f
notifyMentions(
mentionerUserId: string,
body: string,
contextType: "comment" | "dump" | "playlist" | "chat",
contextId: string,
contextTitle: string,
dumpId?: string,
skipUserId?: (userId: string) => boolean
): void

Notifies every user @mentioned in body, except its author, that they were mentioned in the comment, dump, playlist or chat message contextId. Call it again after each edit: a user already notified for this context isn't notified twice. skipUserId spares a mentioned user; the chat uses it for those who have it open.

f
notifyPlaylistFollowersNewDump(
playlistId: string,
playlistTitle: string,
dumpId: string,
dumpTitle: string
): void

Notifies everyone following a playlist that a dump was added to it. The playlist service calls it only when both are public, since a follower can't open a private one.

f
notifyPlaylistFollowersNowPublic(of: { dumpId: string; } | { playlistId: string; }): void

Notifies the followers of each playlist of the dumps in it that became visible to them: a playlist's followers are told of a dump once both are public. The dump service calls it with dumpId when a dump goes public, for each public playlist holding it; the playlist service with playlistId when a playlist goes public, for each public dump in it, from the bottom of the playlist up. A dump the followers were told of before either went private isn't notified again. Adding a dump to a public playlist calls notifyPlaylistFollowersNewDump directly.

f
notifyPlaylistOwnerNewFollower(
followerId: string,
playlistId: string
): void

Notifies a playlist's owner that followerId follows it, unless they are the owner, and once however often they unfollow and follow again. followPlaylist calls it.

f
notifyUserFollowersNewDump(
dumperId: string,
dumpId: string,
dumpTitle: string
): void

Notifies everyone following dumperId that they posted a dump. The dump service calls it for a public dump only, when it is created or goes public. A follower is told of a dump once, however often it goes private and public again.

f
notifyUserNewFollower(
followerId: string,
followedUserId: string
): void

Notifies followedUserId that followerId follows them, once however often they unfollow and follow again. followUser calls it.

f
withCurrentRefs(data: NotificationData): NotificationData

Returns data with what may have changed since it was stored joined from the database: each user's current name, and the slug and owner of its dump and playlist, which the client builds their paths from. A user since deleted keeps their stored name; a deleted dump or playlist gets no ref, and so no link. Every notification the server sends goes through it, and so do the digests.

api/services/rich-content-service.ts

Fetches a link's preview through the provider that knows its site, and reuses it for a few minutes. dump-service.ts calls it when a link is posted, changed or refreshed, and GET /api/remote/preview when the create form previews one.

Functions

f
clearRichContentCache(): void

Forgets every cached preview. The tests' stubNetwork calls it, since previews fetched from the previous stubbed network no longer apply.

f
fetchRichContent(url: string): Promise<RichContent | undefined>

Returns the preview of url, or a stub holding its host name when the page can't be reached. createUrlDump and updateDump use it for a new link, which has no earlier preview to keep.

f
tryFetchRichContent(
url: string,
unnamed 1?: { fresh?: boolean; }
): Promise<FetchRichContentResult>

Fetches the preview of url, and says whether the page could be reached. A preview fetched in the last five minutes is reused unless fresh is set, as a metadata refresh does. It never rejects: a failure resolves to a stub.

Interfaces

I
FetchRichContentResult

Returned by tryFetchRichContent to GET /api/remote/preview, which reports ok as reached, and to refreshDumpMetadata, which gives up when it is false.

  • content: RichContent

    On failure, a stub holding the URL and its host name. Missing only when the URL doesn't parse.

  • ok: boolean

    False when the page couldn't be reached at all, as opposed to reached and found to carry no metadata. A caller that already holds metadata must not overwrite it with the failure stub.

api/services/playlist-service.ts

Stores playlists and the ordered dumps in them. The playlist routes call it, and so do the search, users and dumps routes to list playlists. Every write is broadcast over the WebSocket, and a viewer is only ever sent the dumps isMemberVisibleTo lets them see.

Functions

f
addDumpToPlaylist(
playlistId: string,
dumpId: string,
requestingUserId: string,
canModerate: boolean
): void

Puts a dump at the top of a playlist, for POST /api/playlists/:playlistId/dumps/:dumpId, and broadcasts playlist_dumps_updated. The followers of a public playlist are notified when the dump is public.

f
broadcastHoldersDumps(holders: PlaylistHolder[]): void

Sends each of holders its dumps again, after one of them was deleted or changed visibility, which changes what each viewer may see of them.

f
createPlaylist(
req: CreatePlaylistRequest,
userId: string
): Playlist

Creates a playlist owned by userId, for POST /api/playlists, filed under the playlist categories req names. If it is public, it notifies the users its description mentions. It links the attachments the description embeds and broadcasts playlist_created.

f
deletePlaylist(
playlistId: string,
requestingUserId: string,
canModerate: boolean
): void

Deletes a playlist for DELETE /api/playlists/:playlistId, and broadcasts playlist_deleted. canModerate lets someone other than the owner do it.

f
getEditablePlaylist(
playlistId: string,
requestingUserId: string,
canModerate: boolean
): Playlist

Returns the playlist if requestingUserId may edit it. The cover upload route checks this before it writes the file, then calls setPlaylistImage.

f
getPlaylist(
playlistId: string | Playlist,
requestingUserId: string | null
): PlaylistWithDumps

Returns a playlist with the dumps requestingUserId may see in it, in order, for GET /api/playlists/:playlistId, and for GET /api/users/:key/playlists/:slug once it has found the playlist by its path, for the same viewer. A null requestingUserId is a signed-out viewer. Its dumpCount is the number of those dumps.

f
getPlaylistById(
playlistId: string,
viewerId?: string | null
): Playlist

Returns playlist playlistId, private or not. A caller that shows it to someone must check isPublic first, as the OG middleware does. Its dumpCount and lastDumpAt are viewerId's, or a signed-out viewer's without one.

f
getPlaylistByPath(
userId: string,
slug: string,
viewerId?: string | null
): Playlist

Returns the playlist of userId's whose slug is or was slug, private or not, or throws a 404, as getPlaylistById does, counting its dumps for viewerId likewise. Reached by a former slug, it comes with its current one.

f
getPlaylistImageInfo(
playlistId: string,
requestingUserId: string | null
):
{ id: string; imageMime: string; isPublic: boolean; }
| undefined

Returns what GET /api/playlists/:playlistId/cover needs to serve a playlist's cover, or undefined when it has none. The route serves a private playlist's cover as private, so no shared cache keeps it.

f
getPlaylistMembershipsForDump(
dumpId: string,
userId: string
): PlaylistMembership[]

Returns each of userId's playlists, newest first, and whether it holds dump dumpId, for GET /api/me/playlists?contains=. The client's add-to-playlist modal shows it.

f
getPlaylistsForDump(
dumpId: string,
requestingUserId?: string
): Playlist[]

Returns up to 20 playlists that hold dump dumpId, newest first, for GET /api/dumps/:dumpId/playlists. Private playlists are left out, except requestingUserId's own. The route checks first that the caller may see the dump.

f
listNewPublicPlaylists(
from: Date,
to: Date
): Playlist[]

Returns the public playlists created from from to to, the fullest first and the newer on a tie. The community service gathers them for the /community page and the digests, which show every reader the same figures: only their public dumps are counted.

f
listPlaylistsByUser(
userId: string,
requestingUserId: string | null,
page: number,
limit: number,
sort: PlaylistSort,
categoryId?: string
): Page<Playlist>

Returns a page of userId's playlists, in the order sort names, for GET /api/users/:key/playlists, optionally only those in the playlist category categoryId. The private ones are listed only when requestingUserId is userId.

f
listPublicPlaylists(
requestingUserId: string | null,
page: number,
limit: number,
sort: PlaylistSort,
categoryId?: string
): Page<Playlist>

Returns a page of every public playlist, in the order sort names, for GET /api/playlists, optionally only those in the playlist category categoryId. The client's /playlists page lists them. Their dumps are counted, and so ordered, for requestingUserId, null being a signed-out viewer.

f
playlistsHolding(dumpId: string): PlaylistHolder[]

Returns the playlists holding dump dumpId. The dump service reads them before it deletes the dump or changes its visibility, and passes them to broadcastHoldersDumps after.

f
removeDumpFromPlaylist(
playlistId: string,
dumpId: string,
requestingUserId: string,
canModerate: boolean
): void

Takes a dump out of a playlist, for DELETE /api/playlists/:playlistId/dumps/:dumpId, and broadcasts playlist_dumps_updated. A dump that isn't in it is no error.

f
reorderPlaylist(
playlistId: string,
dumpIds: string[],
requestingUserId: string,
canModerate: boolean
): void

Puts the dumps of a playlist that the requester can see in the order of dumpIds, for PUT /api/playlists/:playlistId/order, and broadcasts playlist_dumps_updated.

f
searchPlaylists(
query: string,
limit: number,
requestingUserId?: string,
categoryId?: string
): Playlist[]

Returns up to limit playlists whose title or description contains query, newest first, for GET /api/search, optionally only those in the playlist category categoryId. Private playlists are left out, except requestingUserId's own. A blank query finds nothing.

f
setPlaylistImage(
playlistId: string,
imageMime: string,
requestingUserId: string,
canModerate: boolean
): Playlist

Records the type of a playlist's new cover and broadcasts playlist_updated. The route must have written the file already, since clients fetch the cover as soon as they get the broadcast.

f
updatePlaylist(
playlistId: string,
req: UpdatePlaylistRequest,
requestingUserId: string,
canModerate: boolean
): Playlist

Applies req to a playlist for PUT /api/playlists/:playlistId, and returns it as it now is. A new slug goes through changeSlug, first, and a moderator is exempt from its rate limit; a new title leaves the slug as it is. canModerate lets someone other than the owner edit it. If the playlist is public, it notifies, in the owner's name, the users the description mentions. When it goes public, its followers are notified of its public dumps they weren't yet told of. It links the description's attachments and tells clients of the change (broadcastPlaylistChange).

Type Aliases

T
PlaylistHolder = Pick<Playlist, "id" | "isPublic" | "userId">

A playlist holding a dump, as playlistsHolding returns it: what broadcastPlaylistDumpsUpdated needs to choose its audience.

api/services/federation-service.ts

Plays the client in federated sign-in: it lets users of other gerbeur instances sign in here with their account there, their home.

Functions

f
completeLogin(request: FederationSessionRequest): Promise<User>

Finishes a sign-in begun by startLogin, for POST /api/federation/sessions, and returns the account it signs in to: the one linked to the identity, brought up to date, or a new one. A login works once. A new account needs the home to be trusted, or the invite the login was started with, claimed now.

f
homePageUrl(
user: User,
path?: string
): Promise<string>

Returns the URL of path on the home of user, an account from another instance, for GET /api/users/:key/home: their profile there by default, or another of its pages, such as /settings, where they change their password. A handle's host is its home's issuer, the API's origin, which needn't be its pages': those are where its /authorize page is, and the home's metadata says. While it doesn't answer, the issuer stands in.

f
receiveEvent(token: string): Promise<void>

Applies the signed event token from a user's home, for POST /api/federation/events: update brings their account in step, revoke ends their sessions here. Only an instance some account here comes from, and not blocked, may send one, about one of those accounts, and at most EVENT_LIMIT's rate. The sender gives up on an event at a 4xx other than 429, and tries it again later otherwise.

f
startLogin(request: FederationLoginRequest): Promise<string>

Starts a sign-in with an account on the instance request.instance names, and returns its /authorize page with the request, for POST /api/federation/logins. Unless that instance is trusted, or someone here already comes from it, the request must carry a valid invite, which is claimed only once the account is made; completeLogin makes no account without one.

api/services/chat-service.ts

Stores the site-wide chat's messages and each user's read marker.

Functions

f
chatMessageMentionsEveryone(id: string): boolean

Checks whether message id is flagged mentionsEveryone, false when there is no such message. The WebSocket route reads it before an edit, to notify everyone only when the edit adds @everyone.

f
createChatMessage(
userId: string,
body: string,
replyToId?: string
): ChatMessage

Stores a message from userId on a chat_send, and claims the pasted images it links to. A replyToId naming no message is dropped, and the message is posted as no reply. The message mentionsEveryone when it says @everyone and userId may. The caller broadcasts the result, and notifies everyone when it is flagged.

f
deleteChatMessage(
id: string,
requestingUserId: string,
canModerate: boolean
): void

Deletes a message on a chat_delete. canModerate lets a moderator delete anyone's message. Replies to it keep their replyToId, without its preview.

f
getChatMessagesAfter(
after: ChatCursor,
limit?
): ChatMessage[]

Returns the first limit messages after after, oldest first. The chatbox uses it to fetch what it missed while the socket was down. limit is clamped to 1–100.

f
getChatReadState(userId: string): ChatReadState

Returns how far userId has read the chat. A user who never opened it has read up to their registration, so the history from before it isn't unread.

f
getRecentChatMessages(
limit?,
before?: ChatCursor
): ChatMessage[]

Returns the latest limit messages, or with before the limit messages just older than it, oldest first either way. limit is clamped to 1–100.

f
markChatRead(
userId: string,
at: string
): ChatReadState

Moves userId's read marker to at, a message's created_at, on a chat_read. It never moves back, so a lagging tab can't undo a newer tab's read. It never passes the newest message, so a timestamp from the future can't mark messages read before they are sent.

f
updateChatMessage(
id: string,
body: string,
requestingUserId: string,
canModerate: boolean
): ChatMessage

Replaces a message's body on a chat_edit, and claims any image the new body links to. canModerate lets a moderator edit anyone's message. The flag mentionsEveryone is set again from the new body and the author's role, not the editor's: a moderator's edit can't make a user's message reach everyone. The caller broadcasts the result.

Interfaces

I
ChatCursor

Passed by routes/chat.ts to page through the history, from the message the client already has at one end. Messages are ordered by createdAt, then by id, so a page boundary between two messages sent in the same millisecond skips neither.

  • createdAt: string

    An ISO timestamp, compared as a string with created_at.

  • id: string

    Without it, the cursor takes in every message at createdAt: they are all older for getRecentChatMessages and all newer for getChatMessagesAfter.

I
ChatReadState

Returned to the WebSocket route by getChatReadState and markChatRead. The route sends it in welcome and in chat_read_update, and the client shows unread on the chat's badge.

api/services/community-service.ts

Gathers what the whole site did over a period, for the /community page and the digests' shared sections. The digests ask communityStatsOf for each reader's own period. The page reads the figures of the week and the month ending at the last refresh, which communityJob redoes every COMMUNITY_REFRESH_MINUTES, so a visit runs no query.

Functions

f
communityStatsOf(
from: Date,
to: Date
): CommunityStats

Returns what the site did from from to to: its most upvoted public dumps, who joined, the new public playlists, the chat's messages and the totals.

f
getCommunityFeed(): CommunityFeed

Returns the community feed for GET /api/community/feed: the latest events, read now, and the last week's figures as of the last refresh.

f
getCommunityStats(period: DigestCadence): CommunityStats

Returns the figures of period as of the last refresh, for GET /api/community, refreshing them first if there are none yet.

f
listCommunityEvents(): CommunityEvent[]

Returns the latest things done in public, newest first, FEED_MAX at most: the public dumps posted and playlists created, the comments and upvotes on public dumps, and who joined. A dump has one comment event and one upvote event at most, its latest, so that a thread or a popular dump doesn't fill the feed.

f
refreshCommunityStats(now?: Date): void

Recomputes the figures of each period, ending at now.

Variables

v
communityJob: Job

Refreshes the figures at startup, then every COMMUNITY_REFRESH_MINUTES. main.ts starts it.

api/services/digest-service.ts

Sends each user their weekly digest and monthly recap, unless they turned it off. main.ts starts digestJob, which every quarter of an hour sends the digests that are due; digest_schedule remembers, for each user and cadence, the start of the period their next one covers.

Functions

f
buildDigest(
readerId: string,
reader: string,
cadence: DigestCadence,
from: Date,
to: Date,
token: string
): DigestContent | null

Returns the digest of user readerId for the period from from to to, or null when it would show nothing. The links are absolute; the unsubscribe one carries token, which turns off cadence.

f
sendDueDigests(now?: Date): Promise<number>

Sends every digest due at now, one after the other, and returns how many went out. A digest that fails to send is logged and not retried: its period is over either way, and the next one starts.

f
syncDigestSchedules(now: Date): void

Brings digest_schedule in line with the settings: a user who turned a cadence off loses its row, and one who receives it without a row, being new or having turned it back on, starts a period at now.

Variables

v
digestJob: Job

Sends the due digests a minute after the server starts, then every quarter of an hour. main.ts starts it.

api/services/email-service.ts

Sends the site's email through the SMTP server of SMTPS_URL, which may be unset. The welcome email, the password reset link and the digests go through it, each rendered first by its module in api/mail/. They check isEmailEnabled before rendering.

Functions

f
isEmailEnabled(): boolean

Checks whether email can be sent. Without an SMTP server, sign-up sends no welcome email, a password reset request does nothing, and no digest goes out.

f
sendEmail(email: OutgoingEmail): Promise<void>

Sends email from FROM_EMAIL and resolves once the SMTP server has accepted it. The welcome and reset emails don't wait for it: a slow server mustn't hold up the request.

f
setEmailTransport(next: EmailTransport | null): void

Replaces the transport, so that email is enabled and every message goes to next; null disables email again. The tests use it to read what was sent.

Interfaces

I
EmailTransport

Hands a message to a mail server. nodemailer's SMTP transport is one, and the tests put a recording one in its place with setEmailTransport.

I
OutgoingEmail

Passed to sendEmail: a rendered email, its recipient, and the headers some emails add (the digests' List-Unsubscribe).

api/services/identity-provider-service.ts

Plays the home instance in federated sign-in: it lets this instance's users sign in to other gerbeur instances with their account here.

Functions

f
createAuthorizationCode(
userId: string,
request: AuthorizeRequest
): Promise<string>

Records user userId's consent to sign in to the client of request, and returns where to send them: the client's callback page with a code, which the client trades within a minute (exchangeCode). The grant lasts until they withdraw it, so the page asks no more.

f
deliverOutbox(): Promise<void>

Sends the outbox's events to each instance that has some, every instance at once so that one that doesn't answer delays none of the others.

f
describeClient(
userId: string,
clientId: string
): Promise<OAuthClient>

Returns the instance clientId names, for the /authorize page's question to user userId, and whether they already let it sign them in.

f
exchangeCode(params: URLSearchParams): Promise<{ id_token: string; token_type: string; }>

Trades a code for an identity token, for POST /api/oauth/tokens, which the client calls server to server with the form params. A code works once, within a minute, for the client and redirect it was made for, and with the verifier whose hash the request carried (PKCE). The token, signed with the instance key and addressed to the client, carries identityClaims and the request's nonce.

f
getInstanceMetadata(): InstanceMetadata

Returns the instance's metadata, served at /.well-known/openid-configuration.

f
identityClaims(user: User): IdentityClaims

Returns what this instance tells another of user: their name, email and avatar, whose URL changes with each new one, so that a client fetches it again only then.

f
listGrants(userId: string): FederationGrant[]

Returns the instances user userId let their account sign in to, the most recently used first, for the settings page.

f
notifyClients(
userId: string,
event: FederationEvent | { type: "update"; },
clientIds?: string[]
): Promise<void>

Tells each instance user userId signed in to of event, or only those of clientIds: update when their name, email or avatar changed, revoke when their sessions there must end. The routes that change a user call it, and for an update, event may be just the type: the claims are read now. The events are queued in the outbox and their delivery starts at once, after those queued before for the same instance (deliverTo).

f
revokeGrant(
userId: string,
host: string
): Promise<void>

Withdraws user userId's grant to the instance whose host is host: it stops answering for them there, and is told to end their sessions.

f
withdrawAllGrants(): Promise<number>

Withdraws every grant, as each user would theirs, and drops the codes not yet traded, so that no account here stays signed in elsewhere: main.ts calls it at startup when FEDERATION.outgoing is off. Returns how many grants it withdrew.

Variables

v
ISSUER: string

The instance's name in every token it signs and every token addressed to it: its public URL, which must be an origin for other instances to find its metadata.

v
outboxJob: Job

Retries the outbox's failed events every minute.

v
REDIRECT_URI: string

The client's page a home instance sends the user back to, on the frontend. Its metadata announces it, and a home accepts no other.

api/services/comment-like-service.ts

Stores likes on comments and keeps each comment's like_count in step with them. Likes have no HTTP endpoint to cast or remove. The WebSocket route (routes/ws.ts) calls this module on comment_like_cast and comment_like_remove messages, then broadcasts the new count. The comment routes use it only to list a comment's likers.

Functions

f
castCommentLike(
commentId: string,
userId: string
): LikeResult

Records userId's like on a comment and notifies the comment's author. The notification is best-effort: if it fails, the like still counts.

f
getCommentLikers(
commentId: string,
page: number,
limit: number
): Page<User>

Returns a page of the users who like a comment, latest like first, for GET /api/comments/:commentId/likers. It doesn't check that the caller may see the comment: the route does that first, with getDump.

f
getUserCommentLikes(userId: string): string[]

Returns the ids of the comments userId likes. The WebSocket route sends them in its welcome message, and the client shows those like buttons pressed.

f
removeCommentLike(
commentId: string,
userId: string
): LikeResult

Takes back userId's like on a comment. It doesn't check that the user can still see the comment, since a like on a dump that has gone private is still theirs to take back.

Interfaces

I
LikeResult

Returned to the WebSocket route by castCommentLike and removeCommentLike. The route acks the liker with likeCount, then broadcasts it to every client, or to the liker's alone when the comment's dump isPrivate.

api/services/vote-service.ts

Stores upvotes on dumps and keeps each dump's vote_count in step with them.

Functions

f
castVote(
dumpId: string,
userId: string
): VoteResult

Records userId's upvote on a dump and notifies the dump's poster. The notification is best-effort: if it fails, the vote still counts.

f
getDumpVoters(
dumpId: string,
page: number,
limit: number
): Page<User>

Returns a page of the users who upvoted a dump, latest vote first, for GET /api/dumps/:dumpId/voters. It doesn't check that the caller may see the dump: the route does that first, with getDump.

f
getUserVotes(userId: string): string[]

Returns the ids of the dumps userId upvoted. The WebSocket route sends them in its welcome message, and the client shows those vote buttons pressed.

f
removeVote(
dumpId: string,
userId: string
): VoteResult

Takes back userId's upvote on a dump. No visibility check: taking back a vote on a dump that has since gone private is harmless, and it's the voter's own row.

Interfaces

I
VoteResult

Returned to the WebSocket route by castVote and removeVote. The route acks the voter with voteCount, then broadcasts it to every client, or to the dump owner's alone when isPrivate.

api/services/dump-service.ts

Posts, edits and deletes dumps, and reads them as each viewer may see them. routes/dumps.ts calls it, and so do the search, user, file and thumbnail routes to read dumps. A dump's slug is made from its title when it is posted, and changes only when an edit sends a new one (slug-service.ts). It broadcasts the changes to public dumps over the WebSocket.

Functions

f
createFileDump(
file: File,
comment: string | undefined,
userId: string,
isPrivate?: boolean,
title?: string,
categoryIds?: string[]
): Promise<Dump>

Posts an uploaded file as userId, for POST /api/dumps, and stores it in DUMPS_DIR under the dump's id. It is announced and notified as createUrlDump does.

f
createUrlDump(
request: CreateUrlDumpRequest,
userId: string
): Promise<Dump>

Posts a link as userId, for POST /api/dumps, with its preview fetched from the page. A public dump is broadcast as dump_created and notified to the poster's followers. Anyone @mentioned in the note is notified even when the dump is private.

f
customThumbnailPath(dumpId: string): string

Returns where dump dumpId's custom thumbnail is kept: the thumbnail route writes and serves it, and this service deletes it with the thumbnail or the dump.

f
deleteDump(dumpId: string): Promise<void>

Deletes a dump with its files and thumbnails, for DELETE /api/dumps/:dumpId, and broadcasts dump_deleted and the new dumps of the playlists that held it.

f
findDumpsByUrl(
url: string,
requestingUserId?: string,
limit?: number
): DumpUrlMatch[]

Returns the dumps requestingUserId may see that were posted with the same link as url, newest first, for the create form's duplicate notice (GET /api/dumps?url=). Links are compared in their canonical form, so tracking parameters, the scheme, www. or a YouTube share format don't hide a repost.

f
getDump(
dumpId: string,
requestingUserId?: string
): Dump

Returns dump dumpId as requestingUserId may see it. The routes resolve every :dumpId with it before acting on the dump.

f
getDumpByPath(
userId: string,
slug: string,
requestingUserId?: string
): Dump

Returns the dump of userId's whose slug is or was slug, as requestingUserId may see it, for GET /api/users/:key/dumps/:slug, which reads a dump by its page's path. Reached by a former slug, it comes with its current one, which the client puts in its URL.

f
getDumpsByUser(
userId: string,
page: number,
limit: number,
includePrivate: boolean,
categoryId?: string
): Page<Dump>

Returns a page of userId's dumps, newest first, for GET /api/users/:key/dumps, optionally only those in categoryId. The route sets includePrivate when users ask for their own.

f
getVotedDumpsByUser(
userId: string,
page: number,
limit: number,
requestingUserId: string | null,
categoryId?: string
): Page<Dump>

Returns a page of the dumps userId upvoted, latest vote first, for GET /api/users/:key/upvoted, optionally only those in categoryId. Their own private dumps are included only when they are the one asking.

f
listDumps(
page: number,
limit: number,
requestingUserId?: string
): Page<Dump>

Returns a page of the dumps requestingUserId may see, newest first, for the index feed (GET /api/dumps).

f
listTopDumps(
from: Date,
to: Date,
limit: number
): Dump[]

Returns the limit most upvoted public dumps posted from from to to, the newer first on a tie, leaving out those no one upvoted. The community service gathers them for the /community page and the digests.

f
refreshDumpMetadata(dumpId: string): Promise<Dump>

Fetches a URL dump's preview again, bypassing the cache, and replaces its title and preview, for POST /api/dumps/:dumpId/metadata. The slug stays.

f
removeDumpThumbnail(dumpId: string): Promise<Dump>

Removes a dump's custom thumbnail and its file, for DELETE /api/dumps/:dumpId/thumbnail.

f
replaceFileDump(
dumpId: string,
file: File,
comment: string | undefined,
title?: string,
categoryIds?: string[],
isPrivate?: boolean,
slug?: string,
exempt?: boolean
): Promise<Dump>

Replaces a file dump's upload and its comment, for PUT /api/dumps/:dumpId/file. A title, slug, categoryIds or isPrivate left out stays as it is. Visibility is taken here so that a new file and a change of visibility land together. The slug changes as in updateDump, before the file is written. The old file is put back if the database update fails, and clients and followers are told as updateDump tells them.

f
searchDumps(
query: string,
page: number,
limit: number,
requestingUserId?: string,
categoryId?: string
): { items: Dump[]; total: number; }

Returns a page of the dumps requestingUserId may see whose title, note, or previewed title or description contains query, newest first, with how many match in all, optionally only those in the dump category categoryId. GET /api/search calls it.

f
setDumpThumbnail(
dumpId: string,
mime: string
): Dump

Records that a dump has a custom thumbnail of type mime, for PUT /api/dumps/:dumpId/thumbnail, once the route has written the file.

f
updateDump(
dumpId: string,
request: UpdateDumpRequest,
exempt?: boolean
): Promise<Dump>

Applies request to a dump, for PUT /api/dumps/:dumpId. A new link has its preview fetched and gives the dump its page's title, unless request sets one. A file dump ignores url. A new slug goes through changeSlug, which exempt (a moderator) frees from its rate limit; a new title leaves the slug as it is. Clients get the dump as dump_updated, or a dump_deleted when it turns private and a dump_created when it turns public. Anyone @mentioned in the note is notified, and when it turns public, its followers are as when it is posted (notifyIfWentPublic).

api/services/ws-service.ts

Keeps the set of open sockets and sends them the server's messages.

Functions

f
broadcastCategoriesChanged(): void

Sends everyone categories_changed, after the category manager created, edited or deleted one, so that every client fetches them again.

f
broadcastChatMessage(
message: ChatMessage,
nonce?: string
): void

Sends chat_message to every signed-in socket, the sender's included. nonce comes from the sender's chat_send, and lets it swap its pending copy for the message.

f
broadcastChatMessageDeleted(id: string): void

Sends chat_message_deleted to every signed-in socket.

f
broadcastChatMessageUpdated(message: ChatMessage): void

Sends chat_message_updated to every signed-in socket.

f
broadcastCommentCreated(
comment: Comment,
dump: { isPrivate: boolean; userId: string; }
): void

Sends comment_created to those who may see the comment's dump: everyone, or its owner alone when it is private. The comments route calls it, as it does the two below.

f
broadcastCommentLikeUpdate(
commentId: string,
likeCount: number,
likerId: string,
action: "cast" | "remove",
onlyUserId?: string
): void

Sends comment_likes_update after the route stored a like, to everyone, or with onlyUserId to that user's sockets alone (for a comment on a private dump).

f
broadcastDumpChange(
before: Dump | null,
after: Dump | null
): void

Tells the clients that may see a dump how it changed, as broadcastChange says, for the dump service after every write.

f
broadcastDumpRelatedChanged(
dump: Pick<Dump, "id" | "isPrivate" | "userId">,
others: Pick<Dump, "id" | "isPrivate" | "userId">[]
): void

Sends dump_related_changed after the links between dump and each of others changed, for the backlink service. A link shows only to those who may see both its ends, so a socket is told only when its user may see dump and one of others, and of those alone.

f
broadcastPlaylistChange(
before: Playlist | null,
after: Playlist | null,
members: DatedPlaylistMember[]
): void

Tells the clients that may see a playlist how it changed, as broadcastChange says, for the playlist service after every write to its details. A playlist turned private thus leaves the others' views without their learning what it became. members are the dumps of after: each socket gets the playlist with the count of those its user may see and the date of the latest, the owner's private dumps being theirs alone to count.

f
broadcastPlaylistDumpsUpdated(
playlist: Pick<Playlist, "id" | "isPublic" | "userId">,
members: DatedPlaylistMember[]
): void

Sends playlist_dumps_updated to the playlist's audience, each socket with only the dumps its user may see, as GET would list them. The private dumps in a public playlist stay hidden from everyone but their posters, and so does their number: a client counts the dumps it is sent.

f
broadcastPresence(): void

Sends everyone presence_update. The route calls it whenever a socket opens or closes.

f
broadcastTyping(): void

Sends chat_typing_update to every signed-in socket. The route also calls it when a typing user's socket closes.

f
broadcastUserUpdated(user: PublicUser): void

Sends everyone user_updated, after a profile edit, a role change or an avatar upload.

f
broadcastVoteUpdate(
dumpId: string,
voteCount: number,
voterId: string,
action: "cast" | "remove",
onlyUserId?: string
): void

Sends votes_update after the route stored a vote, to everyone, or with onlyUserId to that user's sockets alone (for a private dump).

f
closeUserSockets(
userId: string,
reason: string
): void

Closes every socket of userId without signing anyone out. The user service calls it on a password change. Each tab reconnects with the token it holds: the tab that made the change has a fresh one and stays signed in, while the others connect anonymously and sign out on their next 401.

f
getOnlineUsers(): OnlineUser[]

Returns each signed-in user with a socket open, once. The route sends it in welcome, and broadcastPresence in presence_update.

f
getTypingUsers(): OnlineUser[]

Returns each user who sent chat_typing true within the last TYPING_TTL, once, for broadcastTyping.

f
handleClientPong(client: WsClient): void

Records a client's pong, which keeps its socket open.

f
isUserChatOpen(userId: string): boolean

Checks whether userId is reading the chat on any of their sockets. The route passes it to notifyMentions, which skips those users: they saw the mention live, and their client chimes for it.

f
register(client: WsClient): void

Adds a client once its socket opens. From then on, it receives the broadcasts, and is shown online if it is signed in.

f
send(
socket: WebSocket,
data: ServerToClientMessage
): void

Sends data to one socket unless it is closing. The WebSocket route answers a client's own message with it: a pong, a vote's ack, an error.

f
sendToUser(
userId: string,
data: ServerToClientMessage
): void

Sends data to every socket of userId. The notification service sends notification_created and notifications_read with it, the follow service follows_changed, and the route chat_read_update.

f
setClientTyping(
client: WsClient,
typing: boolean
): void

Records a client's chat_typing, and broadcasts the typing list when the client starts or stops. The route also clears it when the client sends a message. A client that stops without saying so is expired after TYPING_TTL.

f
takeToken(
key: string,
limit: RateLimit
): boolean

Takes one token from key's bucket, or returns false when it is empty. The caller picks the key, such as typing:<userId>, and keys by user or account so that more tabs or parallel requests don't buy more tokens.

f
unregister(client: WsClient): void

Removes a client when its socket closes. The caller rebroadcasts presence.

f
updateClientAvatar(
userId: string,
avatarMime: string
): void

Records userId's new avatar on their clients, so presence and the typing list show it, and rebroadcasts presence. routes/avatars.ts calls it on an upload.

f
updateClientUsername(
userId: string,
username: string
): void

Records userId's new name on their clients, so presence and the typing list show it, and rebroadcasts presence. The routes that rename a user call it, along with broadcastUserUpdated.

Interfaces

I
RateLimit

Passed to takeToken by each limited action, which declares its own as a constant.

I
WsClient

Created by the WebSocket route for each socket it opens, and kept here until the socket closes. The user fields are read at connect and left undefined for an anonymous socket.

Variables

v
pingJob: Job

Pings every client each PING_INTERVAL, closing the ones that didn't answer the previous ping. main.ts starts it.

v
typingExpiryJob: Job

Expires the typing users who went quiet without a false, such as by closing the tab, every 2 seconds. main.ts starts it.

api/services/federation-peer-service.ts

Keeps the instances an admin trusts or blocks for federated sign-in (federation_peers). Users of a trusted instance sign in here directly, those of a blocked one never do, and an instance on neither list is invite only. federation-service.ts reads it at each sign-in, /manage edits it through routes/federation.ts, and the login page offers the trusted ones.

Functions

f
getPeerStatus(issuer: string): PeerStatus | null

Returns what an admin decided about the instance at issuer, or null when it is on neither list.

f
listPeers(trustedOnly: boolean): FederationPeer[]

Returns the instances on the lists, by name: every one for /manage, or only the trusted ones, which the login page offers.

f
removePeer(host: string): void

Takes the instance whose host is host off the lists, for /manage: its users are invite only again, and those already here may sign in again if it was blocked.

f
setPeer(
instance: string,
status: PeerStatus
): Promise<FederationPeer>

Puts the instance instance names, by its API's address or its pages' (resolveIssuer), on the list of status, for /manage, and returns it. A trusted one must answer as a gerbeur instance, whose name is kept; a blocked one is blocked whether it answers or not, keeping the name it had. Blocking one ends the sessions of its users here at once: their tokens stop counting by getSessionState, and this closes their sockets.

api/services/category-service.ts

Stores the categories and which dumps and playlists belong to them, a set of categories for each CategoryKind. The category routes manage and list them, the dump and playlist services file each dump or playlist they write under its categories, and every service that lists dumps or playlists fills in their categoryIds and filters them by category with categoryJoin.

Functions

f
assertCategoriesExist(
kind: CategoryKind,
categoryIds: string[]
): void

Checks that every id in categoryIds names a category of kind, so that a dump is never filed under a playlist category, nor the other way round. The dump and playlist services run it before they write a row or store an uploaded file, so a bad id fails the request before anything is written.

f
attachCategoryIds(
kind: CategoryKind,
items: { id: string; categoryIds?: string[]; }[]
): void

Sets categoryIds on each of items, dumps or playlists as kind says, in place, in one query. Their rows don't carry them, so every service that returns dumps or playlists calls this first.

f
categoryJoin(
kind: CategoryKind,
alias: string,
categoryId?: string
): [string, string[]]

Returns the join that keeps only the rows of alias, a dump or playlist as kind says, filed under categoryId, with its parameters, or nothing without one. A query puts the join after its FROM and the parameters before its own.

f
createCategory(req: CreateCategoryRequest): Category

Creates a category, for POST /api/categories. The name is stored trimmed, and the position defaults to 0. Every client is told to fetch the categories again, as after the two writes below.

f
deleteCategory(id: string): void

Deletes category id, for DELETE /api/categories/:id. Its dumps or playlists stay, and lose it from their categoryIds.

f
getCategoryById(id: string): Category

Returns category id, for updateCategory.

f
getCategoryBySlug(
kind: CategoryKind,
slug: string
): Category

Returns the category of kind at slug, for listDumpsByCategory and the list of every public playlist.

f
getCategoryIdBySlug(
kind: CategoryKind,
slug: string
): string | undefined

Returns the id of the category of kind at slug, or undefined when there is none.

f
listCategories(): Category[]

Returns every category, of both kinds, by kind, position then slug, for GET /api/categories.

f
listDumpsByCategory(
slug: string,
page: number,
limit: number,
requestingUserId?: string
): Page<Dump>

Returns a page of the dumps in the dump category at slug, newest first, for GET /api/dumps?category=. Private dumps are left out, except requestingUserId's own.

f
resolveCategoryFilter(
kind: CategoryKind,
params: URLSearchParams,
name?: string
): string | "missing" | undefined

Returns the id of the category of kind whose slug the name parameter of params, ?category= by default, gives, undefined without one, or "missing" when it names none, so that the list comes back empty rather than unfiltered. The users routes filter a profile's lists with it, and the search route its results.

f
setCategories(
kind: CategoryKind,
id: string,
categoryIds: string[]
): void

Replaces the categories of the dump or playlist id, as kind says, with categoryIds, duplicates dropped. An empty list takes it out of every category. Called inside the dump or playlist service's transaction, it joins that one.

f
updateCategory(
id: string,
req: UpdateCategoryRequest
): Category

Applies req to category id, for PUT /api/categories/:id, and returns the category as it now is. Fields req leaves out keep their value.

api/services/instance-key-service.ts

Keeps the instance's Ed25519 key, which signs what it tells other instances in federated sign-in: the identity tokens of identity-provider-service.ts and the events of its outbox. The key is made the first time it's needed and stored in federation_keys, so it survives restarts; other instances fetch its public half from GET /.well-known/jwks.json.

Functions

f
getPublicKeys(): Promise<JSONWebKeySet>

Returns the public halves of every stored key, for GET /.well-known/jwks.json. It makes the first key when there is none, so that the set is never empty.

f
getSigningKey(): Promise<InstanceSigningKey>

Returns the key the instance signs with now: the newest stored, or a new one, stored, when there is none.

f
resetSigningKey(): void

Forgets the loaded key, for the tests, which wipe the table between cases.

api/services/password-reset-service.ts

Emails password reset links and sets the new password when one is used. routes/password-resets.ts calls it for POST /api/password-resets and PUT /api/password-resets/:token; the link opens the client's /reset-password page.

Functions

f
requestPasswordReset(email: string): Promise<void>

Emails a reset link to the account with address email, if there is one and email is configured. It says nothing of what it did, so that the route answers the same whether the address has an account or not. It sends at most 3 links in a row per address, then one every 10 minutes.

f
resetPassword(
token: string,
newPassword: string
): Promise<string>

Sets newPassword for the user of a reset token, marks the token used, and returns the user's id. Through updateUser, this ends the user's sessions here; the route tells the instances they signed in to.

api/services/backlink-service.ts

Records which dumps a dump's note and comments link to, and finds the dumps related to one through those links. The dump and comment services keep the links up to date, and the dump page lists the related dumps, which it fetches again when dump_related_changed says they changed.

api/services/comment-service.ts

Stores the comments on dumps, threaded by parent_id. The comment routes call it and broadcast what it returns. It leaves checking that the caller may see the dump to them.

Functions

f
createComment(
dumpId: string,
userId: string,
body: string,
parentId?: string
): Comment

Posts a comment on dump dumpId, replying to parentId if given, for POST /api/dumps/:dumpId/comments. It notifies the dump's poster and the users it mentions, unless the dump is private, and links the comment's attachments and backlinks. The route has checked that the caller may see the dump.

f
deleteComment(
commentId: string,
requestingUserId: string,
canModerate: boolean
): CommentDump

Deletes a comment, for DELETE /api/comments/:commentId. canModerate lets someone other than the author do it. The body is emptied and the backlinks dropped, but the row stays so its replies keep their place. The route broadcasts the deletion to those who may see the returned dump.

f
getCommentDumpId(commentId: string): string

Returns the id of the dump comment commentId is posted under. The likers route uses it to check that the caller may see that dump.

f
getComments(dumpId: string): Comment[]

Returns every comment on dump dumpId, oldest first, for GET /api/dumps/:dumpId/comments. Deleted comments are included, emptied, so the client can keep their replies in place.

f
updateComment(
commentId: string,
body: string,
requestingUserId: string,
canModerate: boolean
): { comment: Comment; dump: CommentDump; }

Replaces a comment's body, for PATCH /api/comments/:commentId. canModerate lets someone other than the author do it. It notifies, in the author's name, the users it mentions for the first time, unless the dump is private, and relinks the attachments and backlinks. The route broadcasts the comment to those who may see its dump.

Interfaces

I
CommentDump

The dump a comment is on, as updateComment and deleteComment return it: what the route needs to choose the broadcast's audience.

api/services/user-service.ts

Reads and writes the users table. Its Users carry the password hash and email, so a route strips them with toPublicUser or toSelfUser before answering. routes/users.ts is its main caller; lib/auth.ts checks every token against getSessionState.

Functions

f
assertLocalUser(user: User): void

Throws a 400 when user's account lives on another instance (isRemoteUser), whose password, email, avatar and name are that instance's to change. The routes that change them call it first.

f
createRemoteUser(
request: { username: string; email: string; },
inviterId: string | null
): User

Creates the account of a user from another instance, named username (their handle here, name@host), with no password, for federation-service.ts, which links it to their identity there in the same transaction. inviterId is whoever invited them, or null when their instance is trusted.

f
createUser(
request: RegisterUserRequest,
inviterId: string | null
): Promise<User>

Creates a user with the user role, invited by inviterId, for POST /api/users. The caller has validated request and claimed the invite.

f
getInviteTree(userId: string): Invitee[]

Returns everyone userId invited, and everyone they invited in turn, eleven levels deep at most, in sign-up order, for the profile page's invite tree (GET /api/users/:key/invitees).

f
getSessionState(userId: string):
{ role: Role; tokensValidAfter: string | null; }
| undefined

Returns the user's live role and their last password change, before which their tokens don't count, or undefined when the user is gone, or comes from another instance while an admin blocks it or the deployment turns sign-ins from other instances off (FEDERATION.incoming). lib/auth.ts checks every session and media token against it.

f
getUserByEmail(email: string): User | null

Returns the local user whose email is email, in any case, or null, for requestPasswordReset. An account from another instance has no password here to reset.

f
getUserById(userId: string): User

Returns user userId, with their hash and email.

f
getUserByUsername(username: string): User

Returns the user named username now, in any case, with their hash and email. Signing in takes it: a former name doesn't sign in.

f
listNewUsers(
from: Date,
to: Date
): PublicUser[]

Returns the users who joined from from to to, in sign-up order, each with the name of whoever invited them. The community service gathers them for the /community page and the digests.

f
renameUser(
userId: string,
username: string
): User

Renames user userId to username, for PATCH /api/users/:key/username, which only user:manage may call: users can't rename themselves. Returns them renamed. The old name becomes an alias of theirs for good, so /@old/… still reaches them and nobody else may take it; a change of case alone leaves none, since names are looked up in any case. Taking back a former name of their own is allowed. Their dumps' and playlists' slugs belong to their id, so their paths follow the new name.

f
resolveUserKey(key: string): User

Returns the user an API path's key names: @name by their name, current or former (resolveUsername), anything else by their id. Every route under /api/users/:key resolves its key with it.

f
resolveUsername(username: string): User

Returns the user who is or was named username, in any case, or by the handle another instance gives them (localHandle): a page or an API path naming a user so reaches them, and the caller sends the client on to their current name.

f
revokeSessions(
userId: string,
at: Date,
reason: string
): void

Ends every session of user userId opened before at, closing their sockets with reason: their tokens stop counting (users.tokens_valid_after, which getSessionState reads). A password change calls it, and so does the sign-out of a user from another instance, when that instance says so or an admin blocks it.

f
searchUsers(
query: string,
limit: number
): UserSuggestion[]

Returns up to limit users whose name starts with query, for the editor's

f
searchUsersGlobal(
query: string,
limit: number
): PublicUser[]

Returns up to limit users whose name or description contains query, for the /search page (GET /api/search). They are already public profiles.

f
sendWelcomeEmail(user: User): void

Emails user the welcome message, if email is configured. The register route calls it once the account exists, and doesn't wait: a slow or failing SMTP server mustn't hold up the sign-up, so a failure is only logged.

f
setUserAvatar(
userId: string,
data: Uint8Array
): Promise<User>

Makes the image data user userId's avatar and returns them, for an upload (PUT /api/me/avatar) and for the avatar a user from another instance has there (federation-service.ts). Presence alone would update only the online list, so it also sends user_updated, which reaches every avatar on screen, its bumped updatedAt busting their cache.

f
setUsername(
userId: string,
username: string
): User

Names user userId username, keeping the old name as their alias as renameUser describes, and returns them. The caller has checked the name: renameUser checks a local one, and federation-service.ts the handle of a user renamed on their home instance.

f
updateUser(
userId: string,
request: UpdateUserRequest
): Promise<User>

Applies request to user userId and returns the updated user. It trusts request: the routes pick the fields a caller may change. The username changes through renameUser alone, which keeps the old one as an alias. A new password also ends every session opened before it and closes the user's sockets, so the caller must issue fresh tokens if the user is to stay signed in. A description of null keeps the current one.

api/services/settings-service.ts

Reads and writes each user's settings in user_settings. routes/me.ts answers GET and PATCH /api/me/settings with it, and the digests read their readers' choices from the same table.

Functions

f
getSettings(userId: string): UserSettings

Returns the settings of user userId, the defaults if they changed none.

f
updateSettings(
userId: string,
patch: UpdateSettingsRequest
): UserSettings

Applies patch to the settings of user userId and returns them all. The settings it leaves out keep their value.

api/services/slug-service.ts

Changes and resolves the slugs of dumps and playlists. A slug changes only when someone edits it (the dump and playlist services call changeSlug with the slug their edit form sent), and the slug it leaves becomes an alias of the item, so that old links still reach it; the routes that read an item by its page path resolve the slug with resolveSlug, aliases included, and answer the item, whose current slug the client then puts in its URL.

Functions

f
changeSlug(
kind: SlugKind,
item: { id: string; userId: string; slug: string; },
slug: string,
exempt: boolean
): void

Gives item the slug slug, keeping its current one as an alias. An alias always names the item itself, so redirects never chain; taking back one of the item's own aliases swaps it with the current slug. Unless exempt (a moderator), an item's slug changes at most RENAME_LIMITS.SLUG.count times in its window, counted from its aliases. A slug equal to the current one changes nothing.

f
newSlug(
kind: SlugKind,
userId: string,
title: string
): string

Returns a free slug for a new item of userId's titled title, for the dump and playlist services to insert it with, inside their transaction.

f
resolveSlug(
kind: SlugKind,
userId: string,
slug: string
): string | undefined

Returns the id of userId's item of kind whose slug is or was slug, or undefined when none is.

api/remote/providers/soundcloud.ts

Previews SoundCloud links from their page, for rich-content-service.ts.

Variables

v
soundcloudProvider: RichContentProvider

Claims SoundCloud's hosts, short links included, and builds the embed player's URL from the page's address.

api/remote/providers/bandcamp.ts

Previews Bandcamp links from their page, for rich-content-service.ts.

Variables

v
bandcampProvider: RichContentProvider

Claims bandcamp.com and its subdomains, where artists have their pages, and takes the embed player's URL from the page's og:video. The native player fetches the page's streams itself at play time, through GET /api/remote/bandcamp.

api/remote/providers/self.ts

Previews links to this site's own dump, user and playlist pages, as parsePagePath reads them, from the database, for rich-content-service.ts. No request goes out, so this works even when the site runs on a private host that fetchWithTimeout refuses.

Variables

v
selfProvider: RichContentProvider

Comes before the generic provider, and claims what isSelfUrl accepts. A profile's tab previews as the profile. Its fetch rejects with a 404 APIException for a missing page, a private dump or a private playlist: the preview is stored in the linking dump, which others may see.

api/remote/providers/generic.ts

Previews any page no other provider claims, for rich-content-service.ts, from whatever metadata its HTML carries. Its reading of a page, genericPreview, is also the base of the Bandcamp, SoundCloud and YouTube channel previews.

Functions

f
genericPreview(
page: Page,
url: string,
finalUrl: string
): RichContent

Returns the preview of the page url that page read, served from finalUrl after redirects. Each field falls back from OG tags to Twitter tags, JSON-LD and the markup itself. The icon and brand color feed the placeholder the frontend draws when there is no thumbnail.

Variables

v
genericProvider: RichContentProvider

Matches every URL, so it comes last in the service's list.

api/remote/providers/youtube.ts

Previews YouTube videos, playlists and channels, for rich-content-service.ts. Videos and playlists are read from YouTube's oEmbed endpoint, channels from their page.

Variables

v
youtubeProvider: RichContentProvider

Comes first in the service's list. A video's embed keeps the link's playlist and start time, a playlist embeds as a videoseries, and a channel gets no embed. Its fetch never rejects: whatever YouTube doesn't answer is left out.

api/remote/providers/provider.ts

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.

Interfaces

I
RichContentProvider

Implemented by each file of remote/providers/, and listed in priority order by rich-content-service.ts.

  • fetch(url: string): Promise<RichContent>

    Returns the preview of url, which ends up in the dump's rich_content or the create form's preview. It rejects when the page can't be fetched or answers with an error status, and the service then falls back to a stub. A page that isn't HTML yields a preview without metadata. The service tidies what it returns, so a provider leaves text as the page wrote it.

  • matches(url: string): boolean

    Called on a URL that isValidHttpUrl accepted; never throws.

  • name: string
api/remote/public-address.ts

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.

Functions

f
bareHostname(url: URL): string

Returns url's hostname without the brackets URL.hostname keeps around an IPv6 literal: the address checks here read it, and fetch.ts hands it to curl's --resolve.

f
isPrivateAddress(ip: string): boolean

Checks whether ip is an IPv4 or IPv6 address outside the public internet. An IPv4 address carried in IPv6 is judged as IPv4. Anything that isn't an IP address is false, so a hostname must be resolved first.

f
isPrivateHostname(url: URL): boolean

Checks whether the host of url is private without a DNS lookup: a private IP address, or localhost and its subdomains. isValidHttpUrl uses it to reject a link early; only resolvePublicAddress is a real guard.

f
resolvePublicAddress(url: URL): Promise<string>

Resolves the host of url and returns one of its addresses, all of which must be public: the fetch may pick any of them. fetchWithTimeout calls it before each hop.

api/remote/bandcamp-streams.ts

Resolves a Bandcamp page to its streamable tracks when the native player starts it, for GET /api/remote/bandcamp. The stream URLs are signed and expire 24 hours after the page is fetched, so unlike the embed URL they can't be stored in the dump's preview. The browser plays them from bcbits directly.

Functions

f
resolveBandcamp(
pageUrl: string,
unnamed 1?: { force?: boolean; }
): Promise<Tralbum>

Returns the tracklist of the Bandcamp page pageUrl, from the cache when it was resolved in the last 12 hours. force skips the cache: the player sets it when it re-resolves a queue whose URLs expired or are about to, which a cached entry could hand back.

api/remote/bandcamp-tralbum.ts

Reads a release out of a Bandcamp page, for bandcamp-streams.ts. Every track and album page carries the whole release, stream URLs included, as HTML-escaped JSON in a data-tralbum attribute, so one fetch covers an album. The format is undocumented, so every field is treated as optional.

Functions

f
parseTralbum(
html: string,
sourceUrl: string
): Tralbum | null

Returns the release on the Bandcamp page html, fetched from sourceUrl; null when the page has no release data, no audio or no streamable track. Stream URLs outside bcbits.com are dropped.

Variables

v
BANDCAMP_HOST_RE: RegExp

Matches the hostnames bandcamp-streams.ts accepts: bandcamp.com and its subdomains, and not bandcamp.com.example.org. An artist on a custom domain doesn't match, and the player falls back to the embed.

api/remote/fetch.ts

Fetches pages and images from other sites for link previews, the image proxy and the Bandcamp resolver, without letting a user-supplied URL reach the server's own network. Each hop must resolve to a public address, the whole exchange has a deadline, and pages are read up to a cap.

Functions

f
fetchPage(url: string): Promise<FetchedPage>

Fetches a page for its link preview, through fetchWithTimeout. A response that isn't HTML is cancelled unread.

f
fetchWithTimeout(
url: string,
timeoutMs?: number
): Promise<Response>

Fetches a user-supplied URL, following up to five redirects, each of which must resolve to a public address too. timeoutMs bounds each hop, body included. The caller must read or cancel the body, or its connection stays open.

f
isValidHttpUrl(raw: string): boolean

Checks whether raw is an http(s) URL on a host that isn't private, or a link to one of this site's own pages. Routes and services check a URL with it before storing, previewing or proxying it; it does no DNS lookup, which fetchWithTimeout does.

f
readBytesCapped(
res: Response,
maxBytes: number
): Promise<Uint8Array>

Reads the body of res up to maxBytes; the rest is cancelled, never downloaded. The image proxy reads through it, and readTextCapped.

f
readTextCapped(
res: Response,
maxBytes?
): Promise<string>

Reads the body of res as text, up to maxBytes, in the charset the page declares; the rest is cancelled, never downloaded. fetchPage and the Bandcamp and YouTube resolvers read pages through it.

Interfaces

I
FetchedPage

Returned by fetchPage to the link preview providers.

  • html: string

    The page's text, up to MAX_PAGE_BYTES; missing when it isn't HTML.

  • url: string

    The URL the page was served from, after redirects: the base its relative links resolve against.

Variables

v
MAX_PAGE_BYTES: number

The default cap of readTextCapped. The metadata previews read is in the <head>, and a page past this is a download.

api/remote/html.ts

Reads a link preview's metadata out of a page's HTML. parsePage scans the page once for the tags that carry it (<meta>, <link>, <img>, <title> and JSON-LD scripts) without building a document, and returns a Page that the providers' genericPreview picks from. Every field is undefined when the page doesn't say.

Functions

f
absoluteHttpUrl(
raw: string | undefined,
base: string
): string | undefined

Returns raw resolved against base as an absolute http(s) URL; undefined for a data: URI, another scheme or a URL that doesn't parse. Every URL a page names goes through it, so /og.png and //cdn/og.png work.

f
decodeHtmlEntities(str: string): string

Decodes the entities in str in a single pass, so &amp;quot; reads &quot;. An entity it doesn't know, or a code point that doesn't exist, is left as written. Also used by bandcamp-tralbum.ts on the JSON a page carries in an attribute.

f
normalizeCssColor(raw: string | undefined): string | undefined

Returns raw as a lowercase #rrggbb, from a hex, rgb(), rgba() or common named color; undefined for any other form. The generic preview stores it as the preview's accentColor, a tint over the app's own surface, so alpha is dropped, and a fully transparent one is no color at all.

f
parsePage(
html: string,
pageUrl: string
): Page

Reads html, served from pageUrl, in one pass. The generic preview and the providers built on it read a page through it.

Interfaces

I
JsonLdResult

Returned by the JSON-LD reading of parsePage, which the generic preview ranks below the page's OG and Twitter tags.

I
Page

Returned by parsePage: what a page says about itself, entities decoded, values trimmed and URLs absolute.

api/remote/twemoji.ts

Fetches Twemoji glyphs from the jsDelivr CDN, for lib/site-icons.ts to render the favicons from.

Functions

f
fetchTwemojiSvg(
emoji: string,
cp: string
): Promise<string>

Returns Twemoji's SVG for emoji, whose file name cp comes from emojiToCodepoint. emoji only serves the error message. It fetches with plain fetch, since the host is fixed, and gives up after 15 s.

api/remote/image-proxy.ts

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/remote/image serves, which the frontend routes a thumbnail through when it is on http or when loading it directly failed, and scales them down as the site's own are.

Functions

f
clearImageCache(): void

Empties the cache, for the tests' reset between cases.

f
fetchProxiedImage(
url: string,
width?: ImageWidth | null
): Promise<ProxiedImage | null>

Returns the image at url, from the cache or else from its host; null when the host answers with an error, or with anything but a raster image of 5 MB at most. With width, it comes as a WebP copy scaled down to it, or as it is when no copy can be made (scalableImage, and ffmpeg). The route checks url with isValidHttpUrl first.

Interfaces

I
ProxiedImage

Returned to GET /api/remote/image by fetchProxiedImage, which sends it back under contentType.

api/remote/youtube-playlist.ts

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/remote/youtube.

Functions

f
parsePlaylistPage(
html: string,
listId: string
): YouTubePlaylist | null

Returns the playlist listId as read from its page html; null when the page has no ytInitialData or no track in it.

f
resolveYouTubePlaylist(listId: string): Promise<YouTubePlaylist>

Returns the tracklist of the playlist listId, from the cache when it was resolved in the last 6 hours.

api/remote/gerbeur-instance.ts

Talks to other gerbeur instances for federated sign-in: it reads their metadata and public keys, trades a code for an identity token, delivers events, and fetches a user's avatar. federation-service.ts, identity-provider-service.ts and federation-peer-service.ts reach other instances only through it.

Functions

f
assertReachable(url: URL): Promise<void>

Checks that url may be reached: https on a public address, or anything under GERBEUR_FEDERATION_ALLOW_PRIVATE.

f
clearInstanceCaches(): void

Empties the caches, for the tests' reset between cases.

f
exchangeCode(
origin: string,
params: { code: string; clientId: string; redirectUri: string; codeVerifier: string; }
): Promise<string>

Trades code for an identity token at the token endpoint of the instance at origin, and returns the token, unverified.

f
fetchAvatar(url: string): Promise<Uint8Array>

Returns the image at url, a user's avatar on their home instance, up to the size an avatar may be.

f
fetchKeys(
origin: string,
fresh?: boolean
): Promise<JSONWebKeySet>

Returns the public keys of the instance at origin, cached unless fresh asks to refetch them and they weren't in the last minute.

f
fetchMetadata(origin: string): Promise<InstanceMetadata>

Returns the metadata of the instance at origin, its issuer, cached. It must name origin as its issuer and speak this protocol's version.

f
postEvent(
origin: string,
event: string
): Promise<boolean>

Sends the signed event event to the events endpoint of the instance at origin, and returns whether it took it. A 4xx other than 429 means it never will, which the caller treats as delivered.

f
resolveIssuer(origin: string): Promise<string>

Returns the issuer of the instance at origin, cached: origin itself, or, when origin is where the instance's pages are and its API lives at another, the issuer the pages' metadata names, whose own metadata must name those pages back as where its /authorize is. People know an instance by the address in their browser, which a sign-in form or the trust list may be given; a handle and the stored identities name the issuer.

api/config.ts

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.

Type Aliases

T
BandcampPlayer = "embed" | "native"

How the frontend plays Bandcamp. "embed" uses Bandcamp's iframe, which can't autoplay. "native" resolves the page's streams through GET /api/bandcamp/tracks and plays them in the app's player, which autoplays and queues an album's tracks.

Variables

v
ALLOWED_ORIGINS: string[]

The origins that CORS in main.ts and the WebSocket route accept. They are PUBLIC_URL, FRONTEND_URL and the comma-separated GERBEUR_ALLOWED_ORIGINS.

v
ATTACHMENTS_DIR: string

Holds the images pasted into chat messages, comments, dump notes and descriptions. The startup tasks purge those nothing links to.

v
AVATARS_DIR: string

Holds each user's avatar.

v
BANDCAMP_PLAYER: BandcampPlayer

How the frontend plays Bandcamp (GERBEUR_BANDCAMP_PLAYER), passed to it in index.html. Any value but "native" means "embed", so a typo falls back to the player that always works.

v
COMMUNITY_REFRESH_MINUTES: number

How often, in minutes, the /community page's figures are recomputed (GERBEUR_COMMUNITY_REFRESH_MINUTES), 15 by default.

v
DB_PATH: string

The SQLite database file (GERBEUR_DB_PATH). api/test/setup.ts points it at a throwaway file before anything opens the database.

v
DEFAULT_STYLE: StyleName

The instance's style (GERBEUR_DEFAULT_STYLE). The site shows it until a visitor or their account picks one, through index.html, and the emails that aren't a user's own (the welcome and the password reset) are drawn in it. A value that isn't a style means BASE_STYLE.

v
DUMP_ALLOWED_MIME_PREFIXES: string[]

The MIME type prefixes a file dump may have. dump-service also accepts the exact types of DUMP_ALLOWED_MIME_TYPES.

v
DUMP_ALLOWED_MIME_TYPES: Set<string>

The documents and archives a file dump may be, beyond the prefixes.

v
DUMPS_DIR: string

Holds the file of each file dump.

v
EMAIL_ENABLED: boolean

Whether the instance sends email, which is whether SMTPS_URL is set. email-service.ts opens its transport by it, and index.html tells the client, whose settings show the digest switches disabled without it.

v
EMAIL_LOCALE: Locale

The language of every email (GERBEUR_EMAIL_LOCALE). The interface picks each visitor's language in their browser, which the server never learns, so the emails take the instance's. A value that isn't a locale means the source one, English.

v
v
FEDERATION_ALLOW_PRIVATE: boolean

Lets federated sign-in reach instances on private addresses, localhost among them, and over plain http (GERBEUR_FEDERATION_ALLOW_PRIVATE=1), so that two instances on one machine can trust each other in development. Off, remote/gerbeur-instance.ts reaches only public https instances, since an identity is only as safe as the connection it came over.

v
FROM_EMAIL: string

The From address of every email (GERBEUR_FROM_EMAIL), as an address or as Name <address>. email-service.ts refuses to load without it when SMTPS_URL is set.

v
FRONTEND_URL: string

Points password reset links and the Open Graph tags' page URLs at the frontend when it isn't served from PUBLIC_URL, as with a separate Vite server, and allows it as an origin. It is GERBEUR_FRONTEND_URL, without a trailing slash.

v
JWT_SECRET: string

The key lib/jwt.ts signs and verifies tokens with (GERBEUR_JWT_SECRET). The server refuses to start without it.

v
LISTEN_HOST: string

The interface the server binds to (GERBEUR_LISTEN_HOST). The default, 0.0.0.0, lets Docker forward the port; 127.0.0.1 keeps it to loopback.

v
MAX_IMAGE_SIZE_BYTES: number

Caps the size of an image upload, in bytes, for avatars, covers, thumbnails and attachments. lib/upload.ts states it in megabytes in its error.

v
OG_SITE_NAME: string

The site's name (GERBEUR_SITE_NAME). It is filled into index.html, the OG tags, the icons' manifest and the emails.

v
ORPHANED_ATTACHMENTS_RETENTION_HOURS: 1

How long an attachment nothing links to is kept before startup purges it.

v
PAGINATION_DEFAULT_LIMIT: 20

The page size when a request names none.

v
PAGINATION_MAX_LIMIT: 100

The largest page size a request may ask for.

v
PLAYLIST_IMAGES_DIR: string

Holds each playlist's cover.

v
PORT: number

The port the server listens on (GERBEUR_PORT, default 8000).

v
PUBLIC_URL: string

The URL visitors reach the server at (GERBEUR_PUBLIC_URL), without a trailing slash. Behind a reverse proxy it must be the outside URL: it is an allowed origin, it tells links to this site's own pages from other links, and it prefixes the image URLs in their previews.

v
SCALED_DIR: string

Holds the scaled-down copies of the images above, which lib/ffmpeg.ts makes as they are first asked for, each at its upload's path with its width. It can be emptied at any time: the copies are made again.

v
SITE_EMOJI: string

The emoji that serves as the site's logo (GERBEUR_SITE_EMOJI). The icons script draws the favicons from it, and index.html carries it to the app's header, so changing it needs a restart but no rebuild.

v
SMTPS_URL: string

The smtps:// URL of the server email-service.ts sends mail through (GERBEUR_SMTPS_URL). When it is empty, no email is sent: sign-up sends no welcome, a password reset request does nothing, and there are no digests.

v
THEME_COLOR: "#111827"

The background of the generated Apple touch icon, the manifest's colors, and the accent of previews of this site's own pages. index.html's theme-color tags don't follow it.

v
THUMBNAILS_DIR: string

Holds the thumbnails a poster uploads (<id>-custom) and those the thumbnails route generates from file dumps.

v
UNUSED_INVITES_RETENTION_DAYS: 7

How long an invite stays usable. invite-service refuses an older one, and the startup tasks purge it.

v
UPLOADS_DIR: string

The directory uploads are stored under (GERBEUR_UPLOADS_DIR). Each kind has its subdirectory below, created by the first upload, and each file is named after the id of what it belongs to.

v
WELCOME_EMAIL_BODY: string | null

The Markdown of the welcome email sent on registration (GERBEUR_WELCOME_EMAIL_BODY), or null for the built-in one in the EMAIL_LOCALE. api/mail/welcome.ts replaces {{site_name}} and {{username}} in it.

api/db/migrate.ts

Brings the database up to date with the migrations under migrations/, and records each one it applies in schema_migrations. connection.ts runs it each time the server opens the database.

Functions

f
runMigrations(db: DatabaseSync): void

Applies each migration db hasn't recorded yet, in order, each in its own transaction.

api/db/connection.ts

Opens the database at DB_PATH when first imported, creating it from schema.sql if the file doesn't exist, then applies the pending migrations and runs the startup tasks before any importer runs. Only the services import it.

Functions

f
transaction<T>(fn: () => T): T

Runs fn in a transaction, so its writes commit together, in one commit rather than one each, and a throw rolls them all back and rethrows. A call inside another joins the outer transaction. fn must be synchronous: an await would commit before the rest of it runs.

Variables

v
db: DatabaseSync

Serves every query of the server, as its only connection. Its queries are synchronous, so each one blocks the event loop while it runs.

api/db/migrations/0022_reserve_community.ts

Reserves community at the URL root for the community page, renaming a category that had it as its slug.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations.

api/db/migrations/0015_token_revocation.ts

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.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Existing users start with NULL, which revokes no token.

api/db/migrations/0020_digest_schedule.ts

Lets digest-service.ts know when each user's next digest is due. digest_schedule holds, for each user and cadence they receive, the start of the period their next digest covers; it is due once a full period has passed.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Every user present gets both cadences, with a first period that ends at some hour of the coming week or month, picked from their id: the digests start trickling out after the deploy instead of all leaving a week later. Users created later are scheduled by the service when it first sees them.

api/db/migrations/0001_comment_likes.ts

Adds likes on comments, as the comment_likes table and a like_count on each comment that comment-like-service keeps in step, so that listing comments doesn't count their likes.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Existing comments start with no likes, and a database made from schema.sql already has both.

api/db/migrations/0011_split_favicon_thumbnail.ts

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.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. It leaves alone a row that already has a faviconUrl, and one whose rich_content doesn't parse. 0012 undoes its mistakes.

api/db/migrations/0003_dump_backlinks.ts

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.

api/db/migrations/0019_user_settings.ts

Gives each user settings stored on the server, so that they follow the account across browsers and the server can read them: the digests need the reader's choice to receive them and their style. user_settings holds one row per user who changed anything; a user without one has the defaults.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Existing users get no row, so they start with the defaults, digests on, like a new account.

api/db/migrations/0004_user_roles.ts

Makes room for moderators between users and admins. It adds users.role, which replaces the is_admin flag, and 0005 then drops the flag.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Every user becomes a user, except those with is_admin set, who become admins.

api/db/migrations/0012_fix_favicon_reclassification.ts

Repairs the rows 0011 got wrong. 0011 took any path containing "favicon", or ending in .svg, for an icon, so artwork such as /wp-content/uploads/2022/04/mir-logo-favicon.png lost its thumbnailUrl, and its dump showed as a pull-quote instead of an image in the journal mosaic.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. It moves a faviconUrl back to thumbnailUrl only when 0011's rule takes it for an icon and this one doesn't. That also catches the ones a later fetch stored that this rule doesn't recognise, such as a logo.svg. It then moves the icons 0011 missed.

api/db/migrations/0006_categories.ts

Adds categories, which admins create and posters file their dumps under, as the categories table and the dump_categories join table.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Existing dumps start in no category, and a database made from schema.sql already has both tables.

api/db/migrations/0008_chat_messages.ts

Adds chat_messages, which holds the messages of the site-wide chat.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. A database made from schema.sql already has the table.

api/db/migrations/0021_page_urls.ts

Moves the site to v4's page URLs, where dumps and playlists live under their owner (/@bob/dumps/<slug>) and are named by an editable slug alone. A slug was slugify(title)-<8 hex of the id>, unique site-wide; it is now the bare slugify(title), unique in its owner's space for its kind, and every slug an item or a user leaves behind stays an alias that redirects (dump_slug_aliases, playlist_slug_aliases, user_aliases).

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. On a database made from schema.sql the tables and indexes exist and there is nothing to re-slug or rewrite.

api/db/migrations/0007_password_reset_tokens.ts

Adds password_reset_tokens, which holds the single-use tokens that password-reset-service emails to users who forgot their password.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. A database made from schema.sql already has the table.

api/db/migrations/0013_dump_url_canonical.ts

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.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. It fills only the keys still NULL, and leaves NULL a URL that isn't http(s).

api/db/migrations/0002_dump_thumbnail.ts

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.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Existing dumps keep their fetched thumbnails, and a database made from schema.sql already has the column.

api/db/migrations/0025_playlist_categories.ts

Gives playlists categories of their own, a set apart from the dumps': each category gets a kind, dump or playlist, its slug and name now unique within its kind only, and playlist_categories files playlists under them.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Every existing category becomes a dump category, filing the same dumps.

api/db/migrations/0014_chat_read_state.ts

Moves the chat's unread state to the server, so that it survives a reload and agrees across tabs and devices. users.chat_read_at holds the time of the newest message the user has seen.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Existing users start with every message read, instead of the whole history unread. A user created later starts with a NULL chat_read_at, which chat-service reads as the user's creation time.

api/db/migrations/0010_youtube_embed_start.ts

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.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. It leaves alone an embed URL that already has a start, and a row whose rich_content doesn't parse.

api/db/migrations/0009_chat_reply.ts

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.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Existing messages reply to nothing, and a database made from schema.sql already has the column.

api/db/migrations/0024_federation.ts

Prepares federated sign-in, by which a user signs in here with their account on another gerbeur instance, and the other way round. It adds the instance's signing key, the link from a local account to the one it stands for elsewhere, the instances an admin trusts or blocks, the state of the sign-ins in progress on both sides, the grants users gave other instances, and the events owed to them.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Nothing exists to convert, since every account so far is local, but authorize and federation become the pages of a sign-in, so a category that had one as its slug is renamed.

api/db/migrations/0023_mention_everyone.ts

Prepares the chat's @everyone: it adds chat_messages.mentions_everyone, and frees the name everyone, which no account may have any more.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. The messages that a moderator or an admin already sent with @everyone get the flag, so that editing one later doesn't notify everyone of an old message; none is notified now. A user named everyone, in any case, is renamed to the first free everyone_2, everyone_3…, with no alias left behind, and an alias everyone is dropped. Both are logged.

api/db/migrations/0017_case_insensitive_users.ts

Makes usernames and emails unique regardless of case. Usernames were unique only as typed, so "Bob" and "bob" could both sign up, and a mention, which looks names up in lower case, then reached one of them at random. Emails had no constraint at all: two sign-ups racing past the service's check could share an address, and password resets are looked up by address.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. It refuses to run, naming the clashes, when two accounts already share a name or an address in some case: which one keeps it is for whoever runs the site to decide, by editing one of them.

api/db/migrations/0005_drop_is_admin.ts

Drops users.is_admin, now that 0004 has copied it into users.role.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations, after 0004. A database made from schema.sql never had the column.

api/db/migrations/0018_attachment_resource_type.ts

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.

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. Existing bound attachments get the type of the table that holds their resource; one whose resource is gone gets none, and the startup purge deletes it. A database made from schema.sql already has the column.

api/db/migrations/0016_performance_indexes.ts

Adds indexes for the queries that scanned a whole table. The feeds page dumps by created_at, and a user's dumps and upvotes by (user_id, created_at), which replace the indexes on user_id alone. Dumps and playlists are looked up by slug. Each foreign key with ON DELETE gets an index on its child column, which SQLite otherwise scans on every delete of a parent row, and the followers of a user or playlist are read on every new dump. Mentions look usernames up by lower(username).

Functions

f
up(db: DatabaseSync): void

Run once by runMigrations. A database made from schema.sql already has these indexes.

api/db/startup.ts

Deletes what has expired and seeds the first admin, each time connection.ts opens the database.

Functions

f
runStartupTasks(db: DatabaseSync): Promise<void>

Runs after the migrations, each time the server starts. It deletes used or expired reset tokens, invites past their validity, and attachments nothing links to or whose resource is gone. On an instance with no users, it creates the user admin with the password admin, which whoever installs it must change.

shared/chat.ts

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.

Interfaces

I
ChatMessage

Returned by GET /api/chat, and sent by the server in chat_message and chat_message_updated. The client reads it through deserializeChatMessage, which turns its dates back into Dates.

shared/attachment.ts

Says where an image pasted into a text lives, and how to find it again in the text. The editor uploads the image and writes a link to it into the Markdown; the server binds the attachments a saved text links to that text, and Markdown gives a private one the viewer's media token.

Functions

f
attachmentIdsIn(text: string): string[]

Returns the ids of the attachments text links to, in order.

f
attachmentPath(id: string): string

Returns the API path of attachment id, to which the API's base is added.

f
isAttachmentUrl(url: string): boolean

Checks whether url is an attachment's, with no query yet.

Interfaces

I
AttachmentResponse

Returned by POST /api/attachments to the text editor, which inserts the image into the body as a link to attachmentPath.

shared/slug.ts

Defines what a slug is: the readable name of a category, a dump or a playlist in a page URL. The server makes and checks slugs with it, and the edit forms prefill their slug field with slugify.

Functions

f
isValidSlug(
slug: string,
max?: number
): boolean

Checks whether slug matches SLUG_RE and is at most max characters long.

f
slugify(
title: string,
max?: number
): string

Turns title into lowercase ASCII words joined by hyphens, at most max characters long, cut at a hyphen when it must be cut. Accents are dropped, and a title with no ASCII letter or digit gives untitled. The result always passes isValidSlug.

Variables

v
SLUG_MAX: 60

The longest a dump's or a playlist's slug may be.

v
SLUG_RE: RegExp

Matches a well-formed slug: lowercase letters and digits, in words joined by single hyphens. Categories, dumps and playlists share it.

shared/settings.ts

A user's settings follow their account from browser to browser, and the server reads some of them itself (a digest is drawn in its reader's style). This module types them for GET and PATCH /api/me/settings; the client keeps them with its session.

Functions

f
isDigestCadence(value: unknown): value is DigestCadence

Checks whether value is a DigestCadence.

Interfaces

I
UserSettings

Returned by GET /api/me/settings, and by a PATCH once applied. The client's useSettings reads it from the session.

Type Aliases

T
DigestCadence = DIGEST_CADENCES[number]

One of the DIGEST_CADENCES. An unsubscribe link names one, which turns off its setting alone.

T
UpdateSettingsRequest = Partial<UserSettings>

Sent to PATCH /api/me/settings with the settings to change; the others keep their value.

Variables

v
DEFAULT_SETTINGS: UserSettings

The settings of a user who never changed any. Digests are on by default.

v
DIGEST_CADENCES: "weekly" | "monthly"[]

Lists how often a digest can come: weekly, or monthly as a recap.

v
DIGEST_SETTING: Record<DigestCadence, "digestWeekly" | "digestMonthly">

Names the setting that switches each cadence of digest.

v
PERIOD_MS: Record<DigestCadence, number>

How long each cadence's period lasts, rolling: a digest covers the one just ended, and the /community page the one ending now.

shared/mention.ts

Makes the server notify the same users the chat highlights and the editor suggests, by defining once what counts as an @mention.

Functions

f
mentionedUsernames(body: string): string[]

Returns the names body mentions, in lower case, each once. Code, fenced or inline, and URLs mention no one, as Markdown shows them. The server notifies by it, and the chat highlights and chimes by it.

f
mentionsEveryone(body: string): boolean

Checks whether body says @everyone, in any case, by the rule of mentionedUsernames. Only the chat service reads it, to set a message's mentionsEveryone when its author may; the client reads that flag, not the text.

Variables

v
EVERYONE: "everyone"

The name @everyone reaches. Said by a role with chat:everyone in the chat, it notifies every user, so no account may take it (assertUsernameAvailable).

v
MENTION_RE: RegExp

Matches an @handle, a local @name or a remote user's @name@host (HANDLE_SOURCE), with the handle as its first group. The @ must not follow a word character, so an email address mentions no one. Nor may it follow [ or (, which leaves out a Markdown link's text or URL starting with @, and also a plain (@name). The editor's suggestions match its source at the caret; the rest goes through mentionedUsernames. Markdown links mentions with a looser pattern of its own, on text already parsed, where code and URLs are out of the way.

shared/markdown.ts

Flattens the Markdown users write into one line of plain text, where a quote of it can't render markup: the chat's reply previews, the digest email's quotes of a comment or a note, and the line of a description on a playlist's card and on a user in the search results.

Functions

f
markdownToPlainText(
md: string,
imageLabel: string
): string

Returns the Markdown md as one line of plain text. Images become their alt text, or imageLabel without one, and links their text. It also takes a body cut short, possibly inside a link, as the server cuts a chat message's replyToBody: the unfinished link then shows as its text, without half a URL.

shared/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).

Functions

f
hasPermission(
role: Role,
permission: Permission
): boolean

Checks whether role grants permission. The server's can and the client's can both wrap it.

Type Aliases

T
Permission =
"dump:moderate"
| "comment:moderate"
| "playlist:moderate"
| "chat:moderate"
| "chat:everyone"
| "user:manage"
| "category:manage"
| "federation:manage"

An action only some roles may take. A new one goes in this union and in ALL_PERMISSIONS, which is what grants it to admins.

Variables

v
ROLE_PERMISSIONS: Record<Role, readonly Permission[]>

Read through hasPermission. Roles are fixed in code. An admin holds every permission, and a moderator the moderation ones and @everyone in the chat.

shared/url.ts

Names the site a link points at, as the app shows it: a link dump's title when its page has none, a preview's site name, the digest's domain line and the tint of a card without artwork. The server's link checks key on it too (canonical-url.ts), so a link reads as the same site everywhere.

Functions

f
siteHost(raw: string): string | undefined

Returns siteHostOf the URL raw, or undefined when raw isn't one.

f
siteHostOf(url: URL): string

Returns url's host without a leading www.; URL has already lowered its case. www.youtube.com and youtube.com give youtube.com.

shared/federation.ts

Types what crosses the wire for federated sign-in, by which a user signs in to one gerbeur instance with their account on another.

Functions

f
instanceHost(issuer: string): string

Returns the host of the instance at issuer, with its port if it has one: the part after the @ of its users' handles here (alice@music.example).

f
isFederationEvent(value: unknown): value is FederationEvent

Checks whether value is a FederationEvent, as the events endpoint must once the signature is verified.

f
isIdentityClaims(value: unknown): value is IdentityClaims

Checks whether value carries the IdentityClaims, as a client must before creating or updating an account from them.

f
isInstanceMetadata(value: unknown): value is InstanceMetadata

Checks whether value is an InstanceMetadata of this protocol's version, as remote/gerbeur-instance.ts must before reading one.

f
isPeerStatus(value: unknown): value is PeerStatus

Checks whether value is a PeerStatus.

f
parseInstance(input: string): string | null

Returns the origin of the instance input names, or null when it names none. Someone may type a bare host (music.example, taken as https), a URL, or a handle's host with its port (localhost:8001); a URL with a path names no instance. Whether plain http is allowed is the server's call.

Interfaces

I
AuthorizeRequest

Sent to POST /api/oauth/codes by the /authorize page once the user consents, from the query the client instance sent them with.

I
AuthorizeResponse

Returned by POST /api/oauth/codes: the client's callback page with the code, where the /authorize page sends the user.

I
FederationGrant

Returned by GET /api/me/grants to the settings page: an instance the user let their account sign in to.

I
FederationLoginRequest

Sent to POST /api/federation/logins by the login page, or by the register page with the invite it was opened with.

I
FederationLoginResponse

Returned by POST /api/federation/logins: the home instance's /authorize page with the request in its query, which the client navigates to.

I
FederationPeer

Returned by GET /api/federation/peers: the login page offers each trusted one, and /manage lists them all.

I
FederationSessionRequest

Sent to POST /api/federation/sessions by the /federation/callback page, from the query the home instance sent the user back with. The answer is an AuthResponse, as a sign-in's.

I
IdentityClaims

Carried by an identity token and by an update event: what a home instance tells another of a user. The names are OpenID Connect's.

I
InstanceMetadata

Served by every instance at /.well-known/openid-configuration (routes/well-known.ts), and read by another before it trusts it, starts a sign-in on it or sends it an event. The names are OpenID Connect's, and the gerbeur_ fields are this protocol's own.

I
OAuthClient

Returned by GET /api/oauth/clients?client_id= to the /authorize page, which asks the user whether to sign in there.

I
SetPeerRequest

Sent to PUT /api/federation/peers by /manage.

Type Aliases

T
FederationEvent =
{ type: "update"; claims: IdentityClaims; }
| { type: "revoke"; }

The payload of a signed event, which a home instance sends each instance a user signed in to (POST /api/federation/events): update when their name, email or avatar changed, revoke when their sessions there must end, after a password change or when they withdraw the grant.

T
PeerStatus = "trusted" | "blocked"

What an admin decided about an instance. Users of a trusted one sign in here directly; those of a blocked one never do, and its users already here are signed out. An instance with neither is invite only: its users sign in here with an invite.

Variables

v
FEDERATION_VERSION: 1

The version of the protocol below, which an instance announces in its InstanceMetadata. An instance refuses one announcing another.

shared/validation.ts

Makes the forms check the same limits on what users submit as the server enforces, by holding them in one place.

Functions

f
localHandle(
handle: string,
isSelfHost: (host: string) => boolean
): string

Returns handle as this instance writes it, isSelfHost telling its own host. Another instance writes a user from here with this one's host, bob@gerbeur.example, who is bob here: a mention or a link copied from there names them so. Any other handle is returned as it is. The server resolves names by it (resolveUsername, the mention notifications), and the chat's highlight (mentionsUser) reads mentions by it.

f
splitHandle(handle: string): { name: string; host?: string; }

Returns the name and, for a user from another instance, the host of handle. splitHandle("alice@music.example") is { name: "alice", host: "music.example" }; a local name has no host.

Variables

v
DUMP_MAX_FILE_SIZE_BYTES: number

The largest file a file dump may hold, in bytes. dump-service refuses a bigger one, and the create form before uploading it.

v
EMAIL_RE: RegExp

Matches an email address well enough to catch a typo; only the reset email arriving proves it.

v
HANDLE_RE: RegExp

Matches a whole handle (HANDLE_SOURCE).

v
HANDLE_SOURCE: string

The pattern of a handle, the name a user goes by on this instance: a local user's username, or name@host for a user whose account lives on another gerbeur (federation-service.ts stores it so in users.username). Paths, mentions and the profile links all read names by it, through HANDLE_RE, MENTION_RE (shared/mention.ts) and parsePagePath (shared/paths.ts).

v
HOST_RE: RegExp

Matches a whole host (HOST_SOURCE).

v
HOST_SOURCE: string

The pattern of an instance's host as a handle writes it: DNS labels, and a port for instances on one machine (localhost:8001). It ends on a letter or a digit, so the period that ends a sentence after a mention isn't taken in. HANDLE_SOURCE builds on it.

v
RENAME_LIMITS: { SLUG: { count: number; windowMs: number; }; }

How often a dump's or a playlist's slug may change, counted from the aliases the changes left: 3 times a day. Moderators are exempt. slug-service refuses a change past it with a 429, and the edit forms say what the limit is.

v
USERNAME_RE: RegExp

Matches a valid username, made of letters, digits and underscores within the VALIDATION limits. Sign-up and renaming check it on both sides. It uses the same \w that a handle's pattern (HANDLE_SOURCE) reads a name with; only a user from another instance has the @host part.

v
UUID_RE: RegExp

Matches an id the server makes with crypto.randomUUID(). A route checks one before it names a file on disk, and shared/attachment.ts finds the attachments a text links by it.

shared/user.ts

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.

Functions

f
isRole(value: unknown): value is Role

Checks whether value is a Role, as the role endpoint and the row guards must before trusting one.

Interfaces

I
AuthResponse

Returned by sign-up and sign-in.

I
AuthTokens

Returned by sign-up, sign-in and a password change. The client keeps both tokens with its session.

  • mediaToken: string

    Carried as ?token= by the URLs of private media. It can read media and nothing else.

  • token: string

    Sent as Authorization: Bearer …, and as ?token= to the WebSocket.

I
Invitee

Returned, as a flat list in sign-up order, by GET /api/users/:key/invitees. The profile page nests it into a tree by invitedById.

I
InviteResponse

Returned by POST /api/invites to the profile page, which builds the invite link from token.

I
LoginUserRequest

Sent to POST /api/sessions by the login page.

I
PublicUser

Returned by every endpoint that shows a user to others: profiles, the users one follows, search results, and the user_updated socket message. The server makes it with toPublicUser, which drops the email.

I
RegisterUserRequest

Sent to POST /api/users by the register page.

I
SelfUser

Returned to a user about themselves: by sign-up and sign-in in AuthResponse, by GET /api/me, and by an avatar upload.

  • email: string

    Where password reset links go. It belongs to this account only.

I
UpdateUserRequest

Passed to updateUser by the users routes and the password reset, and sent by a client to PATCH /api/me, which takes only description and email; the role and the password change through their own endpoints, and the username through renameUser, by a moderator only.

I
UserSuggestion

Returned by GET /api/users?q= to the editor's @mention autocomplete.

Type Aliases

T
MediaTokenResponse = Pick<AuthTokens, "mediaToken">

Returned by GET /api/me/media-token to a session from before media tokens, which has none.

T
Role = "user" | "moderator" | "admin"

Sets what a user may do beyond the basics, through ROLE_PERMISSIONS (shared/permissions.ts). An admin changes it with PATCH /api/users/:key/role.

Variables

v
ROLES: readonly Role[]

Every Role, least to most privileged.

shared/search.ts

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.

Interfaces

I
SearchResults

Returned by GET /api/search to the Search page. Only the dumps are paged, and each page brings the users and playlists again.

shared/digest-messages.ts

Words the digest emails share with the pages that show or govern them: the /community page shows the digests' figures and sections, Settings and the unsubscribe page name the digests, the avatar menu links the community page. api/mail/digest.ts and those pages translate them from here, so an email and a page always say the same.

Variables

shared/locale.ts

The app is translated into a few languages, one Lingui catalogue each under locales/. This module lists them once for every reader: lingui.config.ts extracts and compiles a catalogue per locale, the client picks one per browser, and the server writes its emails in GERBEUR_EMAIL_LOCALE.

Functions

f
isLocale(value: unknown): value is Locale

Checks whether value is a Locale.

Type Aliases

T
Locale = LOCALES[number]

One of the LOCALES.

Variables

v
LOCALES: "en" | "fr"[]

Lists the locales the app is translated into.

v
SOURCE_LOCALE: Locale

The locale the messages are written in, and the fallback for any other.

shared/community.ts

What the whole site did, told two ways. Over a period, one of the digests' cadences: its most upvoted dumps, who joined, the new public playlists and the totals, which the /community page shows as they were at the server's last refresh and the digests gather for each reader's own period. And as it happens: the community feed, the latest things its members did in public, which the GeoCities style's marquee scrolls.

Interfaces

I
CommunityFeed

Returned by GET /api/community/feed to CommunityFeedProvider, which asks again as the socket tells of changes, and rendered by the CommunityFeed component with who is online, which the client knows from its socket. community-service.ts gathers it. Everything in it is public: every reader gets the same feed.

I
CommunityItem

Names a dump or a playlist in a CommunityEvent: its title, and what dumpPath or playlistPath builds its page's path from.

I
CommunityStats

Returned by GET /api/community?period= to the Community page, and gathered by community-service.ts, which the digests read too.

I
DumpCommentedEvent

A public dump got a comment. However many it got, the feed tells of its latest alone.

I
DumpPostedEvent

A public dump was posted. A dump made public later keeps its date.

I
DumpUpvotedEvent

A public dump got an upvote, told as DumpCommentedEvent is.

I
UserJoinedEvent

Someone joined the site: username is theirs.

Type Aliases

shared/category.ts

Categories file dumps and playlists by topic, in two sets apart, one of each CategoryKind, and give each topic its own page. This module types them for the server and the client, with the bodies that create and change them. Those who hold category:manage manage both sets on /manage. The client lists them in its CategoriesProvider.

Functions

f
isCategoryKind(value: unknown): value is CategoryKind

Checks whether value is one of the CATEGORY_KINDS, for the category validator and the rows the category service reads.

Interfaces

I
Category

Returned by the category routes. A dump category's feed lives at /<slug> on the client, a playlist category's at /playlists/<slug>, and a dump or playlist names its categories by id in categoryIds.

I
CreateCategoryRequest

Passed to POST /api/categories. The slug must be free within its kind, and a dump category's not reserved by a route.

I
UpdateCategoryRequest

Passed to PUT /api/categories/:id. A field left out keeps its value, and the kind is kept.

Type Aliases

T
CategoryKind = CATEGORY_KINDS[number]

One of the CATEGORY_KINDS.

Variables

v
CATEGORY_KINDS: "dump" | "playlist"[]

Lists what a category files: a dump category files dumps, a playlist category playlists. Each kind is a set of its own, whose slugs and names are unique within it.

shared/theme.ts

The site's look is a style and a color scheme, which the client applies and the emails render in. This module names them for both halves: the client's ThemeProvider checks a stored choice against them, and the server checks GERBEUR_DEFAULT_STYLE and picks an email's palette by them.

Functions

f
isColorScheme(value: unknown): value is ColorScheme

Checks whether value is a ColorScheme.

f
isStyleName(value: unknown): value is StyleName

Checks whether value is a StyleName, as a stored choice or an environment variable must be before it is applied.

Type Aliases

T
ColorScheme = COLOR_SCHEMES[number]

One of the COLOR_SCHEMES.

T
StyleName = STYLE_NAMES[number]

One of the STYLE_NAMES.

Variables

v
BASE_STYLE: StyleName

The style the stylesheets draw when the root element has no data-style, and the one an unset or unknown GERBEUR_DEFAULT_STYLE means.

v
COLOR_SCHEMES: "auto" | "light" | "dark"[]

Lists every color scheme. "auto" follows the system's.

v
DEFAULT_COLOR_SCHEME: ColorScheme

The scheme of whoever hasn't picked one: the system's.

v
STYLE_NAMES:
"smooth"
| "brutalist"
| "geocities"
| "nyt"
| "kanagawa"
| "melange"
[]

Lists every site style. Each has a stylesheet under src/themes/, keyed on the root element's data-style, and an email style in api/mail/styles.ts read from that stylesheet.

shared/dump.ts

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.

Functions

f
dumpFileVersion(dump: { fileSize?: number; createdAt?: Date; updatedAt?: Date; }): string

Returns a file dump's version, which the file and thumbnail URLs carry as ?v=: the frontend's dumpFileUrl and dumpThumbnailUrl, and the digest's thumbnails. It changes whenever the file may have, so the file and thumbnail routes cache a URL naming the current one for good and revalidate any other. Without its dates, a dump gets a version no route serves as current.

Interfaces

I
CreateUrlDumpRequest

Passed by the create form to POST /api/dumps as a JSON body, to post a link. A file is posted as a multipart form instead.

I
Dump

Returned by every endpoint that answers dumps, and pushed to clients by the dump_created and dump_updated socket messages. A URL dump has url and richContent, a file dump the file… fields. A private dump reaches its poster only.

I
DumpUrlMatch

Returned by GET /api/dumps?url= for each dump already posted with the link being typed, and shown by the create form's DuplicateNotice. It holds only what the notice displays and links to.

I
LinkPreview

Returned by GET /api/remote/preview to the create form, which shows it before the link is posted.

  • reached: boolean

    False when the page couldn't be fetched. richContent is then a stub holding the host name, and this flag is how the form tells a dead link from a page with no preview.

  • richContent: RichContent | null

    Null only for a link that doesn't parse, which the route rejects before fetching anyway.

I
RichContent

Built by a provider of rich-content-service.ts when a link is posted or previewed, and stored with the dump as JSON. The frontend's RichContentCard and player read it.

I
UpdateDumpRequest

Passed to PUT /api/dumps/:dumpId by the edit page, and by the dump page's inline title edit. A field left out stays as it is.

Type Aliases

T
RichContentType = "youtube" | "bandcamp" | "soundcloud" | "generic"

Names the kind of link a RichContent previews: a site the player plays its own way, or generic for any other page, this site's included.

shared/waveform.ts

Defines an audio file's waveform, the bars the player seeks on: how many peaks it has, and how they are taken from the samples. The server makes an uploaded file's from its decoded audio (api/lib/ffmpeg.ts), and the dump form a local file's in the browser (src/utils/player/waveform.ts), both with peaksOf, so the two draw alike.

Functions

f
peaksOf(
samples: ArrayLike<number>,
n?: number
): number[]

Returns n peaks of samples, each the loudest of its slice, scaled so the loudest of all is 1 and rounded to three places: a quiet recording fills the waveform as a loud one does. samples may be signed, as audio is, or already peaks of smaller slices. Silence gives zeros.

Interfaces

I
DumpWaveform

Returned by GET /api/dumps/:dumpId/waveform for an audio file dump, to the player's waveform.

Variables

v
WAVEFORM_PEAKS: 150

How many peaks, and so bars, a waveform has.

shared/ws.ts

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.

Interfaces

I
CategoriesChangedMessage

Sent by the category service to every socket after a category is created, edited or deleted. CategoriesProvider fetches the categories again.

I
ChatDeleteMessage

Sent by WSProvider when the user deletes a chat message, as ChatEditMessage. The server broadcasts ChatMessageDeletedMessage.

I
ChatEditMessage

Sent by WSProvider when the user edits a chat message, their own or, with chat:moderate, anyone's. The server broadcasts ChatMessageUpdatedMessage, or answers a chat ErrorMessage.

I
ChatFocusMessage

Sent by WSProvider when the chatbox opens or closes, when the tab is hidden or shown, and after each welcome. The server notifies no one of a chat mention while one of their sockets says open.

I
ChatMessageDeletedMessage

Sent by the server to every signed-in socket when a chat message is deleted. The client also keeps the id, to show the replies to it as replies to a deleted message.

I
ChatMessageMessage

Sent by the server to every signed-in socket, the sender's included, when a chat message is posted. WSProvider adds it to the live chat, counts it unread unless the user is reading the chat, and may chime.

I
ChatMessageUpdatedMessage

Sent by the server to every signed-in socket when a chat message is edited.

I
ChatReadMessage

Sent by WSProvider, at most once a second, as the user reads the chat. The server moves their read marker to at, a message's ISO createdAt, and answers ChatReadUpdateMessage. It drops the message silently when it is malformed or over its rate limit.

I
ChatReadUpdateMessage

Sent by the server to every socket of a user after their ChatReadMessage, so that all their tabs and devices update the chat's badge.

I
ChatSendMessage

Sent by WSProvider when the user posts in the chatbox, which shows the message as pending meanwhile. The server broadcasts it as ChatMessageMessage, or answers a chat ErrorMessage.

I
ChatTypingMessage

Sent by the chatbox while the user types, every 3 s, and once they pause. The server answers every signed-in socket with ChatTypingUpdateMessage when the user starts or stops, and forgets a typer it hasn't heard from in 6 s.

I
ChatTypingUpdateMessage

Sent by the server to every signed-in socket when a user starts or stops typing in the chat, with everyone typing. The chatbox shows their avatars.

I
CommentCreatedMessage

Sent by the server when a comment is posted, to every socket if its dump is public, else to the dump's poster's. WSProvider passes it on to the dump's page and the dump lists as a comment event, as it does the two messages below.

I
CommentDeletedMessage

Sent by the server when a comment is deleted, as when one is posted.

I
CommentLikeAckMessage

Sent by the server to the socket that liked or unliked a comment, as VoteAckMessage is for a vote.

I
CommentLikeCastMessage

Sent by WSProvider when the user likes a comment, and handled as VoteCastMessage is, with CommentLikeAckMessage for the ack.

I
CommentLikeRemoveMessage

Sent by WSProvider to withdraw a like, as CommentLikeCastMessage.

I
CommentLikesUpdateMessage

Sent by the server after a like, as VotesUpdateMessage is after a vote: to the dump's poster alone when the comment is on a private dump.

I
CommentUpdatedMessage

Sent by the server when a comment is edited, as when one is posted.

I
DumpCreatedMessage

Sent by the server when a dump is posted, to every socket if it is public and to its poster's alone if it is private. It also goes to everyone but the poster when a private dump goes public, since to them it is new. WSProvider puts it at the top of its recent dumps.

I
DumpDeletedMessage

Sent by the server when a dump is deleted, to every socket if it was public and to its poster's alone if it was private. It also goes to everyone but the poster when a public dump goes private. WSProvider drops it from every list.

I
DumpRelatedChangedMessage

Sent by the server when the dumps related to some dumps changed: a note or a comment gained or lost a link to a dump, or a dump that has such links turned public. It goes to the sockets whose user may see both ends of a link. The Dump page showing one of dumpIds fetches its related dumps again.

I
DumpUpdatedMessage

Sent by the server when a dump changes, to every socket if it is public and to its poster's alone if it is private, as when it has just gone private. WSProvider refreshes it in place, without moving it up the feeds.

I
ErrorMessage

Sent by the server to the socket whose message failed. With dumpId or commentId, WSProvider undoes that optimistic vote or like at once rather than after its 5 s timeout. With scope, the chatbox shows it, on the pending message nonce names or as a notice. Any other error is ignored.

I
FollowsChangedMessage

Sent by the server to every socket of a user who followed or unfollowed a user or a playlist, so that FollowProvider in their other tabs refetches the follow status.

I
NotificationCreatedMessage

Sent by the notification service to every socket of the notified user. WSProvider bumps the unread count and chimes.

I
NotificationsReadMessage

Sent by the notification service to every socket of a user who opened their notifications, which marks them all read. WSProvider zeroes the bell's count, in the other tabs and devices too.

I
OnlineUser

Sent by the server in WelcomeMessage, PresenceUpdateMessage and ChatTypingUpdateMessage, once per signed-in user whatever their number of sockets.

I
PingMessage

Sent by the server to every socket every 30 s. WSProvider answers PongMessage, and a socket that missed the previous ping is closed. The server also answers a client's ping, though the client never sends one.

I
PlaylistCreatedMessage

Sent by the server when a playlist is created, to every socket if it is public, else to its owner's. It also goes to everyone but the owner when a private playlist goes public, since to them it is new. WSProvider passes it on to the playlist pages as a playlist event, as it does the three messages below.

I
PlaylistDeletedMessage

Sent by the server when a playlist is deleted, to those who could see it. It also goes to everyone but the owner when a public playlist goes private.

I
PlaylistDumpsUpdatedMessage

Sent by the server when dumps are added to, removed from or reordered in a playlist, to those who can see it, and when one of its dumps is deleted or changes visibility, which changes what its viewers see of it.

I
PlaylistUpdatedMessage

Sent by the server when a playlist changes, to every socket if it is public, else to its owner's, as when it has just gone private. Its dumpCount and lastDumpAt are the socket's user's, as in playlist_created.

I
PongMessage

Sent in answer to a PingMessage. The server records the client's to keep its socket open, and the client ignores the server's.

I
PresenceUpdateMessage

Sent by the server to every socket when a socket opens or closes, and when a connected user's avatar changes.

I
UserUpdatedMessage

Sent by the server to every socket after a profile edit, a rename, a role change or an avatar upload. WSProvider refreshes the user's avatar wherever it shows, and passes it on as a user event, which updates their profile and their name on the lists.

I
VoteAckMessage

Sent by the server to the socket that sent a VoteCastMessage or VoteRemoveMessage, once the vote is stored. WSProvider confirms its optimistic vote with it.

I
VoteCastMessage

Sent by WSProvider when the user upvotes a dump, after it has shown the vote. The server answers VoteAckMessage, or an ErrorMessage naming the dump, on which the client undoes the vote, as it does when neither comes within 5 s.

I
VoteRemoveMessage

Sent by WSProvider to withdraw an upvote, as VoteCastMessage.

I
VotesUpdateMessage

Sent by the server after a vote, to every socket, or to the poster's alone for a private dump. WSProvider updates the count, and when voterId is the user, it updates their votes too, so their other tabs follow.

I
WelcomeMessage

Sent by the server to a socket once it opens, just after the PresenceUpdateMessage that announces it. WSProvider replaces its state with this one, dropping its pending votes and likes. After a reconnect, it also bumps connectionEpoch, so that pages refetch what they missed. The per-user fields are empty or zero on an anonymous socket.

Type Aliases

shared/paths.ts

Builds and parses the site's page paths, by the one rule they all follow: the path is a hierarchy, @name is a user (@name@host one whose account lives on another instance), and any other word under the root is a dump category or an app page, /playlists holding the playlist categories. Under a user each segment is a fixed tab name, and under dumps or playlists a slug. A view of a page is one more segment, and a default view is the bare path. A category narrowing a list on another page, a profile's or a search's, is a query, ?category=, not a segment, and so is the order of a list of playlists, ?sort=, the default order being the bare path, and the playlists page's list of the viewer's own, ?mine=1.

Functions

f
categoryPath(
kind: CategoryKind,
slug: string
): string

Returns the path of the page of the category of kind at slug: a dump category's feed (feedPath), or a playlist category's playlists (playlistsPath). The category chips link to it.

f
communityPath(view?: DigestCadence): string

Returns the path of the community page on view, the week by default.

f
dumpPath(
dump: OwnedItem,
view?: "edit"
): string

Returns the path of a dump's page, /@owner/dumps/<slug>, or of its editor, one segment more.

f
feedPath(
categorySlug?: string,
tab?: FeedTab,
view?: FollowView
): string

Returns the path of a feed: every dump's at the root, or a category's at /<slug>, on tab, or on the viewer's default tab without one. The follow tab takes a view, the users followed by default.

f
parsePagePath(pathname: string): PagePath | null

Returns what a page path under a user names: a profile (with its tab), a dump or a playlist (or its editor). It returns null for any other path, a feed or an app page, and for one under a user that no page answers. The names are decoded but not looked up, so the user or the item may not exist.

f
parsePlaylistsTab(params: URLSearchParams): PlaylistsTab

Returns the tab of the playlists page its query names: the viewer's own playlists with a mine, else the order sort names, as parsePlaylistSort reads it.

f
playlistPath(
playlist: OwnedItem,
view?: "edit"
): string

Returns the path of a playlist's page, /@owner/playlists/<slug>, or of the page with its editor open, one segment more.

f
playlistSortQuery(
sort?: PlaylistSort,
extra?: URLSearchParams
): string

Returns the query string that puts a list of playlists in the order sort names, with its ?, or "" for the default order. extra holds the list's other parameters, such as a profile's category.

f
playlistsPath(
categorySlug?: string,
tab?: PlaylistsTab
): string

Returns the path of the page of every public playlist, /playlists, or of a playlist category's at /playlists/<slug>, on the tab tab names: an order, the default one without it, or the viewer's own playlists.

f
searchPath(
tab?: SearchTab,
query?: string
): string

Returns the path of the search page on tab, searching query when it is given.

f
userPath(
username: string,
tab?: ProfileTab,
view?: FollowView
): string

Returns the path of username's profile, of one of its tabs, or of a view of the follow tab. The default view, users, is the bare tab.

Interfaces

I
OwnedItem

Passed to dumpPath and playlistPath, usually as the item.

Type Aliases

Variables

v
COMMUNITY_VIEWS: readonly DigestCadence[]

The views of the community page, one per digest cadence: the week at /community, the month at /community/monthly.

v
FEED_TABS: "hot" | "new" | "journal" | "follow"[]

The feed tabs, in the tab bar's order. Each is a path segment: /hot for every dump, /music/hot for a category. They are reserved at the root, so no category can take one.

v
FOLLOW_VIEWS: "users" | "playlists"[]

The views of a follow tab, of what is followed: the users, at the bare tab, or the playlists. A profile's lists them, /@name/follow and /@name/follow/playlists, and a feed's shows their dumps, /follow and /follow/playlists.

v
PLAYLISTS_TABS: any[]

The tabs of the playlists page, in the tab bar's order: each order of every public playlist, ?sort=, then the viewer's own playlists, ?mine=1, which only a signed-in user is offered.

v
PROFILE_TABS:
"dumps"
| "upvoted"
| "playlists"
| "follow"
| "invitees"
| "manage"
[]

The tabs of a profile, each a segment after /@name. The bare /@name is the overview.

v
SEARCH_TABS: "dumps" | "users" | "playlists"[]

The tabs of the search page: the dumps at /search, the others at /search/<tab>.

shared/player.ts

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.

Interfaces

I
Tralbum

Returned by GET /api/remote/bandcamp for a Bandcamp album or track page, when the native player plays it.

I
TralbumTrack

Listed by a Tralbum, in the release's order.

I
YouTubePlaylist

Returned by GET /api/remote/youtube to the player, which shows the titles and lengths of a playlist embed's videos, since the embed exposes neither.

I
YouTubePlaylistTrack

Listed by a YouTubePlaylist, in the playlist's order.

shared/follow.ts

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.

Interfaces

I
FollowStatus

Returned by GET /api/me/follow to FollowProvider, which then keeps it up to date itself as the user follows and unfollows.

shared/playlist.ts

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 /@owner/playlists/:slug, on profiles and in search results.

Functions

f
comparePlaylists(sort: PlaylistSort): (
a: Playlist,
b: Playlist
) => number

Returns the comparison that puts playlists in the order sort names, as the API pages them, the newer first on a tie. usePlaylistListSync finds with it where a playlist the socket announces goes in a list.

f
parsePlaylistSort(value: string | null): PlaylistSort

Returns the order a ?sort= names, the default one when it is missing or names none: like a page number, an order never fails a list.

f
playlistActivity(playlist: Pick<Playlist, "createdAt" | "lastDumpAt">): Date

Returns when playlist last took a dump, or was created if it holds none: what the updated order sorts by and the cards show. Its title, cover and description may change after without bringing it back up a list.

Interfaces

I
CreatePlaylistRequest

Passed to POST /api/playlists by PlaylistCreateForm.

I
Playlist

Returned by the playlist routes, and by the search, users and dumps routes that list playlists. The playlist_created and playlist_updated WebSocket messages carry it too.

  • categoryIds: string[]

    The ids of the playlist categories it is filed under. The services fill them in on every playlist they return.

  • createdAt: Date
  • description: string

    It is written in Markdown.

  • dumpCount: number

    It counts the dumps the viewer may see in it, as its page lists them. It is undefined when the query didn't count them.

  • id: string
  • imageMime: string

    It is undefined when the playlist has no cover.

  • isPublic: boolean

    Only its owner sees a private playlist. Moderators don't, though they may still edit or delete it.

  • lastDumpAt: Date

    When the latest of the dumps the viewer may see was added to it, which playlistActivity reads. It is undefined when it holds none, or when the query didn't count them.

  • ownerUsername: string

    The owner's current name, which the playlist's path starts with.

  • slug: string

    Names the playlist in its page's path, playlistPath (shared/paths.ts) building it with ownerUsername. Made from the title on creation, it changes only when edited, and is unique among its owner's playlists.

  • title: string
  • updatedAt: Date

    playlistImageUrl puts it on the cover's URL, so a new cover isn't hidden by the cached old one.

  • userId: string
I
PlaylistMembership

Returned by GET /api/me/playlists?contains=:dumpId, one per playlist of the caller's. PlaylistMembershipPanel ticks the playlists with hasDump, in the add-to-playlist modal and the dump form.

I
PlaylistWithDumps

Returned by GET /api/users/:key/playlists/:slug to PlaylistDetail, and by GET /api/playlists/:playlistId, with the dumps the caller may see, in the playlist's order.

I
ReorderPlaylistRequest

Passed to PUT /api/playlists/:playlistId/order by PlaylistDetail, after a drag or to put back a dump whose removal was undone.

  • dumpIds: string[]

    Every dump of the playlist the caller can see, in the new order. The dumps hidden from them keep their slots.

I
UpdatePlaylistRequest

Passed to PUT /api/playlists/:playlistId by PlaylistDetail. A field left out keeps its value.

Type Aliases

T
PlaylistSort = (PLAYLIST_SORTS)[number]

One of the PLAYLIST_SORTS.

Variables

v
PLAYLIST_SORTS: "updated" | "new" | "dumps"[]

The orders a list of playlists comes in, the first being the default one: updated, by playlistActivity, the latest first; new, by creation, the newest first; dumps, the fullest first. GET /api/playlists and GET /api/users/:key/playlists take one as ?sort=, the /playlists page and a profile's playlists tab show them as tabs and keep the one picked in their URL's ?sort=.

shared/comment.ts

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.

Interfaces

I
Comment

Returned by the comment routes, and sent in the comment_created and comment_updated WebSocket messages. It carries its author's name and avatar, so the thread needn't look them up.

I
CreateCommentRequest

Passed to POST /api/dumps/:dumpId/comments by CommentThread, and by CommentNode for a reply. The body must not be blank.

I
UpdateCommentRequest

Passed to PATCH /api/comments/:commentId by CommentNode.

shared/api.ts

Lets the client tell success from failure the same way for every JSON endpoint, through the envelope they all answer with. It also defines Wire, the form a type takes once it has crossed JSON. The routes build the envelope, and the frontend's API helpers unwrap it.

Interfaces

I
APIError

Sent in an APIFailure. The frontend shows message to the user, so it is written for them.

I
APIFailure

Sent by the error middleware when a request fails, and by the few routes that answer an error themselves.

I
APISuccess

Sent by a route that succeeded, around its payload.

I
PaginatedData

Returned by the list endpoints, as the data of an APISuccess.

Type Aliases

T
APIResponse<T> = APISuccess<T> | APIFailure

Returned by every JSON endpoint. A reader tells the two apart by success.

T
Wire<T> = T extends Date ? string : T extends readonly (infer U)[] ? Wire<U>[] : T extends object ? [K in keyof T]: Wire<T[K]> : T

T as the client reads it from JSON, over HTTP, the socket or localStorage, where every Date has become an ISO string.

Variables

v
T
APIErrorCode: { BAD_REQUEST: string; UNAUTHORIZED: string; FORBIDDEN: string; NOT_FOUND: string; CONFLICT: string; RATE_LIMITED: string; SERVER_ERROR: string; BAD_GATEWAY: string; }

Lists the codes an APIError carries, one per HTTP status. The backend's APIException derives its code from the status it is given, and the frontend shows the message without reading the code.

shared/notification.ts

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.

Interfaces

I
I
DumpRef

Where a notification's dump is now, for its link (dumpPath). The server joins it each time it sends the notification, so a renamed dump or poster still links right; it is absent once the dump is deleted.

I
DumpUpvotedData

Carried by a dump_upvoted notification, sent to the dump's poster.

I
Notification

Pushed to its user's sockets in a notification_created message when it is made, and returned by GET /api/me/notifications.

I
PlaylistDumpAddedData

Carried by a playlist_dump_added notification, sent to the followers of a public playlist when a public dump is added to it.

I
PlaylistFollowedData

Carried by a playlist_followed notification, sent to a playlist's owner.

I
PlaylistRef

Where a notification's playlist is now, joined as DumpRef is.

I
UserDumpPostedData

Carried by a user_dump_posted notification, sent to the followers of a user who posted a public dump.

I
UserFollowedData

Carried by a user_followed notification, sent to the user followed.

I
UserMentionedData

Carried by a user_mentioned notification, sent to a user @mentioned in a comment, a dump, a playlist's description or a chat message, or to every user by a chat message's @everyone. Its DumpRef is the dump's, or the comment's dump's; its PlaylistRef the playlist's.

Type Aliases

T
NotificationData =
PlaylistFollowedData
| UserFollowedData
| UserDumpPostedData
| PlaylistDumpAddedData
| DumpUpvotedData
| UserMentionedData
| DumpCommentedData
| CommentLikedData

Stored when the notification is made, so a title changed later still shows the old one. The …Username fields and the refs are joined when it is sent, so they follow a rename. The notification's type says which member it is.

T
NotificationType =
"playlist_followed"
| "user_followed"
| "user_dump_posted"
| "playlist_dump_added"
| "dump_upvoted"
| "user_mentioned"
| "dump_commented"
| "comment_liked"

Says which of the …Data interfaces a notification's data is. Each value has one notify… trigger in notification-service.ts.

shared/image.ts

Names the widths the site's own images come in. Every image the API serves (avatars, covers, thumbnails, attachments, proxied images) takes ?w= and answers a copy scaled down to it, so a card or an avatar doesn't load the upload at full size. The frontend builds those URLs from this list (imageSrcSet in src/utils/shared/urls.ts) and the server makes no other width (serveImage in api/lib/upload.ts), which bounds what it keeps.

Functions

f
parseImageWidth(raw: string | null): ImageWidth | null

Returns the width a ?w= value asks for, snapped to IMAGE_WIDTHS, or null when it is absent or not a positive number, which asks for the image as it is.

f
snapImageWidth(width: number): ImageWidth

Returns the narrowest of IMAGE_WIDTHS that covers width pixels, or the widest when none does.

Type Aliases

Variables

v
IMAGE_WIDTHS: 128 | 320 | 640 | 960[]

The widths, in pixels, smallest first: an avatar on a dense screen, a small cover, a card on a phone, and the widest feed card on a dense screen.

v
MAX_IMAGE_WIDTH: ImageWidth

The largest of IMAGE_WIDTHS, which a thumbnail never exceeds.

src/pages/Settings.tsx

Renders the signed-in user's settings page, /settings, where they change their password, their account's settings and this browser's.

Functions

f
Settings(): React.JSX.Element

Renders /settings, signed in only: the username, which only a moderator changes (ProfileManageTab), the password change, which opens ChangePasswordModal, the other instances the account signs in to (GrantList) unless it may sign in to none, and the settings, each saved as soon as it is clicked. An account from another instance changes its password there. The style, color scheme and digests are saved to the account; the default feed tab and sounds, for now, to this browser.

src/pages/Community.tsx

Renders the community page, /community for the last week and /community/monthly for the last month, for a signed-in user: the site's figures, its most upvoted dumps, who joined and the new public playlists. The server recomputes them on a timer (community-service.ts), and the digests send each reader the same sections, in the same words.

Functions

f
Community(): React.JSX.Element

Renders /community and /community/:view, from GET /api/community. A view no period names is NotFound.

src/pages/Playlists.tsx

Renders every public playlist, /playlists, and those of one playlist category, /playlists/:categorySlug, in the order ?sort= names, or with ?mine=1 the signed-in user's own. The header's link and the avatar menu's lead here.

Functions

f
Playlists(): React.JSX.Element

Renders the list for the route's category, or every public playlist without one, on the tab the URL's query names. Each category's list on each tab is another, which the page mounts afresh. A slug that names no playlist category, once the categories have loaded, is a page not found.

src/pages/Index.tsx

Renders the home page, which shows every dump, or one category's, in the "hot", "new", "journal" and "follow" tabs.

Functions

f
Index(unnamed 0: IndexProps): React.JSX.Element

Renders /, /:feedTab, /:categorySlug and /:categorySlug/:feedTab, for anyone, the "follow" tab, and its /follow/playlists, for a signed-in user only. The feed comes from GET /api/dumps, or GET /api/dumps?category=, a page at a time, and the "hot", "new" and "journal" tabs share it. WSContext adds the dumps posted since, or brought into the feed by an edit, by date, the live vote counts, and who's online. The router keys the page by category, so switching tabs keeps the feed and switching categories refetches it. It also opens the dump creation modal on a link shared from Android (?share_url=).

Interfaces

I
IndexProps

Passed to Index by the router, from the path.

src/pages/ResetPassword.tsx

Renders the reset of a forgotten password, both its steps on one page: the request for a link by email, which "Forgot password?" on the login page leads to, and the new password, which the link leads to.

Functions

f
ResetPassword(): React.JSX.Element

Renders /reset-password, for anyone: the request for a reset link, or, with the link's ?token=, the new password. It doesn't sign the user in: a success links to /login.

src/pages/Unsubscribe.tsx

Renders the page a digest's unsubscribe link opens, where the reader turns that digest off with one click, signed in or not.

Functions

f
Unsubscribe(): React.JSX.Element

Renders /unsubscribe?token=, for anyone holding a digest's link. It asks before turning the digest off, since mail scanners open every link in an email, then posts the token to POST /api/unsubscribe.

src/pages/UserRegister.tsx

Renders the sign-up page, which only works from an invite link. Members copy those links from their own profile page.

Functions

f
UserRegister(): React.JSX.Element

Renders /register?token=, for the signed out. It checks the invite before showing the form, and signs the new user in on success. A user of another instance joins with that account instead, through FederatedSignIn, unless the instance takes none.

src/pages/PlaylistDetail.tsx

Renders a playlist's page, where its owner and moderators also edit it and reorder its dumps.

Functions

f
PlaylistDetail(unnamed 0: PlaylistDetailProps): React.JSX.Element

Renders /@:username/playlists/:slug, for anyone who may see the playlist: a private one only to its owner. usePlaylist loads it, by a former name or slug too, and keeps it live. Its owner, or a user with playlist:moderate, can drag its dumps into a new order and remove one, with a short window to undo, edit its title and description in place, and edit the rest or delete it in the editor that opens over its header at /@:username/playlists/:slug/edit. Both paths render the page at the same place, so opening the editor keeps the playlist loaded. Anyone else there gets the page, or is asked to sign in first.

Interfaces

I
PlaylistDetailProps

Passed to PlaylistDetail by App's route.

src/pages/RestrictedLoggedIn.tsx

Guards App's routes that need a session: the dump editor, /notifications, /community, /settings and /manage. Index sends a guest on the "follow" tab to sign in the same way.

Functions

f
RestrictedLoggedIn(unnamed 0: RestrictedLoggedInProps): React.JSX.Element

Renders children when someone is signed in, and otherwise replaces the page with /login, which comes back to it once signed in: a digest's link to /notifications opened signed out lands there after all.

Interfaces

I
RestrictedLoggedInProps

Passed to RestrictedLoggedIn by App, around a route's page.

src/pages/RestrictedGuest.tsx

Guards App's routes that only make sense signed out, /login and /register.

Functions

f
RestrictedGuest(unnamed 0: RestrictedGuestProps): React.JSX.Element

Renders children when no one is signed in, and redirects otherwise: to the page a ?next= names (nextPath), or to /.

Interfaces

I
RestrictedGuestProps

Passed to RestrictedGuest by App, around a route's page.

src/pages/Notifications.tsx

Renders the notifications page, /notifications, for a signed-in user. It reads them from GET /api/me/notifications and adds the ones WSContext receives while it is open.

Functions

f
Notifications(): React.JSX.Element

Renders /notifications, for a signed-in user, a page at a time as the list scrolls. Opening the page marks every notification read on the server, and so does each that arrives while it is open. The unread ones stay highlighted until the page is left. Going back restores the list and the scroll, with what came in meanwhile on top.

src/pages/UserPublicProfile.tsx

Renders a user's profile page, which is also where the signed-in user edits their own profile.

Functions

f
UserPublicProfile(): React.JSX.Element

Renders /@:username and its tabs, /@:username/:tab, and the follow tab's view /@:username/follow/playlists, for anyone. useProfile loads the user, by a former name or another case too, and the URL then takes the current one; each tab loads its own lists, by the user's id, when shown. On their own profile, users edit their avatar, description and email, and create invites. The "manage" tab, another user's role and name, is for those who hold user:manage. A tab or view the profile hasn't is NotFound.

src/pages/Search.tsx

Renders the results of a search, in a tab for dumps, one for users and one for playlists, the dumps and the playlists narrowed to a category of their own.

Functions

src/pages/Authorize.tsx

Renders the page where another gerbeur instance asks to sign the user in with their account here, /authorize. That instance's login page sends them, with its request in the query, and this page sends them back to it.

Functions

f
Authorize(): React.JSX.Element

Renders /authorize?client_id=&redirect_uri=&state=&code_challenge=&nonce=. A signed-out visitor signs in first and comes back. The page names the instance asking (GET /api/oauth/clients) and what it will receive; once the user continues, or at once if they already did before, it gets a code (POST /api/oauth/codes) and goes back to the instance with it. An account that itself comes from another instance can't be used from here.

src/pages/FederationCallback.tsx

Renders the page another gerbeur instance sends a user back to once they let it sign them in here, /federation/callback.

Functions

f
FederationCallback(): React.JSX.Element

Renders /federation/callback?code=&state=. It finishes the sign-in (POST /api/federation/sessions) if this tab started it (startedHere), and once: a code works only once, and a second request would fail the sign-in. On success the user lands on /, signed in.

src/pages/DumpEdit.tsx

Renders the page where a dump's poster, or a moderator, edits or deletes it.

Functions

f
DumpEdit(): React.JSX.Element

Renders /@:username/dumps/:slug/edit, for the dump's poster and moderators; anyone else signed in gets an error rather than a form the API would refuse to save. The dump comes from GET /api/users/@:username/dumps/:slug, which answers a former slug too, and the URL then takes the current one. The thumbnail, the preview refresh and the deletion apply at once, and the form's other fields on "Save", which goes back to the dump's page, at its new path if the slug changed. A deletion goes to the index.

src/pages/UserLogin.tsx

Renders the sign-in page.

Functions

f
UserLogin(): React.JSX.Element

Renders /login, for the signed out. A sign-in lands on the page ?next= names, or /, and "Forgot password?" leads to /reset-password, which asks for a reset link. Users of another instance sign in through FederatedSignIn, unless the instance takes none.

src/pages/Dump.tsx

Renders a dump's page, with its preview, comments, related dumps and the playlists holding it.

Interfaces

f
Dump(): React.JSX.Element

Renders /@:username/dumps/:slug, for anyone who may see the dump: a private dump answers 404 to anyone but its poster. The dump comes from GET /api/users/@:username/dumps/:slug, or from the navigation's state when DumpEdit or a title edit here just saved it. Reached by a former name or slug, the URL takes the current ones. Its comments, related dumps and playlists come from their own endpoints once it is loaded, and the socket's events keep them current; a reconnection fetches them and the dump again. A dump deleted, or made private by its poster, while someone else views it gives way to a notice. The poster and moderators can edit the title and the note in place, and get a link to DumpEdit for the rest.

I
Dump

Returned by every endpoint that answers dumps, and pushed to clients by the dump_created and dump_updated socket messages. A URL dump has url and richContent, a file dump the file… fields. A private dump reaches its poster only.

src/pages/NotFound.tsx

Renders the 404 page, for any path App's route table doesn't match and for a category slug Index doesn't know.

Functions

f
NotFound(): React.JSX.Element

Renders the 404 page, with a link home. Anyone may see it.

src/pages/Manage.tsx

Renders the site's tools, /manage, for those who hold category:manage or federation:manage. Another user's role and name are managed on their profile's "manage" tab.

Functions

f
Manage(): React.JSX.Element

Renders the managers of the dump and the playlist categories, and the instances the site trusts or blocks, each to those who hold its permission, which the API checks again, the instances only while federated sign-in goes at least one way; without either, the page isn't found.

src/hooks/playlist/usePlaylistListSync.ts

Keeps a page's list of playlists in step with the socket's playlist_* messages, and their owners' names. The user's profile, the playlists page and the search call it.

Functions

f
usePlaylistListSync(
items: Playlist[],
setPlaylists: (fn: (prev: Playlist[]) => Playlist[]) => void,
options?: PlaylistListSyncOptions
): void

Applies the socket's playlist events to items, the loaded list, which setPlaylists updates. A created playlist goes where the list's order puts it, an updated one changes in place or leaves when it turns private (unless isOwner), one that left comes back where it was when it returns, a deleted one leaves, and a playlist's dumpCount and lastDumpAt follow its dumps. One that a change brings up the order moves up to its place, as a playlist does in the updated order when it takes a dump; one that falls stays where it is, its place being perhaps on a page not loaded. A renamed user's playlists take their new name.

Interfaces

I
PlaylistListSyncOptions

Passed to usePlaylistListSync by each page, to fit its list.

  • isOwner: boolean

    Keeps private playlists in the list, for their owner.

  • ownerId: string

    Adds only this user's playlists, when one is created or comes back into view. Without it, anyone's are added.

  • sort: PlaylistSort

    The order the list is in, for one the API sorts: the playlists page's and a profile's. A list in an order of its own leaves it out, the followed playlists being by the date they were followed and a search's results the search's: it takes no new playlist, since the socket says neither what the user follows nor what a search matches, and moves none. Playlists already listed still change, go, and come back where they were.

src/hooks/playlist/usePlaylistMemberships.ts

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.

Functions

f
usePlaylistMemberships(dumpId: string | null): UsePlaylistMembershipsResult

Returns the user's playlists with whether each holds dumpId, from GET /api/me/playlists?contains=:dumpId. A null dumpId, as before DumpCreateModal has posted its dump, loads nothing.

Interfaces

I
UsePlaylistMembershipsResult

Returned by usePlaylistMemberships to AddToPlaylistModal and DumpCreateModal, for their PlaylistMembershipPanel.

src/hooks/playlist/usePlaylist.ts

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.

Functions

f
usePlaylist(
username: string | undefined,
slug: string | undefined,
view?: "edit"
): UsePlaylistResult

Returns the playlist at /@username/playlists/slug from GET /api/users/@:username/playlists/:slug, with the dumps the viewer may see in it. The socket's events keep its details and dumps live: a dump another client removes, or that turns private, fades out, one it adds, or that turns public, is fetched, a new order is applied, and a playlist deleted, or turned private by someone else, sends the page home. A reconnection loads it again. A new slug replaces the URL, keeping the page's view, and a path that names the playlist on screen, as that one does, fetches nothing.

Interfaces

I
UsePlaylistResult

Returned by usePlaylist to PlaylistDetail.

src/hooks/feed/usePagedList.ts

Gives every paged list of the app the same loading. Every list the API pages goes through useLoadMore for its next pages, and a list that loads on its own through usePagedList, from its first page on.

Functions

f
useLoadMore<T>(
list: PagedList<T> | null,
setList: (fn: (list: PagedList<T>) => PagedList<T>) => void,
fetchPage: FetchPage<T>
): () => void

Returns the call that loads the page after list's last one and appends it through setList. It does nothing unless canLoadMore(list); a page that fails leaves the list as it was, for the next call to try again. A page that loads its list along with more, as the search page does, uses it directly, and usePagedList for the others.

f
usePagedList<T extends { id: string; }>(
key: string | undefined,
fetchPage: FetchPage<T>,
cached?: Omit<FeedCacheEntry<T>, "savedAt"> | null
): UsePagedListResult<T>

Returns the list fetchPage serves for key, from its first page, and the call that loads the next. key changes as useFetchState's does, and cached, useFeedCache's restored list for it, if any, is shown without loading. The index's feeds, the profile's lists, the notifications and the popover of voters and likers use it.

Interfaces

I
UsePagedListResult

Returned by usePagedList.

Type Aliases

T
FetchPage<T> = (
page: number,
signal?: AbortSignal
) => Promise<PaginatedData<T>>

Passed to useLoadMore and usePagedList by a list's page or hook. It resolves to page page of the list, its items deserialized, as fetch with pageUrl and readPage gives it, and rejects when the page can't be had.

src/hooks/feed/useScrollSave.ts

Saves a feed page's state, with its scroll position, as the page is left, and restores the position when the page is back.

Functions

f
useScrollRestore(
scrollY: number | null | undefined,
loaded: boolean
): void

Scrolls the window to scrollY once loaded, and never again, before paint so that the restored page doesn't flash at the top. Feed pages pass useFeedCache's cached?.scrollY, which is undefined when nothing was restored.

f
useScrollSave(
enabled: boolean,
onSave: (scrollY: number) => void
): void

Calls onSave(scrollY) when the component unmounts or the tab is hidden (pagehide), while enabled. Feed pages pass enabled once their feed is loaded, and an onSave that calls useFeedCache's saveState.

src/hooks/feed/useInfiniteScroll.ts

Loads a feed's next page when the end of the feed scrolls into view.

Functions

f
useInfiniteScroll(
onLoadMore: () => void,
enabled: boolean
): (node: HTMLDivElement | null) => void

Returns a callback ref for the sentinel element placed after a feed's last item, and calls onLoadMore when the sentinel comes within 200px of the view while enabled. Feed pages pass canLoadMore of their list.

src/hooks/feed/useDefaultFeedTab.ts

Lets a user choose which feed tab the index opens on, remembered in this browser.

Functions

f
useDefaultFeedTab(): [FeedTab, (tab: FeedTab) => void]

Returns the feed tab the index opens on, and a setter that stores a new choice. The settings page sets it; the feed reads it through useViewerDefaultFeedTab. A new choice reaches every reader at once, in this browser tab and the others.

f
useViewerDefaultFeedTab(): FeedTab

Returns the tab the bare / and /<category> show this viewer: their choice, or DEFAULT_FEED_TAB for "follow" when nobody is signed in, since it needs a session. Index and its tab bar read it.

src/hooks/feed/useFeedCache.ts

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.

Functions

f
useFeedCache<T>(
key: string,
hydrateItem: (raw: T) => T
): UseFeedCacheResult<T>

Returns the feed saved under key and a way to save it. The feed pages and useUserDumpFeed use it. key must tell apart every list whose items differ; the viewer is added to it here, since they see their own private items. hydrateItem restores what JSON lost, such as dates. A full or blocked storage is ignored.

Interfaces

I
FeedCacheEntry

Stored as JSON by useFeedCache's saveState, and read back by the hook when the page is next reached by going back.

I
UseFeedCacheResult

Returned by useFeedCache to a feed page.

Type Aliases

T
SavedList<T> = Pick<PagedList<T>, "items" | "page" | "hasMore">

Passed to useFeedCache's saveState by the feed pages, from the list they hold, without its loading flag.

src/hooks/feed/usePositionMemory.ts

Lets a live list put an item that left it, as by turning private, back at its old position when it returns.

Functions

f
insertAt<T extends { id: string; }>(
list: T[],
item: T,
idx: number
): T[]

Returns list with item at idx, or at the end when the list has since shrunk below it, unless the list holds it already.

f
usePositionMemory<T extends { id: string; }>(items: T[]): (id: string) => number | undefined

Remembers the index of every item that leaves items, whatever took it out, for useDumpListSync and usePlaylistListSync, which own the lists' live changes. items is the loaded list, or [] before it loads. The returned call gives an item's old index, once, and forgets it; it is undefined for an item that never left.

src/hooks/feed/useFading.ts

Lets a removal made by mistake be undone before the item leaves the list, by timing the fade-out of items leaving a list.

Functions

f
fadeClassName(phase: FadeState | undefined): string | undefined

Returns the class a fading card takes in phase, for DumpCard's className, or undefined for a card that isn't fading.

f
useFading(onFaded?: (id: string) => void): UseFadingResult

Returns the fading state of a list's items and the calls that drive it. onFaded is called with an item whose fade ran to its end, for the list to drop it; a cancelled fade doesn't call it. The timers are cleared on unmount.

Interfaces

I
UseFadingResult

Returned by useFading to useUpvotedList and usePlaylist. The list keeps showing an item while it is in fading.

Type Aliases

T
FadeState = "cooldown" | "dismissing"

Held by useFading for each fading item, and mapped to a CSS class by fadeClassName. cooldown lasts 2 seconds, and dismissing lasts the 350ms of the CSS collapse.

src/hooks/theme/useTheme.ts

Gives components the theme that ThemeProvider holds.

Functions

f
useTheme(): ThemeContextValue

Returns the style and color scheme, and their setters. The profile page's settings are the only place that changes them.

src/hooks/theme/useThemeChoice.ts

Holds one half of the theme, the style or the color scheme, for ThemeProvider, which uses it once for each. Both halves work alike: the account's choice wins, then this browser's, then a fallback; the result is applied to the root element and mirrored to localStorage, where index.html's first-paint script reads it.

Functions

f
useThemeChoice<T extends string>(spec: ThemeChoiceSpec<T>): UseThemeChoiceResult<T>

Returns the half of the theme spec describes. An unknown stored value, such as "default", which "smooth" was once stored as, reads as no choice.

Interfaces

I
ThemeChoiceSpec

Passed to useThemeChoice by ThemeProvider, as a constant: one for the style, one for the scheme.

I
UseThemeChoiceResult

Returned by useThemeChoice to ThemeProvider.

src/hooks/ws/useWS.ts

Gives components the server's live updates. Each hook reads one of the contexts WSProvider fills, so that a component re-renders only with the part it uses. Outside WSProvider, they return inert defaults.

Functions

f
useOnReconnect(callback: () => void): void

Calls callback after each reconnection of the socket, never on the first connection, for the component to fetch again what it may have missed while the socket was down. The live lists and pages use it.

f
useWS(): WSContextValue

Returns the connection's status, the live votes, likes, new and deleted dumps and playlists, the unread count, and the functions that vote and like. A component using it re-renders on every change to any of them.

f
useWSChat(): WSChatContextValue

Returns the chat's messages, typing and read state. A component using it re-renders with every message and typing change, so a component that needs only the unread count still pays for them.

f
useWSEvent<K extends keyof WSEvents>(
kind: K,
handler: (event: WSEvents[K]) => void
): void

Calls handler with every event of kind the socket brings, as it arrives, for as long as the component is mounted. The handler applies it to what the component fetched, through state setters: it runs outside render, and always as the latest render defined it. The list sync hooks, the dump and playlist pages, the profile, the bell and the categories use it.

f
useWSPresence(): WSPresenceContextValue

Returns who is online, for the presence row.

src/hooks/player/usePlayFile.ts

Plays an uploaded audio or video file in the global player. FilePreview and JournalCard call it, so a file plays and pauses the same from a list card, a journal tile and its dump's page.

Functions

f
fileArtworkUrl(
dump: Dump,
token?: string | null
): string | undefined

Returns the artwork of an uploaded file, for its thumbnail and the player: the uploaded thumbnail, or the server's still of a video or scaled-down copy of an image. A bare audio file has none, since the thumbnail endpoint has nothing to serve for it, and shows a placeholder instead.

f
isPlayableFile(dump: Dump): boolean

Checks whether dump is an uploaded file the global player can play.

f
usePlayFile(dump: Dump): UsePlayFileResult

Returns the playback state of the uploaded file dump and a function that plays or pauses it. The player's title links to the dump's page.

Interfaces

I
UsePlayFileResult

Returned by usePlayFile to the views that play an uploaded file.

  • active: boolean

    True while the file is the player's current item, playing or paused.

  • fileUrl: string

    The file's URL, with the viewer's media token, as the player loads it.

  • playing: boolean

    True while the file is playing.

  • toggle: () => void

    Plays the file from the start, or pauses and resumes it once it is the player's current item, so a second click doesn't restart it.

src/hooks/player/usePeaks.ts

Gives MediaPlayer the waveform it draws on its Scrubber.

Functions

f
usePeaks(source: PeaksSource | undefined): number[] | null

Returns the peaks of source once loaded, and null until then, without a source, or when it can't be loaded: the scrubber then draws a plain bar.

src/hooks/player/useYouTubePlaylist.ts

Gives GlobalPlayer the tracklist and transport of a YouTube playlist embed, whose list stays inside the iframe. The titles and lengths come from the server, since the embed exposes neither; the order, the position and the controls come from the IFrame Player API. Without the API (script blocked, embedding restricted) the rows are inert; without the server's list they have no titles.

Functions

f
useYouTubePlaylist(
embedUrl: string | null,
iframeRef: RefObject<HTMLIFrameElement | null>
): UseYouTubePlaylistResult

Returns the tracklist and transport of the playlist embedUrl plays in the iframe iframeRef, or no rows when it plays none. The iframe's URL must carry enablejsapi (see withJsApi) for the API to drive it.

Interfaces

I
PlaylistRow

A row of the tracklist GlobalPlayer shows. Its title and length come from the server's list, and are missing for a video that list lacks.

I
UseYouTubePlaylistResult

Returned by useYouTubePlaylist to GlobalPlayer, which shows the tracklist and its transport when there is more than one row.

src/hooks/player/usePlayRichContent.ts

Plays a link's preview in the global player, as the provider's embed or as a queue of Bandcamp streams. RichContentCard and JournalCard call it when their thumbnail is clicked.

Functions

f
canPlayRichContent(rc: PlayableRichContent): boolean

Checks whether rc can be played, for the cards to offer a play button. In native mode a Bandcamp page plays even without an embed, since its streams are resolved from the page itself.

f
usePlayRichContent(): UsePlayRichContentResult

Returns a function that plays a link's preview in its provider's embed. When the deployment sets GERBEUR_BANDCAMP_PLAYER=native, a Bandcamp page is resolved into its mp3 streams instead and played as a queue, which autoplays and plays an album through. If that fails (offline, a preorder, Bandcamp changing its markup), the embed plays, and a page without one (an artist's root, a /music index) plays nothing.

Interfaces

I
PlayableRichContent

Passed to playRichContent and canPlayRichContent by RichContentCard and JournalCard. It is a dump's RichContent, its thumbnail replaced by the dump's own when it has one.

I
UsePlayRichContentResult

Returned by usePlayRichContent to the cards that play a link.

src/hooks/form/useApiForm.ts

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.

Functions

f
useApiForm<T extends FieldValues>(options?: UseFormProps<T>): UseApiFormResult<T>

Returns a react-hook-form form made with options, plus submit, which turns a failed request into the form's root error.

Interfaces

I
UseApiFormResult

Returned by useApiForm, the form with submit added.

  • submit: (handler: SubmitHandler<T>) => (e?: React.BaseSyntheticEvent) => Promise<void>

    Wraps an async submit handler with the shared API-form lifecycle: clears the previous root error, runs the handler while RHF keeps formState.isSubmitting true, and on a thrown error surfaces a single root error via friendlyFetchError. Pair it with expectOk so API-level failures and network failures both become root errors through one code path. Returns the onSubmit handler to spread onto <form onSubmit={...}>.

src/hooks/editor/useEmojiTrigger.ts

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.

Functions

f
useEmojiTrigger(replace: (
start: number,
end: number,
text: string,
word: boolean
) => void
): UseEmojiTriggerResult

Returns the picker's state and the functions that drive it. A pick goes through replace, TextEditor's function that puts the text over [start, end) of the textarea's value, followed by a space when word is set.

Interfaces

I
UseEmojiTriggerResult

Returned by useEmojiTrigger to TextEditor.

Type Aliases

T
EmojiMode = "closed" | "inline" | "browse"

Returned by useEmojiTrigger to TextEditor, which keys its focus and key handling on it. An inline picker was opened by a typed :query: focus stays in the textarea and a pick replaces the query. A browse picker was opened from the button: focus goes to its search and a pick lands at the caret.

src/hooks/editor/useMentionAutocomplete.ts

Suggests users to @mention as the user types in TextEditor. It keeps the state of the suggestions, fetched from GET /api/users?q=, and TextEditor renders them in a MentionDropdown.

Functions

f
useMentionAutocomplete(
replace: (
start: number,
end: number,
text: string,
word: boolean
) => void
,
everyone?: boolean
): UseMentionAutocompleteResult

Returns the suggestions' state and the functions that drive them. A pick goes through replace, TextEditor's function that puts the text over [start, end) of the textarea's value, followed by a space when word is set. With everyone, which the chat's composer sets for a role with chat:everyone, @everyone is offered too.

Interfaces

I
UseMentionAutocompleteResult

Returned by useMentionAutocomplete to TextEditor.

Type Aliases

T
MentionKey = Pick<
KeyboardEvent,
"key"
| "ctrlKey"
| "metaKey"
| "shiftKey"
| "altKey"
| "preventDefault"
| "stopPropagation"
>

Passed to UseMentionAutocompleteResult.handleMentionKeyDown by TextEditor. It is the part of a React keydown event that the list reads.

src/hooks/dump/useUserDumpFeed.ts

Feeds a profile's lists of dumps, the user's own and those they upvoted, to its overview and to its "dumps" and "upvoted" tabs, and keeps them live.

Functions

f
useUserDumpFeed(
userId: string,
list: UserDumpList,
unnamed 2: UseUserDumpFeedOptions
): UseUserDumpFeedResult

Returns list of user userId, from GET /api/users/:userId/<list>, fetched signed in so users see their own private dumps, with the call that loads its next page; with categorySlug, only the dumps in that category. The socket keeps it live: the user's new dumps come in, a dump that goes private and public again comes back at its place, one edited out of the category leaves, one edited into it comes in, and the upvoted list follows the user's votes, source holding dumps at hand that a vote of theirs puts first at once. A reconnection fetches the pages loaded again. The list and the scroll position are saved as the page is left, under a key the overview and the unfiltered tab share, and going back restores them.

Interfaces

I
UseUserDumpFeedOptions

Passed to useUserDumpFeed by the profile's overview and tabs.

I
UseUserDumpFeedResult

Returned by useUserDumpFeed.

Type Aliases

T
UserDumpList = "dumps" | "upvoted"

One of a profile's lists of dumps, a path segment of its API and page.

src/hooks/dump/useDumpListSync.ts

Keeps a page's list of dumps in step with the socket: the dumps deleted, edited and posted, their comment counts, and their posters' names.

Functions

f
useDumpListSync(
items: Dump[],
setDumps: (fn: (prev: Dump[]) => Dump[]) => void,
options?: DumpListSyncOptions
): void

Applies the socket's dump, comment and user events to items, the loaded list, which setDumps updates. A deleted dump is removed, an edited one is replaced in place, one gone private leaves unless isOwner, and a new or returning one is added as belongs and addNew say. A new or deleted comment moves its dump's commentCount, and a renamed user's dumps take their new name.

Interfaces

I
DumpListSyncOptions

Passed to useDumpListSync by the lists that need more than edits and deletions.

  • addNew: boolean

    Also adds a dump belongs accepts that the list never held, where the newest-first order puts it among the dumps loaded: a new one first. One older than all of them is left to the next pages. For the lists sorted by createdAt: the index's feed, a profile's dumps, the followed users'.

  • belongs: (dump: Dump) => boolean

    Which dumps the list may hold. A dump that left it, as by turning private, comes back where it was when it returns and this accepts it. Without it, the list never gains a dump.

  • isOwner: boolean

    The list holds the viewer's own private dumps, as their profile, the index and the search do, so that one they make private stays. Only its poster hears of a private dump's edits.

src/hooks/dump/useUpvotedList.ts

Keeps a list of the dumps a user upvoted in step with their votes.

Functions

f
useUpvotedList(unnamed 0: UseUpvotedListOptions): UseUpvotedListResult

Follows the votes of profileUserId into their upvoted list. A dump they upvote is fetched and put first. One whose vote they withdraw fades out (useFading), then leaves the list, unless they vote again meanwhile. The socket's vote events report every browser's votes; on one's own list, myVotes also reports this tab's at once.

Interfaces

I
UseUpvotedListOptions

Passed to useUpvotedList by useUserDumpFeed, for an upvoted list.

I
UseUpvotedListResult

Returned by useUpvotedList.

src/hooks/dump/useDumpCardLink.ts

Makes a dump's card open the dump as a link would, and mark it visited.

src/hooks/profile/useProfile.ts

The user a profile page is about comes through this hook, which loads them by the name in the URL and keeps them live. Each tab of the profile loads its own lists, by the user's id.

Functions

f
useProfile(username: string | undefined): UseProfileResult

Returns the user named username, from GET /api/users/@:username, which answers a former name or another case too. The profile then puts the current name in its URL, and that path, naming the user on screen, fetches nothing; another user's does. The socket's user_updated keeps them live, and a reconnection fetches them again.

Interfaces

I
UseProfileResult

Returned by useProfile to UserPublicProfile.

src/hooks/category/useCategories.ts

Gives components the categories of one kind, of those CategoriesProvider loads once for the whole app.

Functions

f
useCategories(kind: CategoryKind): KindCategories

Returns the categories of kind, dump or playlist, and a function that refetches them. Under no CategoriesProvider, the list is empty.

Interfaces

I
KindCategories

Returned by useCategories: one kind's categories.

src/hooks/category/useCategoryFilter.ts

Keeps a list narrowed to one category, whatever the socket puts in it.

Functions

f
useCategoryFilter<T extends { categoryIds?: string[]; }>(
setItems: (fn: (prev: T[]) => T[]) => void,
categoryId?: string
): (fn: (prev: T[]) => T[]) => void

Returns setItems wrapped so that every list it sets keeps only the items filed under categoryId, or setItems as it is without one. A list of one category's dumps or playlists passes it to the hooks that apply the socket's messages, so that one created outside the category isn't added, and one edited out of it leaves.

src/hooks/category/useCategorySelection.ts

Holds the categories picked in a form's CategorySelect.

Functions

f
useCategorySelection(initial?: Iterable<string>): [
Set<string>,
(id: string) => void,
(ids: Set<string>) => void
]

Returns the ids picked, starting from initial, a function that turns one on or off, and one that replaces them all. The dump creation dialog, the dump editor and the playlist forms send [...selected] as categoryIds.

src/hooks/chat/useChat.ts

Lets a button open the chat, by reading ChatContext.

Functions

f
useChat(): ChatContextValue

Returns the chat modal's open state, and the functions that open and close it. Needs ChatProvider above, which App mounts around every page.

src/hooks/auth/useAuth.ts

Gives components the session, and the functions that sign in, sign out and make requests with its token.

Functions

f
useAuth(): UseAuthResult

Returns the session and the functions that act on it. It must be called under AuthProvider.

f
useRequiredAuth(): UseRequiredAuthResult

Returns the session, for a page that RestrictedLoggedIn guards, such as DumpEdit, so it needn't check for a user.

Interfaces

I
UseAuthResult

Returned by useAuth to any component that needs the session. Its functions are stable until the session's token changes.

I
UseRequiredAuthResult

Returned by useRequiredAuth. It is UseAuthResult with a user that can't be null.

src/hooks/auth/useSettings.ts

Gives components the signed-in user's settings, which are stored on the server and kept with the session. ThemeProvider reads the style and scheme from it, and the settings page the digest switches.

Functions

f
useSettings(): UseSettingsResult

Returns the settings and their setter. It must be called under AuthProvider.

Interfaces

I
UseSettingsResult

Returned by useSettings to whatever reads or changes settings.

src/hooks/shared/useDocumentTitle.ts

Sets the browser tab's title from the page's title, the site's name and the unread count. Every page calls useDocumentTitle, and WSProvider calls setTitleBadge.

Functions

f
setTitleBadge(count: number): void

Prefixes the tab's title with the unread count, as in "(3) Chat · gerbeur", or removes the prefix at 0. Past 99 it shows "99+".

f
useDocumentTitle(title: string | null | undefined): void

Sets the tab's title to title · site name for the page that calls it, since a client-side navigation leaves the server's <title> in place. A page passes null for the bare site name, and undefined while its data loads, which keeps the title the server rendered for that URL rather than flashing the site name.

Variables

v
SITE_EMOJI: string

The site's emoji, read once at startup, as the header shows it.

v
SITE_NAME: string

The site's name, read once at startup, as the header shows it.

src/hooks/shared/useCanonicalPath.ts

Puts a page's current path in the URL once the page knows it. A user, dump or playlist reached by a former name or slug, or by a name in another case, comes back from the API under its current one, from which the page builds its path; the server's OG middleware answers the same with a 301.

Functions

f
useCanonicalPath(canonical: string | null | undefined): void

Replaces the current path with canonical when they differ, keeping the query string, the hash and the history entry's state. It replaces the entry, so going back skips the old path. Nothing happens while canonical is null, as while the page loads. The profile, dump, dump editor and playlist pages call it.

src/hooks/shared/useFetchState.ts

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.

Functions

f
useFetchState<T>(
key: string | undefined,
load: (signal: AbortSignal) => Promise<T>,
unnamed 2?: FetchStateOptions<T>
): UseFetchStateResult<T>

Returns what load resolves to for key, loaded the first time it is enabled, and kept afterwards. A new key starts over and aborts the load of the last one, as leaving the page does; an undefined key loads nothing. The key must change with whatever the load depends on, the viewer included when it is signed in. load must reject with an Error, as expectOk does, whose message it shows.

Interfaces

I
FetchStateOptions

Passed to useFetchState.

  • enabled: boolean

    False until it should load, as the profile's tabs pass "this tab is open". Turning it off doesn't abort a load under way, so those tabs finish loading in the background. True by default.

  • initial: T

    It holds the data already at hand for a new key, as a feed restored from useFeedCache, which starts loaded and isn't loaded again. It is read only when the key changes.

I
UseFetchStateResult

Returned by useFetchState.

Type Aliases

T
FetchState<T> = LoadState<{ data: T; }>

Held by useFetchState, and passed on to ListState.

src/hooks/shared/useCardLink.ts

Gives a dump's or a playlist's card, which is a clickable <li> and not a link, a link's behaviour: a middle or modified click opens its page in a new tab.

src/hooks/shared/useRoomBeside.ts

Measures the width an element has beside another in a shared row, for the presence avatars in the feed's header (PresenceRow).

Functions

f
useRoomBeside(
el: HTMLElement | null,
unnamed 1: RoomBesideOptions
): number | null

Returns the width el has to itself in a row it shares with an element that comes first, such as the tabs that leave room for the presence avatars. The first element counts at its content's natural width, since that depends on the theme's fonts and the locale, and the row's gap is taken off too. It returns null until measured.

Interfaces

I
RoomBesideOptions

Passed to useRoomBeside by PresenceRow, from its own props. Without both selectors, the hook measures nothing and returns null.

src/hooks/shared/useScrollLock.ts

Freezes the page's scroll under Modal and ConfirmModal.

Functions

f
useScrollLock(): void

Freezes the page's scroll while the calling component is mounted, so that the page doesn't slide under a dialog's backdrop. Nested locks are counted.

src/hooks/shared/useTabParam.ts

Keeps a page's active tab in its path, as one more segment, for the profile and search pages.

Functions

f
useTabParam<T extends string>(
base: string,
validTabs: Iterable<T>,
defaultTab: T
): [T | null, (
tab: T,
search?: string
) => void]

Returns the tab the path segment after base names, defaultTab on the bare base, or null when the segment names none of validTabs, for the page to answer as not found; and a setter that navigates to a tab in place of the current history entry, the default tab to the bare base, with the query string search, the current one by default. A segment after the tab's, as the profile's /follow/playlists, is the tab's own business.

src/hooks/shared/useHeightTransition.ts

Eases an element's height when its content swaps, for the pages and cards that open an edit form or a reply box in place.

Functions

f
useHeightTransition<T extends HTMLElement>(watch: unknown): (el: T | null) => (() => void) | undefined

Returns a callback ref for the element whose height eases whenever watch changes, usually the flag that swaps its content. Only the box is animated, so the content that appears should take the animate-in class to fade in. The duration follows --dur-reveal, and nothing moves under prefers-reduced-motion.

src/hooks/shared/useAnchoredFloat.ts

Places a floating panel against its anchor with position: fixed, for TextEditor's emoji picker and mention list.

Functions

f
useAnchoredFloat(
anchorRef: RefObject<HTMLElement | null>,
open: boolean,
unnamed 2: AnchoredFloatOptions
): CSSProperties | null

Returns the style that places a panel above or below anchorRef, whichever has more room, kept inside the viewport and following scrolls and resizes. The panel must be portaled out of any clipping ancestor, and its scrolling part must take its height from the --float-max-h property of the style.

Interfaces

I
AnchoredFloatOptions

Passed to useAnchoredFloat by the component that owns the panel, in pixels. The heights let the hook size the panel's scrolling part to the room left.

src/hooks/shared/useSoundsEnabled.ts

Gives the profile page's settings the toggle for notification and chat sounds.

Functions

f
useSoundsEnabled(): [boolean, (enabled: boolean) => void]

Returns whether sounds are on in this browser, and a setter that stores the choice. playChime reads the stored value each time it plays, so the choice applies at once without this hook, and another mounted copy of it isn't updated.

src/hooks/social/useFollows.ts

Tells a Follow button what the user follows, and follows or unfollows, by reading FollowContext.

Functions

f
useFollows(): FollowContextValue

Returns the users and playlists the signed-in user follows, and the functions that follow and unfollow them. Needs FollowProvider above, which App mounts around every page.

src/hooks/social/useCommunityFeed.ts

Gives a component the community feed, by reading CommunityFeedContext.

Functions

f
useCommunityFeed(): CommunityFeed | null

Returns the community feed, kept up to date for as long as the component is mounted, or null until it is loaded and for a visitor. The CommunityFeed component renders it. Needs CommunityFeedProvider above, which App mounts around every page.

src/config/imageSizes.ts

States the width each of the site's images is laid out at, which App.css sets, so that imageProps asks the server for a copy no larger. A number is a fixed width in CSS pixels, a string a sizes attribute for a width that follows the viewport's. The components that show a thumbnail, a cover or a player's artwork take theirs from here; an avatar's is its size.

Variables

v
IMAGE_SIZES: { compactThumbnail: number; richThumbnail: string; journal: { feature; wide; tall; square; }; playlistThumb: number; playlistTile: string; playlistBanner: string; playerArtwork: number; }

The width of each image, by the place it is shown.

src/config/instance.ts

Lets a deployment set what the app can't know from the browser: the style a visitor sees before choosing one, whether the server sends email, without which the digest settings do nothing, and which ways federated sign-in goes.

Variables

v
DEFAULT_STYLE: StyleName

The instance's style, which applies until a visitor or their account picks one. A missing or unknown value means BASE_STYLE. ThemeProvider reads it.

v
EMAIL_ENABLED: boolean

Whether the server sends email. Without it the settings page shows the digest switches disabled, with a note why.

v
FEDERATION_INCOMING: boolean

Whether users of other instances sign in here. Without it the login and register pages offer no other instance.

v
FEDERATION_OUTGOING: boolean

Whether this instance's users sign in to other instances. Without it the settings page doesn't list the instances they signed in to, since none remain.

src/config/playerMode.ts

Lets a deployment choose between Bandcamp's embed and the site's own player. It reads the choice for usePlayRichContent.

Type Aliases

T
BandcampPlayer = "embed" | "native"

A Bandcamp player. "embed" is Bandcamp's own iframe, "native" is the site's player on the resolved streams.

Variables

v
BANDCAMP_PLAYER: BandcampPlayer

This deployment's Bandcamp player, read once when the app loads.

src/config/api.ts

Gives every request of the frontend its base URL, and the feeds their page sizes.

Variables

v
API_URL: string

The API's origin, which every fetch of the frontend prefixes to its path. It is empty in production, where the API is the page's own origin.

v
COMMUNITY_FEED_URL: string

The community feed's endpoint, for CommunityFeedProvider.

v
COMMUNITY_URL: string

The community endpoint, for the community page.

v
DEFAULT_PAGE_SIZE: 20

How many items a page of a feed or list asks the API for.

v
NOTIFICATIONS_PAGE_SIZE: 30

How many notifications a page of /notifications asks the API for.

v
SEARCH_URL: string

The search endpoint, for the search page.

v
WS_URL: string

The socket's origin, for WSProvider. It is always absolute, since WebSocket takes no relative URL.

src/config/feedTabs.ts

Holds the feed tab the index opens on until a user picks one. The tabs themselves are FEED_TABS in shared/paths.ts, since they are path segments the server reserves at the root.

Variables

v
DEFAULT_FEED_TAB: FeedTab

The tab the bare / and /<category> show visitors, and users who haven't picked one in their settings.

src/i18n.ts

Picks the interface's language and loads its messages into Lingui's global i18n, which the components' t and Trans read. main.tsx calls it once before rendering.

Functions

f
loadCatalog(locale?: Locale): Promise<void>

Loads the messages of locale, the detected one by default, then activates it and stores it for the next visit. There is no language switch in the interface yet, so the stored one comes from the browser's language.

src/contexts/theme/ThemeContext.ts

Shares the chosen theme with every component. ThemeProvider fills this context and useTheme reads it.

Interfaces

I
ThemeContextValue

Provided by ThemeProvider to useTheme. A setter applies the choice and stores it in this browser, and in the account's settings when signed in.

Variables

v
ThemeContext: React.Context<ThemeContextValue>

The theme's context. Its default is only seen outside ThemeProvider.

src/contexts/theme/ThemeProvider.tsx

Lets each user pick the site's style and color scheme, and applies the choice to the document. A signed-in user's choice is one of their settings, so it follows them across browsers; a visitor's stays in this browser. Until either picks a style, the instance's default applies. App mounts it inside AuthProvider, whose session holds the settings.

Functions

f
ThemeProvider(unnamed 0: ThemeProviderProps): React.JSX.Element

Provides ThemeContext. Each half of the theme goes through useThemeChoice, which sets it on the root element as data-style or data-color-scheme and mirrors it to localStorage, where an inline script in index.html applies it before the first paint, so the page doesn't flash another theme. The two must agree. When an account has no choice yet and this browser has one, it is saved to the account, once per session.

Interfaces

I
ThemeProviderProps

Passed to ThemeProvider by App.

src/contexts/ws/WSContext.ts

Live updates from the server reach the pages through the contexts declared here. Three hold state: votes, likes, deletions and counts in one, the chat in another, who is online in the third, split so that a feed doesn't re-render with every chat message or presence change. The fourth passes on events, each change to a dump, playlist, comment or profile and each notification, to the components that subscribe to their kind. WSProvider fills them all from its WebSocket, and components read them with useWS, useWSChat, useWSPresence and useWSEvent.

Functions

f
createWSEventBus(): WSEventBus

Returns a bus with no subscribers.

Interfaces

I
ChatError

Tells the user the server refused their chat edit or delete. WSProvider sets it as chatError, and ChatModal shows it for a few seconds.

I
ChatReplyTarget

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.

I
CommentEvent

Keeps comment threads and comment counts live. WSProvider emits it as a comment event on each comment_* message; the Dump page applies it to its thread, and useDumpListSync to the counts on dump cards.

I
DumpEvent

Keeps every dump on screen up to date. WSProvider emits it as a dump event on each dump_created and dump_updated; useDumpListSync applies it to lists, and the Dump page to its dump. A dump that turns public comes created to those who couldn't see it.

I
PendingChatMessage

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.

I
PlaylistEvent

Keeps the playlists on screen up to date, in lists, on a playlist's page and on a dump's page. WSProvider emits it as a playlist event on each playlist_* message; lists apply it through usePlaylistListSync, a playlist's page through usePlaylist. Which fields are set depends on type.

I
RelatedEvent

Keeps the related dumps on a dump's page live. WSProvider emits it as a related event on each dump_related_changed, and the Dump page fetches its related dumps again when dumpIds names its dump.

I
UserEvent

Keeps a user's profile, and their name on what they posted, up to date. WSProvider emits it as a user event on each user_updated, which the server sends on any change to a profile; useProfile applies it to the profile page, and the list sync hooks to the names on cards.

I
VoteEvent

Keeps a user's upvoted list live, on their profile's overview and tab. WSProvider emits it as a vote event on each votes_update, and useUpvotedList adds or drops the dump.

I
WSChatContextValue

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 WSChatContext, and components read it with useWSChat. The socket sends chat only to signed-in users, so for a visitor it stays empty.

I
WSContextValue

Carries the live state but the chat and presence, for pages to lay over what they fetched. WSProvider provides it through WSContext, and components read it with useWS. The changes to single items come as events instead, through WSEventsContext. After a reconnect, connectionEpoch tells pages to fetch what they missed.

I
WSEventBus

Delivers WSEvents to their subscribers. WSProvider creates one with createWSEventBus and emits on it as messages arrive; components subscribe through WSEventsContext, and tests emit on their own.

I
WSEvents

The events WSProvider passes on, by kind, to the components that subscribe with useWSEvent. Unlike the state of WSContext, every event reaches every subscriber, however many arrive before the next render. categories says the categories changed, and carries nothing.

I
WSPresenceContextValue

Carries who is online, for PresenceRow. WSProvider provides it through WSPresenceContext, and components read it with useWSPresence.

Variables

v
defaultWSChatValue: WSChatContextValue

Lets a component render outside WSProvider, with an empty chat.

v
defaultWSPresenceValue: WSPresenceContextValue

Lets a component render outside WSProvider, with nobody online.

v
defaultWSValue: WSContextValue

Lets a component render outside WSProvider, as if the socket were connecting.

v
WSChatContext: React.Context<WSChatContextValue>

Carries the chat's live stream and read state. Read it with useWSChat.

v
WSContext: React.Context<WSContextValue>

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.

v
WSEventsContext: React.Context<Pick<WSEventBus, "subscribe">>

Carries the bus WSProvider emits its events on, for useWSEvent. Outside WSProvider, it is a bus no one emits on.

v
WSPresenceContext: React.Context<WSPresenceContextValue>

Carries who is online. Read it with useWSPresence.

src/contexts/ws/WSProvider.tsx

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, and into the events of WSEventsContext. App mounts it around every page. The protocol is shared/ws.ts, and the server's side is api/routes/ws.ts.

Functions

f
WSProvider(unnamed 0: WSProviderProps): React.JSX.Element

Provides WSContext, WSChatContext, WSPresenceContext, AvatarOverrideContext and WSEventsContext. It connects with the session's token, or anonymously, and reconnects with a backoff doubling up to 30 s. A new token (sign-in, sign-out, another account) clears the live state and reconnects. A new notification or chat message chimes, and the unread counts go in the tab title.

Interfaces

I
WSProviderProps

Passed to WSProvider by App.

src/contexts/ws/AvatarOverrideContext.ts

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.

Interfaces

I
AvatarOverride

Set by WSProvider for a user whose profile changed, and read by Avatar, where it wins over the possibly stale props the page fetched.

Variables

v
AvatarOverrideContext: React.Context<Record<string, AvatarOverride>>

Shows changed avatars without a reload, as an 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.

src/contexts/player/PlayerContext.ts

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.

Functions

f
playerItemKey(item: PlayerItem): string

Returns a key that identifies item whatever its stream URL, so that a re-resolved track is still the same track. GlobalPlayer keys its tracklist on it and tells a new item by it; PlayerProvider tells repeated failures of one track by it.

Interfaces

I
PlayerContextValue

Provided by PlayerProvider through PlayerContext. Cards and previews read what plays and call the controls; GlobalPlayer and MediaPlayer use the internal fields to report the media's state back.

I
PlayerItemBase

The fields of every PlayerItem, which GlobalPlayer's header shows.

I
PlayerTimeValue

Provided by PlayerProvider through PlayerTimeContext, apart from the rest because it changes several times a second while something plays. Only what draws a position, like the dump page's audio preview, reads it, so the cards that read PlayerContext don't re-render with it.

Type Aliases

Variables

v
defaultPlayerTimeValue: PlayerTimeValue

Given by PlayerTimeContext outside PlayerProvider.

v
defaultPlayerValue: PlayerContextValue

Given by PlayerContext outside PlayerProvider: nothing plays and no control does anything. The tests' render helper starts from it.

v
PlayerContext: React.Context<PlayerContextValue>

The player's state and controls, from PlayerProvider.

v
PlayerTimeContext: React.Context<PlayerTimeValue>

The playhead of the current file or stream, from PlayerProvider.

src/contexts/player/PlayerProvider.tsx

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.

Functions

f
PlayerProvider(unnamed 0: PlayerProviderProps): React.JSX.Element

Provides PlayerContext and PlayerTimeContext. The queue and the position are saved as they change and restored on load, paused; signing out, or in as someone else, closes the player. A stream whose signed URL expires is re-resolved and resumed where it was, and falls back to its embed if that fails.

Interfaces

I
PlayerProviderProps

Passed to PlayerProvider by App, around the whole app.

src/contexts/category/CategoriesProvider.tsx

Spares every page fetching the site's categories: it loads both kinds once for the whole app, and again whenever the category manager changes them, here or in another browser, and after the socket reconnects. App mounts it inside WSProvider.

Functions

f
CategoriesProvider(unnamed 0: CategoriesProviderProps): React.JSX.Element

Provides CategoriesContext, fetching the categories on mount. Until a fetch succeeds, isLoaded stays false, so that no page takes an empty list for the truth and answers a category's URL with a 404; a failed first fetch is tried again 10 s later. A failed refresh keeps the previous list.

Interfaces

I
CategoriesProviderProps

Passed to CategoriesProvider by App.

src/contexts/category/CategoriesContext.ts

Shares the site's categories, of both kinds, with every component. CategoriesProvider fills this context and useCategories reads one kind's from it.

Interfaces

I
CategoriesContextValue

Provided by CategoriesProvider to useCategories, for the category switchers and pickers, the dump and playlist pages and cards, and the category manager.

Variables

v
CategoriesContext: React.Context<CategoriesContextValue>

The categories' context. Its default, an empty list never loaded, is only seen outside CategoriesProvider.

src/contexts/chat/ChatContext.ts

Lets the header's chat button and the floating one open the same chat modal, which ChatProvider owns. Read it with useChat.

Interfaces

I
ChatContextValue

Provided by ChatProvider through ChatContext. The modal closes itself, so opening is all its callers need.

Variables

v
ChatContext: React.Context<ChatContextValue>

Opens the chat modal. Outside ChatProvider it does nothing.

src/contexts/chat/ChatProvider.tsx

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.

Functions

f
ChatProvider(unnamed 0: ChatProviderProps): React.JSX.Element

Provides ChatContext and renders the one chat modal while it is open. openChat opens it, and so does a ?chat= query parameter on any page: ?chat=open, or ?chat=<message id> to open on that message, which is where a chat mention notification links. It stays closed for visitors.

Interfaces

I
ChatProviderProps

Passed to ChatProvider by App.

src/contexts/auth/AuthContext.ts

Shares the session with every component. AuthProvider fills this context and useAuth reads it; nothing else should touch it.

Interfaces

I
AuthContextValue

Provided by AuthProvider to useAuth, which builds the session's API on it.

Variables

v
AuthContext: React.Context<AuthContextValue>

The session's context. Its default, a signed-out session whose setter does nothing, is only seen outside AuthProvider, as in some tests.

src/contexts/auth/AuthProvider.tsx

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.

Functions

f
AuthProvider(unnamed 0: AuthProviderProps): React.JSX.Element

Provides AuthContext. The session starts as the stored one and follows it when another tab changes it. The server checks it on mount and whenever the tab regains focus: a 401 ends it, and a user or settings changed on the server replace the stored ones. A session from before media tokens gets one.

Interfaces

I
AuthProviderProps

Passed to AuthProvider by App.

src/contexts/social/CommunityFeedProvider.tsx

Keeps the community feed for the components that show it, loaded from GET /api/community/feed and again whenever the socket tells of something the feed may hold. App mounts it around every page, so that the feed outlives the header it is shown in, which every page renders anew.

Functions

f
CommunityFeedProvider(unnamed 0: CommunityFeedProviderProps): React.JSX.Element

Provides CommunityFeedContext. It fetches the feed once a component watches it, for the signed-in user alone. The socket's messages don't carry a feed's lines, an upvote's naming neither its dump nor its voter, so each one that may change the feed has it fetched again, and the server's query stays the one place that decides what the feed holds. A change that comes while nothing watches is caught up with on the next watch.

Interfaces

I
CommunityFeedProviderProps

Passed to CommunityFeedProvider by App.

src/contexts/social/CommunityFeedContext.ts

Hands the community feed, the latest things the site's members did in public, to whatever shows it: the GeoCities style's marquee, for now. CommunityFeedProvider provides this context and useCommunityFeed reads it.

Interfaces

I
CommunityFeedContextValue

Provided by CommunityFeedProvider through CommunityFeedContext.

  • feed: CommunityFeed | null

    Null until it is fetched, which waits for a first watch, and always for a visitor: the feed is for signed-in users.

  • watch: () => () => void

    Says a component is showing the feed, until the returned call. The feed is fetched, and kept up to date, only while one is: most styles show it nowhere. useCommunityFeed calls it.

Variables

v
CommunityFeedContext: React.Context<CommunityFeedContextValue>

Provided by CommunityFeedProvider, and read with useCommunityFeed.

v
defaultCommunityFeedValue: CommunityFeedContextValue

Given to a consumer outside CommunityFeedProvider: no feed.

src/contexts/social/FollowProvider.tsx

Keeps what the signed-in user follows for the Follow buttons, loaded from GET /api/me/follow and again when another tab changes it. App mounts it around every page.

Functions

f
FollowProvider(unnamed 0: FollowProviderProps): React.JSX.Element

Provides FollowContext. It fetches the follows whenever the token changes, again when follows_changed says another tab changed them, and after the socket reconnects. It empties them on sign-out.

Interfaces

I
FollowProviderProps

Passed to FollowProvider by App.

src/contexts/social/FollowContext.ts

Tells the Follow buttons what the signed-in user follows. FollowProvider provides this context and useFollows reads it.

Interfaces

I
FollowContextValue

Provided by FollowProvider through FollowContext. The four functions change the sets at once and undo the change when the request fails. They resolve to whether it worked, and never reject.

Variables

v
defaultFollowValue: FollowContextValue

Given to a consumer outside FollowProvider, for which nothing is followed.

v
FollowContext: React.Context<FollowContextValue>

Tells the Follow buttons what the signed-in user follows. Read it with useFollows.

src/utils/feed/hotScore.ts

Ranks dumps for the "hot" and "journal" tabs of the index page.

Functions

f
hotScore(dump: Dump): number

Returns the score that DumpFeed and composeJournal sort by, highest first. It grows with the votes and decays with the age in hours, so a new dump without votes can outrank an old popular one. The server doesn't rank, so only the loaded dumps are ranked.

src/utils/feed/journalLayout.ts

Lays out the "journal" tab of the index page: it ranks the loaded dumps by hotScore, gives each a tile size in a 4-column mosaic, and cuts the mosaic into blocks. JournalFeed renders the blocks.

Functions

f
composeJournal(dumps: Dump[]): JournalBlock[]

Sorts dumps by hotScore and lays them out as a mosaic whose rows all close, cut into blocks. JournalFeed calls it on every change of the feed.

f
hasQuote(dump: Dump): boolean

Checks whether dump has a non-blank note, which JournalCard shows as a pull-quote when the dump has no image.

f
hasThumbnail(dump: Dump): boolean

Checks whether dump has an image to show: an uploaded thumbnail, an image, video or audio file, or a link preview's thumbnail. Only such dumps get the large tiles, and JournalCard shows them as image tiles.

Interfaces

I
JournalBlock

Returned by composeJournal. JournalFeed renders each block as its own grid, with a rule between two blocks.

I
JournalEntry

Returned by composeJournal within a JournalBlock.

Type Aliases

T
JournalShape = "feature" | "wide" | "tall" | "square"

The grid span of a tile, which JournalCard turns into a CSS class. A feature spans 2×2 cells, a wide 2×1, a tall 1×2 and a square 1×1. Whether the tile shows an image, a quote or text depends on the dump alone.

src/utils/feed/pagedList.ts

Makes every list the API serves a page at a time work the same way. Each is a PagedList, whose next page useLoadMore appends, and this module reads its pages.

Functions

f
canLoadMore(list: PagedList<unknown> | null): boolean

Checks whether list has a next page to load now, which useLoadMore requires and useInfiniteScroll takes as enabled. A list not loaded yet is null.

f
firstPage<T>(
items: T[],
hasMore: boolean
): PagedList<T>

Returns the list holding only its first page, items.

f
mapItems<T>(fn: (items: T[]) => T[]): (list: PagedList<T>) => PagedList<T>

Returns a setter of a list's items as a setter of the list, for the sync hooks, which edit items only.

f
pageUrl(
url: string,
page: number,
limit?
): string

Returns url asking for page page, limit items long.

f
readFirstPage<T>(
res: Response,
deserialize: (wire: Wire<T>) => T
): Promise<PagedList<T>>

Returns the first page res answers as a list, or an empty list when the server refuses it. The profile pages read their lists with it beside the user, whose failure alone fails the page.

f
refetchPages<T extends { id: string; }>(
pages: number,
fetchPage: (page: number) => Promise<PaginatedData<T>>
): Promise<PaginatedData<T>>

Fetches pages 1 to pages of a list again, and resolves to their items, each once, and whether more follow. An item added between two pages' fetches shifts the later page by one, which would list an item twice. A live list calls it after a reconnection, through usePagedList's resync or, for the search's dumps, directly; it rejects when a page fails.

f
withByDate<T extends { id: string; createdAt: Date; }>(
items: T[],
item: T
): T[]

Returns items, newest first, with item where its createdAt puts it, as withInOrder does. useDumpListSync puts a new or newly visible dump in a list with it.

f
withFirst<T extends { id: string; }>(
items: T[],
item: T
): T[]

Returns items with item first, or items as they are when it is already there, as when a list's own echo of a new item arrives after it.

f
withInOrder<T extends { id: string; }>(
items: T[],
item: T,
compare: (
a: T,
b: T
) => number
): T[]

Returns items, in the order of compare as the API pages them, with item where that order puts it: before the first item that doesn't come before it. An item that comes after every one loaded is left to a later page, and items returned as they are, as they are when item is already there. usePlaylistListSync puts a new, newly visible or risen playlist in a list with it, whatever its order.

Interfaces

I
PagedList

Held for a list by usePagedList, or by a page that loads it along with more, as the search page does. useLoadMore loads the pages after page.

src/utils/player/volume.ts

Remembers the media player's volume across tracks and reloads. GlobalPlayer keys MediaPlayer on its source, so every track mounts a new media element at full volume; MediaPlayer reads the last volume from here on mount and writes back every change, which localStorage keeps across reloads.

Functions

f
rememberVolume(
volume: number,
muted: boolean
): void

Remembers volume and muted for the next media element, and in localStorage when it is available.

f
storedVolume(): { volume: number; muted: boolean; }

Returns the volume and mute state last remembered, or full and unmuted.

src/utils/player/bandcamp.ts

Turns a Bandcamp page into a queue of streams for the site's own player. usePlayRichContent calls it when a Bandcamp link is played in native mode, and streamSources.ts again when the streams' signed URLs expire.

Functions

f
resolveBandcampQueue(
pageUrl: string,
ctx?: BandcampContext,
unnamed 2?: { force?: boolean; }
): Promise<PlayerItem[]>

Resolves the Bandcamp page pageUrl into one stream item per streamable track, through GET /api/remote/bandcamp, each carrying ctx. force bypasses the server's cache, for re-resolving once the signed URLs expire.

Interfaces

I
BandcampContext

Passed to resolveBandcampQueue by its callers and copied onto every item of the queue, since the server's tracklist knows nothing of the dump.

src/utils/player/duration.ts

Formats track lengths for the media player and the global player's tracklist.

Functions

f
fmt(s: number): string

Returns s seconds as m:ss, or 0:00 when s isn't finite, as a media element's duration is until its metadata loads.

src/utils/player/waveform.ts

Loads an audio file's waveform for Scrubber, by way of usePeaks: an uploaded file's from the server, which made it once with ffmpeg, and a file not yet uploaded (the dump form's preview) by decoding it here.

Functions

f
loadPeaks(source: PeaksSource): Promise<number[]>

Returns the peaks of source, from 0 to 1.

f
peaksKey(source: PeaksSource): string

Returns the key loads keeps source's peaks under.

Type Aliases

T
PeaksSource = { peaksUrl: string; } | { localFile: string; }

Where a waveform's peaks come from: the server's for an uploaded file (dumpWaveformUrl), or the bytes of a local file, by its blob: URL.

src/utils/player/youtube.ts

Reads, prepares and lists YouTube playlist embeds, for GlobalPlayer and useYouTubePlaylist to show and drive a playlist's tracklist.

Functions

f
embedListId(embedUrl: string): string | null

Returns the id of the playlist embedUrl plays, or null. It reads both shapes the server's YouTube embeds take, /embed/videoseries?list=… for a bare playlist and /embed/<id>?list=… for a video opened in one.

f
fetchPlaylistTracks(listId: string): Promise<YouTubePlaylistTrack[]>

Returns the titles and lengths of the videos of playlist listId, from GET /api/remote/youtube, since the embed exposes neither.

f
withJsApi(url: string): string

Returns url with the parameters the IFrame Player API needs to drive the embed. enablejsapi can't be turned on after the iframe loads, so GlobalPlayer adds it to every playlist embed, whether or not the API script loads.

src/utils/player/streamSources.ts

Keeps stream items' signed URLs fresh. PlayerProvider calls it when a stream fails or a restored session's stream is stale, without knowing which service the stream comes from.

Functions

f
isStreamStale(item: Extract<PlayerItem, { kind: "stream"; }>): boolean

Checks whether the signed URL of item is older than STREAM_STALE_MS. PlayerProvider refreshes a restored session's stream when it is, so that the first press of play doesn't stall.

f
resolveStreamQueue(item: Extract<PlayerItem, { kind: "stream"; }>): Promise<PlayerItem[]>

Re-resolves the whole queue that stream item came from, bypassing the server's cache. The promise rejects when the stream's type has no resolver here, or when its resolver fails.

Variables

v
STREAM_STALE_MS: number

How old a stream's signed URL may get before isStreamStale says so. Bandcamp's live about 24 hours, and this leaves room for a long listen.

src/utils/editor/skinTone.ts

Remembers the emoji picker's skin tone across pickers and pages. It lives in localStorage, and EmojiPickerPopup reads and writes it.

Functions

f
readSkinTone(): SkinTone

Returns the stored skin tone, or none when there is none or storage is blocked. The stored value is not checked against frimousse's tones.

Variables

v
SKIN_TONE_KEY: "emoji-skin-tone"

Names the skin tone in localStorage. EmojiPickerPopup writes under it.

src/utils/editor/text.ts

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.

Functions

f
insideCode(textBefore: string): boolean

Checks whether the caret at the end of textBefore sits inside Markdown code, an open ``` fence or an unclosed span on the current line. The mention and emoji autocompletes stay closed there, since `@bob` and `:smile` are literal text in code.

f
isEmojiOnly(
text: string,
max?: number
): boolean

Checks whether text is nothing but max emoji at most, which the chat shows large, as in "🎉" or "👍👍". Emoji are counted as graphemes, so a family or a flag counts once.

src/utils/dump/draft.ts

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.

Functions

f
clearDumpDraft(): void

Removes the stored draft, once the dump is posted or the poster chose to start afresh.

f
isDumpDraftEmpty(draft: DumpDraft): boolean

Checks whether draft holds nothing worth restoring. A title taken from the preview doesn't count, since the preview gives it back.

f
loadDumpDraft(): DumpDraft | null

Returns the stored draft, or null when there is none worth restoring or storage is blocked. A field that doesn't parse takes its empty value.

f
saveDumpDraft(draft: DumpDraft): void

Stores draft, or removes the stored one when draft is empty. A failure is silent: the draft is a convenience, never a precondition for posting.

Interfaces

I
DumpDraft

Saved by DumpCreateModal on every change, and restored when it next opens without a shared link. Posting the dump clears it.

Variables

v
EMPTY_DUMP_DRAFT: DumpDraft

The modal's starting values, when no draft is restored.

src/utils/chat/message.ts

Decides when a chat message can be sent and how its text is shown, for the chat modal's composer, edit box and message rows.

Functions

f
messageTextClass(body: string): string

Returns the class for the text of a sent or pending message. A message of nothing but a few emoji gets the one that shows it large.

f
sendable(draft: string): boolean

Checks whether draft can be sent or saved. Like the server, it measures the trimmed text.

f
snippet(body: string): string

Returns body as one line of plain text, for the reply bar above the composer and the reply line above a message.

Variables

v
COUNT_FROM: 1800

The length from which the composer and the edit box show their character counter, near VALIDATION.CHAT_MESSAGE_MAX.

src/utils/chat/text.ts

Finds the mentions of the signed-in user in a chat message.

Functions

f
mentionsUser(
body: string,
username: string
): boolean

Checks whether body @mentions username, ignoring case, with the pattern the server uses for mention notifications, this instance's own host included (localHandle). WSProvider uses it to pick the chime, and ChatModal to highlight the message.

src/utils/chat/draft.ts

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.

Functions

f
readDraft(userId: string | undefined): StoredDraft

Returns the stored draft of userId, or an empty one when there is none, userId is undefined, or storage is blocked.

f
writeDraft(
userId: string | undefined,
value: StoredDraft
): void

Stores value as the draft of userId, or removes it when it has neither text nor a reply target. Does nothing when userId is undefined.

Interfaces

I
StoredDraft

Returned by readDraft to ChatModal, which passes it back to writeDraft.

src/utils/chat/time.ts

Formats the chat modal's dates, in the interface's locale, for its day separators and the times beside grouped messages.

Functions

f
clockTime(date: Date): string

Returns the hours and minutes of date, without the day period ("PM") so it fits the narrow space beside a grouped message. Its tooltip shows the full time.

f
dayLabel(date: Date): string

Returns the label of the day separator for date. It is "Today", "Yesterday", or the full date, with the year only when it isn't this one.

f
isSameDay(
a: Date,
b: Date
): boolean

Checks whether a and b fall on the same local day.

src/utils/auth/permissions.ts

Checks the signed-in user's permissions, for the pages and components that hide what the server would refuse.

Functions

f
can(
user: Pick<PublicUser, "role"> | null | undefined,
permission: Permission
): boolean

Checks whether user has permission. A null user, for the signed out, has none. The server checks again, so this only decides what to show.

src/utils/auth/federation.ts

Ties a federated sign-in to the tab that started it, so that a link to the callback page made from someone else's sign-in can't sign this browser in to their account (login CSRF). FederatedSignIn keeps the sign-in's state before leaving for the other instance, in sessionStorage, which outlives the trip there and back in one tab, and FederationCallback finishes only a sign-in whose state it finds.

Functions

f
forgetSignIn(): void

Forgets the sign-in this tab started, once it is being finished.

f
rememberSignIn(authorizeUrl: string): void

Keeps the state of the sign-in that authorizeUrl starts.

f
startedHere(state: string): boolean

Returns whether state is that of the sign-in this tab started.

src/utils/auth/next.ts

Brings a visitor back to the page that sent them to sign in. The /authorize page sends a signed-out visitor to /login?next=, and the login page and RestrictedGuest follow next once they are signed in.

Functions

f
loginPath(path: string): string

Returns the /login path that comes back to path after signing in, a path on this site with its query.

f
nextPath(params: URLSearchParams): string | null

Returns the next of params when it is a path on this site, or null. Anything else, //elsewhere.example included, is ignored, so a link can't send someone off the site through the login page.

src/utils/auth/home.ts

Points at the instance an account from another gerbeur comes from, its home, where its password, email, avatar and name are changed. Such a user's handle here is name@host (splitHandle); the profile, the settings and the /authorize page read it through these.

Functions

f
homeHost(handle: string): string | undefined

Returns the host of handle's home instance, or undefined for a local user.

f
homeUrl(
handle: string,
path?: string
): string | null

Returns a link to path on handle's home instance, their profile there by default, or null for a local user. The host in a handle is the home's API, not necessarily its pages, so the link goes through GET /api/users/:key/home, which redirects to them.

f
isSelfHost(host: string): boolean

Checks whether host, as a handle writes it, is this instance's: its API's, which handles carry, or its pages'. Another instance writes this one's users with it.

src/utils/auth/storage.ts

Keeps the user signed in across reloads and tabs, in one localStorage entry every tab shares. AuthProvider and useAuth are its only users.

Functions

f
forgetStoredSession(token: string | undefined): void

Forgets the stored session, but only if it still holds token. Another tab may have replaced it with fresh tokens from a password change, and a 401 for the old token here must not wipe those.

f
isTokenExpired(token: string): boolean

Checks whether token, a JWT, has expired, without asking the server. An unreadable one counts as expired.

f
readStoredSession(): Session | null

Returns the stored session, or null. A session that is unreadable, expired or from before roles existed is removed from storage.

f
storeSession(session: Session): void

Stores session, for every tab.

Type Aliases

T
Session =
Omit<AuthResponse, "mediaToken">
& { mediaToken?: string; settings?: UserSettings; }

Held by AuthProvider and stored here. It is the AuthResponse of a sign-in or sign-up, except that one stored before media tokens existed has none until AuthProvider fetches it. AuthProvider adds the user's settings once it has read them from the server, and useSettings keeps them current.

Variables

v
AUTH_STORAGE_KEY: "authResponse"

The localStorage key of the session. AuthProvider listens for changes to it from other tabs.

src/utils/auth/unsubscribe.ts

Reads which digest an emailed unsubscribe link turns off, so that the /unsubscribe page can name it before the reader confirms. The server checks the token's signature when the page posts it; this only reads it.

Functions

f
readUnsubscribeCadence(token: string): DigestCadence | null

Returns the cadence token names, or null when it isn't an unsubscribe token at all.

src/utils/shared/relativeTime.ts

Shows a date as the time since it, beside every dump, comment, message and notification.

Functions

f
relativeTime(date: Date): string

Returns how long ago date was, as "3 minutes ago", in the current locale. It is computed at render, so the text only moves on when the caller re-renders. A month counts 30 days and a year 365. Every date shown is of something that happened, so one in the future, which only a clock ahead of this one can give, reads as "now".

src/utils/shared/format.ts

Formats a file dump's size and name for display, in the file fields, the previews and the dump creation dialog.

Functions

f
formatBytes(bytes: number): string

Returns bytes as a size in bytes, kilobytes or megabytes, where a kilobyte is 1024 bytes, in the app's language ("1.5 kB", "1,5 ko"). Intl names the units, so they follow the language without a catalog.

f
titleFromFileName(name: string): string

Returns the file name name as the title the dump creation dialog proposes. The extension goes, since the dump keeps its MIME type, and runs of underscores become spaces, so holiday_photo-01.final.jpg reads as holiday photo-01.final. A name that is only an extension (.gitignore) stays as it is.

src/utils/shared/loadState.ts

Gives every component that loads from the API the same states, so that ListState renders loading, error and empty alike everywhere.

Type Aliases

T
LoadState<L extends object> =
{ status: "loading"; }
| { status: "error"; error: string; }
| ({ status: "loaded"; } & L)

Held by a page or hook for what it fetches, as useFetchState returns it, and passed to ListState. L is what the loaded state carries besides its status, as { data: T } for useFetchState or a feed's page fields.

src/utils/shared/sound.ts

Plays the sounds of live events, which WSProvider triggers, and stores whether they are on. They are synthesised with Web Audio, so there's no file to ship: a soft blip for a chat message, and a rising two-note chime for a notification.

Functions

f
playChime(
kind: ChimeKind,
eventId: string
): void

Plays the chime for a live event, unless sounds are off, another tab already chimed for eventId, or a chat blip played less than 4 seconds ago. The caller must pass an id that is the same in every tab, such as the message's.

f
previewChime(kind: ChimeKind): void

Plays the chime kind whatever the setting, for the profile page's settings to preview it. It is called from a click, which unlocks the audio.

f
readSoundsEnabled(): boolean

Returns whether sounds are on in this browser, which they are unless turned off. playChime checks it on every play.

f
writeSoundsEnabled(enabled: boolean): void

Stores whether sounds are on, for useSoundsEnabled's setter.

Type Aliases

T
ChimeKind = "chat" | "notify"

Passed to playChime and previewChime to pick the sound.

src/utils/shared/deserialize.ts

Turns the JSON that the API, the socket and the browser's storage hand back, where dates are ISO strings (Wire), into the shared types with Dates. Every reader of those sources calls these on what it receives. The shape is not checked.

Functions

f
deserializePlaylistWithDumps(raw: Wire<PlaylistWithDumps>): PlaylistWithDumps

Returns raw as a PlaylistWithDumps, its dumps included.

f
hydrateDump(raw: unknown): Dump

Returns raw as a Dump, for useFeedCache, which reads it back untyped from sessionStorage.

f
hydrateNotification(raw: unknown): Notification

Returns raw as a Notification, for useFeedCache, which reads it back untyped from sessionStorage.

f
hydratePlaylist(raw: unknown): Playlist

Returns raw as a Playlist, for useFeedCache, which reads it back untyped from sessionStorage.

src/utils/shared/proxyImage.ts

Routes other sites' images through the API's image proxy, GET /api/remote/image, for Thumbnail and ThumbnailPlaceholder.

Functions

f
canProxyImage(url: string): boolean

Checks whether url points at another site, where proxying it could help. Thumbnail retries through the proxy only then, since the site's own files and thumbnails would fail the same way again.

f
proxyImageUrl(
url: string,
opts?: ProxyImageOptions
): string

Returns url routed through GET /api/remote/image, or unchanged when it needs no proxy. Only an HTTP URL is proxied by default, so that an HTTPS page doesn't block it as mixed content. A relative or localhost URL is never proxied.

Interfaces

I
ProxyImageOptions

Passed to proxyImageUrl by Thumbnail to retry a failed image.

  • force: boolean

    Proxies an HTTPS URL too, for a retry after a direct load failed, usually on hotlink protection.

src/utils/shared/thumbnailTint.ts

Picks the colour and the letter of ThumbnailPlaceholder. The CSS mixes the tint into the theme's surface (--thumb-tint) rather than painting it alone, so the text keeps its contrast whatever the site's colour and the theme.

Functions

f
initialsFor(
siteName: string | undefined,
url: string | undefined
): string

Returns the letter drawn when there's no favicon to show, the first letter or digit of siteName, or else of the hostname of url, or "?".

f
tintFor(unnamed 0: { accentColor?: string; url?: string; seed?: string; }): string

Returns the page's own colour when it declared a valid one, or else a hue drawn from seed or the hostname of url, so that every card gets a tint, even for a dump saved before colours were extracted.

src/utils/shared/earlyFetch.ts

Starts the first request of the page a visit lands on before that page's code has loaded, so that the two travel together rather than one after the other. main.tsx starts it as the app boots (startLandingRequest), and useAuth's authFetch hands its response to the page that asks for the same URL (takeEarly), in place of a request of its own.

Functions

f
clearEarlyRequests(): void

Forgets the requests started, between the tests' cases.

f
landingRequest(pathname: string): string | null

Returns the URL the page at pathname loads first, as that page builds it, or null for a page that isn't started early: the feed of every dump, on any of its tabs, asks for its first page, and a dump's page for the dump. The other pages wait for their code, as before.

f
startLandingRequest(pathname: string): void

Sends the request landingRequest names for pathname, with the stored session's token as authFetch would, and keeps its response for takeEarly. It never throws: a request that fails does so in the hands of the page that takes it, as its own would.

f
takeEarly(
url: string,
token: string | undefined
): Promise<Response> | undefined

Returns the response of the request started for url, once and to the session it was sent as, or undefined when there is none, or none recent enough: the caller then sends its own. authFetch asks it for every GET.

src/utils/shared/urls.ts

Builds the site's own URLs, for its pages and for the files, thumbnails and covers the API serves. Every link and media element of the frontend takes them from here, or from shared/paths.ts, whose page paths this wraps for dumps and playlists.

Functions

f
dumpFileUrl(
dump: VersionedDump,
token?: string | null
): string

Returns the URL of the uploaded file of dump. The caller passes the session's media token (useAuth's mediaToken) as token, which goes in the query string since a media element can't send a header. The server needs it for a private dump's file.

f
dumpThumbnailUrl(
dump: VersionedDump,
token?: string | null
): string

Returns the URL of the thumbnail of dump, with the media token as for dumpFileUrl. The server answers with the custom thumbnail if the dump has one, or else with one made from the file, a video still or a scaled-down image.

f
dumpUrl(
dump: OwnedItem,
view?: "edit"
): string

Returns the path of the page of dump, /@owner/dumps/<slug>, or of its editor (dumpPath). Its owner's name and its slug come with every dump the API sends.

f
dumpWaveformUrl(
dump: VersionedDump,
token?: string | null
): string

Returns the URL of the waveform of the audio file dump, which the server makes from the file, with the media token as for dumpFileUrl. The player draws it to seek on.

f
feedApiUrl(categorySlug?: string): string

Returns the URL of the feed's dumps, GET /api/dumps, every dump's or those of the category categorySlug; pageUrl adds the page to it. The index page loads it, and landingRequest names it to start it early.

f
imageProps(
url: string,
sizes: number | string
): { src: string; srcSet: string; sizes: string; }

Returns what an <img> needs to load url, one of the API's images, no larger than it is shown: a srcset of the copies the server makes (shared/image.ts), from which the browser takes the narrowest that fills sizes on its screen, and a src for whatever reads only that. sizes is the width the image is laid out at, in CSS pixels, or a sizes attribute when that width follows the viewport's. Avatar, Thumbnail and the playlists' covers spread it on their image.

f
isApiImageUrl(url: string): boolean

Checks whether url is an image the API serves, its own or one it proxies, which all take ?w= and answer a scaled-down copy. Another site's image, loaded from its own host, doesn't.

f
isAt(
item: OwnedItem | null | undefined,
username: string | undefined,
slug: string | undefined
): boolean

Checks whether item, a dump or a playlist, is the one a page's /@username/…/slug names, the name in any case. A page that already shows it needn't fetch it again when its URL changes to its current path.

f
normalizeUrl(input: string): string

Returns what a user typed as a link, trimmed, with https:// added when it has no http(s) scheme. The result is not checked to be a valid URL.

f
playlistImageUrl(
playlist: { id: string; updatedAt?: Date; isPublic?: boolean; },
token?: string | null
): string

Returns the URL of the cover of playlist, versioned on updatedAt, which a new cover bumps. A private playlist's URL carries the media token, as for dumpFileUrl. A public one doesn't, so that its URL is the same for everyone. A playlist without isPublic counts as public.

f
playlistUrl(
playlist: OwnedItem,
view?: "edit"
): string

Returns the path of the page of playlist, /@owner/playlists/<slug>, or of the page with its editor open (playlistPath).

f
userApiUrl(
user: { id: string; } | { username: string; },
...segments: string[]
): string

Returns the URL of /api/users/:key, or of what is under it, segments such as "dumps", slug, each encoded: every request about a user takes it from here. The key is user's id, or, when the caller has only the name a page's path gives, @ and that handle, which the API resolves as the page would, a former name or a remote user's included.

Type Aliases

T
VersionedDump = { id: string; fileSize?: number; createdAt?: Date; updatedAt?: Date; }

Passed to dumpFileUrl and dumpThumbnailUrl, usually as a whole Dump. Without the dates, the URL carries no current version and the browser revalidates it.

src/utils/shared/charCount.ts

Gives the character counters of the text fields and TextEditor their warning states.

Functions

f
charCountClass(
len: number,
max: number
): string

Returns the counter's modifier class for len characters out of max, with a leading space so that it appends to text-editor-count. It warns from 85% of max and turns to danger at max.

src/utils/shared/api.ts

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.

Functions

f
dataOr<T>(
res: Response,
fallback: T
): Promise<T>

Returns the payload of res, or fallback when the response says success: false. readFirstPage reads the profile pages' lists with it, and the profile its invitees, so that a list the server refuses shows empty rather than failing the page.

f
expectOk<T>(res: Response): Promise<T>

Returns the payload of res. In a form, the error it throws reaches useApiForm's submit, which shows it as the form's error.

f
friendlyFetchError(err: unknown): string

Returns the message to show for err, an error caught around a request. A TypeError means the request never reached the server (no network, CORS), and gets a message saying so. The server's own messages come through as they are.

f
parseAPIResponse<T>(raw: unknown): APIResponse<T>

Returns raw as an API envelope. Only the success field is checked, so the payload's type is the caller's word.

src/utils/shared/fetchPublicUser.ts

Fetches the user a profile page is about.

Functions

f
fetchPublicUser(
username: string,
signal: AbortSignal
): Promise<PublicUser>

Returns GET /api/users/@:username, for the profile, which fails as a whole when it does. username may be a former name, or the name in another case: the user comes back with their current one, which the profile puts in its URL. It is fetched afresh even when the lists come from useFeedCache.

src/utils/shared/visited.ts

Remembers in localStorage which dumps and playlists this browser has opened, for the unread dot on the dump and playlist cards.

Functions

f
isDumpVisited(id: string): boolean

Checks whether dump id was opened in this browser.

f
isPlaylistVisited(id: string): boolean

Checks whether playlist id was opened in this browser.

f
isRecent(createdAt: Date): boolean

Checks whether createdAt is less than 7 days old. Older items show no unread dot, since an old item not opened here isn't news.

f
markDumpVisited(id: string): void

Records dump id as opened. Nothing re-renders, so a card already on screen keeps its dot until it renders again.

f
markPlaylistVisited(id: string): void

Records playlist id as opened, as markDumpVisited does a dump.

src/components/playlist/PlaylistMembershipPanel.tsx

Renders the user's playlists as toggles of whether each holds a dump, in AddToPlaylistModal and in the last step of the dump form.

Functions

f
PlaylistMembershipPanel(unnamed 0: PlaylistMembershipPanelProps): React.JSX.Element

Renders the playlists as checkboxes, which the keyboard toggles too, and a button that opens PlaylistCreateForm to make one holding the dump.

Interfaces

I
PlaylistMembershipPanelProps

Passed to PlaylistMembershipPanel by AddToPlaylistModal and the dump form, which fetch the memberships and call the server on a toggle. The panel only shows them.

src/components/playlist/PlaylistDumpList.tsx

Renders the dumps of a playlist's page, which those who may edit it drag into a new order and remove.

Functions

f
PlaylistDumpList(unnamed 0: PlaylistDumpListProps): React.JSX.Element

Renders the dumps, or a line saying there are none. With the calls, each dump not fading has a drag handle and a remove button, and one fading an "Undo" during its cooldown.

Interfaces

I
PlaylistDumpListProps

Passed to PlaylistDumpList by PlaylistDetail, from usePlaylist. The calls are given only to those who may edit the playlist.

src/components/playlist/PlaylistCardList.tsx

Renders a list of playlists as cards.

Functions

f
PlaylistCardList(unnamed 0: PlaylistCardListProps): React.JSX.Element

Renders playlists as a .dump-feed of PlaylistCard, one to a row, or as a .playlist-grid of them, a card being its owner's when the playlist is the signed-in user's.

Interfaces

I
PlaylistCardListProps

Passed to PlaylistCardList by every list of playlist cards.

src/components/playlist/AddToPlaylistModal.tsx

Renders the modal in which the dump page's "add to playlist" button adds the dump to the user's playlists or takes it out of them.

Functions

f
AddToPlaylistModal(unnamed 0: AddToPlaylistModalProps): React.JSX.Element

Renders a PlaylistMembershipPanel in a modal, for the playlists usePlaylistMemberships loads and toggles.

Interfaces

I
AddToPlaylistModalProps

Passed to AddToPlaylistModal by the dump page.

src/components/playlist/PlaylistEditForm.tsx

Renders the form that edits a playlist's details, categories and cover, in place of the header of its page.

Functions

f
PlaylistEditForm(unnamed 0: PlaylistEditFormProps): React.JSX.Element

Renders the form that edits the playlist, in place of the page's header. It saves the details with PUT /api/playlists/:playlistId, sending only the title, slug, description and categories that changed, then a new cover, if one was picked, with PUT /api/playlists/:playlistId/cover.

Interfaces

I
PlaylistEditFormProps

Passed to PlaylistEditForm by PlaylistDetail.

src/components/playlist/PlaylistCreateForm.tsx

Renders the form that creates a playlist, in NewPlaylistButton's modal and in PlaylistMembershipPanel, with the playlist categories to file it under.

Functions

f
PlaylistCreateForm(unnamed 0: PlaylistCreateFormProps): React.JSX.Element

Renders the form that creates a playlist with POST /api/playlists, then adds dumpId to it when given. The playlist exists even if adding the dump fails, so onCreated runs either way and says which.

Interfaces

I
PlaylistCreateFormProps

Passed to PlaylistCreateForm by NewPlaylistButton and PlaylistMembershipPanel.

src/components/playlist/NewPlaylistButton.tsx

Renders the button that creates a playlist in a modal, on the user's own profile and playlists pages.

Functions

f
NewPlaylistButton(unnamed 0: NewPlaylistButtonProps): React.JSX.Element

Renders a button that opens PlaylistCreateForm in a modal, which closes once the playlist is created.

Interfaces

I
NewPlaylistButtonProps

Passed to NewPlaylistButton by the user's profile and playlists pages, which add the new playlist to their list through onCreated.

src/components/playlist/PlaylistSortTabs.tsx

Renders the tabs that put a list of playlists in another order, on the playlists page and on a profile's playlists tab, and the playlists page's tab of the viewer's own.

Functions

Interfaces

I
PlaylistSortTabsProps

Passed to PlaylistSortTabs by ProfilePlaylistsTab, whose tabs are PLAYLIST_SORTS, and by Playlists, whose PLAYLISTS_TABS add the viewer's own playlists for a signed-in user. Both keep the active one in their URL's query.

src/components/playlist/PlaylistCard.tsx

Renders the card that shows a playlist in profiles, search results, the playlists page and the dump page's "In collections" section.

Functions

f
PlaylistCard(unnamed 0: PlaylistCardProps): React.JSX.Element

Renders a playlist as a card that opens the playlist when clicked, in a new tab on a middle click (useCardLink), with its cover, or a tile of its own colour without one, the playlist categories it is filed under and when it last took a dump. Another user's recent playlist shows an unread dot until it is opened on this device. The card is an <li>, for a <ul>, whose class lays it out: a row in a .dump-feed, a tile in a .playlist-grid.

Interfaces

I
PlaylistCardProps

Passed to PlaylistCard by each list of playlists.

  • grid: boolean

    Set for a tile of the playlists page's grid, whose cover is larger.

  • isOwner: boolean

    Hides the owner link and the unread dot.

  • onDelete: () => void

    Shows a delete button, which calls it. The owner's playlists pages pass one that asks for confirmation first.

  • playlist: Playlist
  • privateBadgeOnly: boolean

    Used by dump pages to display "In collections" lists. Prevents the visibility badge from showing, except to the owner of a private playlist: public badges on every card there would say nothing.

src/components/playlist/PlaylistHeader.tsx

Renders the header of a playlist's page, with its cover, title, description, visibility, owner, dates and categories.

Functions

f
PlaylistHeader(unnamed 0: PlaylistHeaderProps): React.JSX.Element

Renders the playlist's details, with a follow button for anyone but its owner. Its editors change the title and the description in place, saved to PUT /api/playlists/:playlistId, and the rest in PlaylistEditForm, which takes the header's place while it is open.

Interfaces

I
PlaylistHeaderProps

Passed to PlaylistHeader by PlaylistDetail.

src/components/feed/FeedTabBar.tsx

Renders the tab bar of the index page, with the category switcher before the tabs.

Functions

f
FeedTabBar(unnamed 0: FeedTabBarProps): React.JSX.Element

Renders the "hot", "new" and "journal" tabs, and "followed" for a signed-in user. A tab leads to the same scope: /new for every dump, /music/new for a category, and the viewer's default tab to the bare / or /music. A tab is a view of the same page, so it takes the place of the current history entry, while picking a category, another page, adds one and keeps the tab and its view, from /music/follow/playlists to /cinema/follow/playlists. Index renders it twice, in the header and below it, and CSS shows one by screen width.

Interfaces

I
FeedTabBarProps

Passed to FeedTabBar by Index, from the URL it renders.

src/components/feed/PresenceRow.tsx

Shows who is online, as a row of avatars next to the index page's tabs.

Functions

f
PresenceRow(unnamed 0: PresenceRowProps): React.JSX.Element | null

Renders the online users from WSContext as overlapping avatars linking to their profiles, as many as the tabs leave room for, and a "+N" counter for the others. It renders nothing when nobody is online.

Interfaces

I
PresenceRowProps

Passed to PresenceRow by Index, which renders one in the header and one below it. Both selectors go to useRoomBeside.

src/components/feed/FeedFooter.tsx

Renders what follows the last item of an infinitely scrolled list.

Functions

f
FeedFooter(unnamed 0: FeedFooterProps): React.JSX.Element

Renders the sentinel while there is more to load, "Loading more…" while it loads, and "You've reached the end." once there is no more. The list renders it after a non-empty list only, since an empty list has its own message.

Interfaces

I
FeedFooterProps

Passed to FeedFooter by every list that loads its next page on scroll.

src/components/feed/JournalFeed.tsx

Renders the "journal" tab of the index page, which lays the feed out as a mosaic of tiles of several sizes.

Functions

f
JournalFeed(unnamed 0: MainFeedProps): React.JSX.Element

Renders the feed Index passes as blocks of JournalCard, laid out by composeJournal, followed by the infinite-scroll sentinel, or the loading, error or empty state. The votes come from WSContext.

src/components/feed/FollowedFeed.tsx

Renders the "followed" tab of the index page, which shows the dumps of the users and the playlists the signed-in user follows.

Functions

f
FollowedFeed(unnamed 0: FollowedFeedProps): React.JSX.Element

Renders the "follow" tab in two views, "from people" at /follow and "from playlists" at /follow/playlists, from GET /api/dumps?follow=users and GET /api/dumps?follow=playlists. Both load at once, so switching is instant, and each keeps its own pages, cached like the index's feed. Switching takes the place of the current history entry, as a tab does.

Interfaces

I
FollowedFeedProps

Passed to FollowedFeed by Index, which renders the tab for a signed-in user only. Unlike the other tabs, it loads its own dumps.

src/components/feed/DumpFeed.tsx

Renders the "hot" and "new" tabs of the index page, from the feed Index loads and passes down as MainFeedProps.

Functions

f
DumpFeed(unnamed 0: DumpFeedProps): React.JSX.Element

Renders the "hot" and "new" tabs as a DumpCardList, sorted on the client, followed by the infinite-scroll sentinel, or the loading, error or empty state. Index is its only user.

Interfaces

I
DumpFeedProps

Passed to DumpFeed by Index for the "hot" and "new" tabs. It adds the active tab, as order, to the state all tabs share.

I
MainFeedProps

Passed by Index to whichever of the "hot", "new" and "journal" tabs is showing. The tabs share this one state, so switching tabs doesn't refetch.

Type Aliases

T
DumpFeedState = FetchState<PagedList<Dump>>

Held by usePagedList for Index's feed and for each section of FollowedFeed.

src/components/player/PlayerIcons.tsx

Draws the icons of the media controls, for MediaPlayer and the dump page's audio preview (FilePreview). They take the text colour, so the buttons' CSS colours them.

Functions

f
IconFullscreen(): React.JSX.Element

Renders the icon of a video's fullscreen button.

f
IconPause(): React.JSX.Element

Renders the pause icon, the play button's other state.

f
IconPlay(): React.JSX.Element

Renders the play icon, nudged right so the triangle looks centred.

f
IconVolume(unnamed 0: IconVolumeProps): React.JSX.Element

Renders the mute button's icon, crossed when muted.

Interfaces

I
IconVolumeProps

Passed to IconVolume by MediaPlayer's mute button.

src/components/player/Scrubber.tsx

Renders the track MediaPlayer seeks on: an audio file's waveform, or a plain bar for a stream, a video, or audio whose waveform isn't in yet. The dump page's audio preview shows one too, beside its cover.

Functions

f
Scrubber(unnamed 0: ScrubberProps): React.JSX.Element

Renders a slider over the media's timeline. A pointer seeks where it lets go, showing where it is while held; arrows seek KEY_STEP seconds, Home and End to either end. The played part of the waveform is the same path clipped to the playhead by a nested <svg>, so it moves smoothly within a bar.

Interfaces

I
ScrubberProps

Passed to Scrubber by MediaPlayer and FilePreview. Times are in seconds.

src/components/player/GlobalPlayer.tsx

Renders the player docked at the bottom of every page. App mounts it once, beside the routes, so playback goes on from page to page.

Functions

f
GlobalPlayer(): React.JSX.Element | null

Renders what PlayerContext plays, with a link to its dump: a MediaPlayer for a file or a stream, the provider's iframe for an embed, and the tracklist of the queue or of a YouTube playlist. It renders nothing while nothing plays.

src/components/player/MediaPlayer.tsx

Renders an audio or video file with the site's own controls. GlobalPlayer plays files and streams in it; the file previews of the dump pages and of the dump form play a file in it.

Functions

f
MediaPlayer(unnamed 0: MediaPlayerProps): React.JSX.Element

Renders a media element with the site's own controls. Audio seeks on its waveform when it has one; video gets a slim bar, a fullscreen button, and controls that hide while it plays. Whether it plays is read from the element's own events, so a media key or the OS's controls show too. The volume carries over to the next MediaPlayer, which matters since GlobalPlayer mounts a new one for each track.

Interfaces

I
MediaPlayerProps

Passed to MediaPlayer by GlobalPlayer, which wires the callbacks and the handles to PlayerContext, and by the file previews, which pass only the media.

src/components/form/InlineEdit.tsx

Renders the editing of a text in place, with no page or form of its own. InlineEdit shows the text and opens the field over it: a dump's title and note, a playlist's title and description, a profile's description and email. InlineEditField is that field alone, for a text that opens it with an action of its own: a comment's Edit, a chat message's ✎.

Functions

f
InlineEdit(unnamed 0: InlineEditProps): React.JSX.Element

Renders children, which a click on them, or the ✎ button after them, swaps for a field holding value, with Save and Cancel under it. The ✎ shows on hover and on keyboard focus. A click on a link in the text, or one that ends a selection, follows the link or keeps the selection instead. Closing the field gives the focus back to the ✎ when the field had it.

f
InlineEditField(unnamed 0: InlineEditFieldProps): React.JSX.Element

Renders the field holding value, focused with the caret at its end, and Save and Cancel under it. Enter saves a line, Ctrl/Cmd+Enter a Markdown text, Escape leaves. Saving is refused while the text is empty and required, or longer than maxLength. It is mounted afresh on each opening, so that it starts from value.

Interfaces

I
InlineEditFieldProps

Passed to InlineEditField by InlineEdit, and by the components that open it with an action of their own, CommentNode and ChatMessageItem, in place of the text while it is edited.

I
InlineEditProps

Passed to InlineEdit by the component showing the text, which renders it as children and saves it in onSave.

src/components/form/SubmitButton.tsx

Renders the submit button of the forms built with useApiForm, which shows the submit in progress.

Functions

f
SubmitButton(unnamed 0: SubmitButtonProps): React.JSX.Element

Renders a submit button that is disabled while the form submits, so that a second click can't post twice. It reads the form's state, so it must sit under a react-hook-form FormProvider.

Interfaces

I
SubmitButtonProps

Passed to SubmitButton by a form, usually inside FormActions.

src/components/form/DeleteButton.tsx

Renders the destructive action of an edit form's bar, in DumpEdit and PlaylistEditForm.

Functions

f
DeleteButton(unnamed 0: DeleteButtonProps): React.JSX.Element

Renders a compact "Delete" button, at the start of the bar's .form-actions. A bar too narrow for it beside Cancel and Save shows its icon instead (.form-delete-icon).

Interfaces

I
DeleteButtonProps

Passed to DeleteButton by an edit form.

src/components/form/SegmentedField.tsx

Renders a form's choice among a few values as a row of buttons, and the public/private toggle that the dump and playlist forms build on it.

Functions

f
SegmentedField<T extends FieldValues, V>(unnamed 0: SegmentedFieldProps<T, V>): React.JSX.Element

Renders a Segmented bound to the field name, under its label. It must sit under a react-hook-form FormProvider.

f
VisibilityToggle<T extends FieldValues>(unnamed 0: VisibilityToggleProps<T>): React.JSX.Element

Renders a Public/Private toggle on a boolean field that is true for public. A dump's API takes isPrivate instead, so the dump forms bind the toggle to an isPublic field and invert it on submit.

Interfaces

I
VisibilityToggleProps

Passed to VisibilityToggle by a dump or playlist form.

src/components/form/FileField.tsx

Renders the file field of the dump forms, a FileDropZone bound to react-hook-form.

Functions

f
FileField<T extends FieldValues>(unnamed 0: FileFieldProps<T>): React.JSX.Element

Renders the drop zone with its label and the field's error. It must sit under a react-hook-form FormProvider, such as one given useApiForm's result.

Interfaces

I
FileFieldProps

Passed to FileField by a dump form. The field it names holds the File itself, or null.

src/components/form/TextField.tsx

Renders a one-line text field bound to react-hook-form, for the account forms, a dump's title and a playlist's.

Functions

f
TextField<T extends FieldValues>(unnamed 0: TextFieldProps<T>): React.JSX.Element

Renders the input with its label and the field's error. It must sit under a react-hook-form FormProvider, such as one given useApiForm's result.

Interfaces

I
TextFieldProps

Passed to TextField by a form that holds a string field.

src/components/form/FormActions.tsx

Renders the row of buttons that ends a form, in the pages and dialogs that edit something.

Functions

f
FormActions(unnamed 0: FormActionsProps): React.JSX.Element

Renders the Cancel button, if any, then the children, aligned right. The Cancel button is type="button", so it never submits the form.

Interfaces

I
FormActionsProps

Passed to FormActions by a form, with its submit button as child.

src/components/form/FileDropZone.tsx

Renders the zone that takes a dump's file, by drop or by picker. FileField binds it to a form.

Functions

f
FileDropZone(unnamed 0: FileDropZoneProps): React.JSX.Element

Renders a zone that takes a dropped file, or opens the file picker on a click or Enter. Once a file is chosen, it shows its name and size and a button to remove it. Only the first of several dropped files is kept.

Interfaces

I
FileDropZoneProps

Passed to FileDropZone by FileField, from the form's state. The zone keeps no file of its own.

src/components/form/RichTextField.tsx

Renders a TextEditor bound to react-hook-form, for the forms that take a Markdown text (a dump's note, a playlist's description).

Functions

f
RichTextField<T extends FieldValues>(unnamed 0: RichTextFieldProps<T>): React.JSX.Element

Renders the editor with its label and the field's error. It must sit under a react-hook-form FormProvider, such as one given useApiForm's result.

Interfaces

I
RichTextFieldProps

Passed to RichTextField by a form that holds a string field.

src/components/form/SlugField.tsx

Renders the slug of a dump or a playlist, the last segment of its page's path, in their edit forms. A slug changes only from here: a new title proposes one, which saving applies, and the old one stays an alias of the item on the server.

Functions

f
SlugField<T extends FieldValues>(unnamed 0: SlugFieldProps<T>): React.JSX.Element

Renders the slug's input after its path, and its error. Until the user types in it, it follows the title, proposing slugify(title), or the saved slug when the title slugifies as the saved one does. It must sit under a react-hook-form FormProvider.

Interfaces

I
SlugFieldProps

Passed to SlugField by DumpEdit's and PlaylistEditForm's forms, which send the slug only when it differs from saved.slug.

src/components/form/FormError.tsx

Renders the error of a failed submit, for the forms built with useApiForm.

Functions

f
FormError(unnamed 0: FormErrorProps): React.JSX.Element | null

Renders the form's root error as an ErrorCard, or nothing without one. useApiForm's submit sets that error when the request fails. It must sit under a react-hook-form FormProvider.

Interfaces

I
FormErrorProps

Passed to FormError by a form, to say what failed.

src/components/editor/MentionDropdown.tsx

Renders the @mention suggestions that TextEditor portals to <body>. useMentionAutocomplete fetches them and tracks the highlighted one.

Functions

f
MentionDropdown(unnamed 0: MentionDropdownProps): React.JSX.Element | null

Renders the matching users as a listbox, scrolled to keep the highlighted one in view, or nothing when none match. The keyboard stays in the textarea.

Interfaces

I
MentionDropdownProps

Passed to MentionDropdown by TextEditor, from the state of useMentionAutocomplete.

src/components/editor/EmojiPickerPopup.tsx

Renders the emoji picker that TextEditor portals to <body>, built on frimousse. TextEditor owns its state and its keyboard handling.

Functions

f
EmojiPickerPopup(unnamed 0: EmojiPickerPopupProps): React.JSX.Element

Renders the picker in the current locale, with the skin tone last picked on this device. It fetches its emoji data from the site itself, where the Vite build copies it (VITE_EMOJIBASE_URL).

Interfaces

I
EmojiPickerPopupProps

Passed to EmojiPickerPopup by TextEditor, which keeps the refs to detect outside presses, focus the search and step the keyboard into the grid.

src/components/editor/Markdown.tsx

Renders the Markdown that TextEditor writes, wherever a dump's note, a comment, a chat message or a description is shown.

Interfaces

I
MarkdownProps

Passed to Markdown by any component that shows a user's text.

  • children: string
  • className: string
  • everyone: boolean

    Shows @everyone as a mention, from a chat message's mentionsEveryone. Without it, @everyone is plain text: anyone may type it, and it reaches no one.

  • inline: boolean

    Drops the images, for a line-clamped teaser in a card, where an image would break the layout. The full text is one click away.

Variables

v
Markdown: React.MemoExoticComponent<(props: MarkdownProps) => React.JSX.Element>

Renders GFM with @mentions as profile links and single newlines kept. It is memoized because parsing runs the whole remark pipeline, and the text rarely changes when its parent re-renders.

src/components/editor/TextEditor.tsx

Provides the Markdown textarea behind every text a user writes. Forms reach it through RichTextField, while the comment and chat composers and the inline editors of the profile and playlist pages use it directly.

Interfaces

I
TextEditorHandle

Exposed through the ref of TextEditor, so that the component that opens an editor (a reply, an edit, the chat composer) can focus it.

I
TextEditorProps

Passed to TextEditor by the component that owns the text, which keeps it in its state (the editor is controlled).

Variables

v
TextEditor: React.ForwardRefExoticComponent<TextEditorProps & React.RefAttributes<TextEditorHandle>>

Renders a textarea that completes @mentions and :emoji, opens an emoji picker from its button, and counts characters near maxLength. A pasted or dropped image is uploaded to POST /api/attachments with the session's token and inserted as Markdown, so the editor must sit under AuthProvider. The textarea is read-only while an upload runs.

src/components/federation/PeerManager.tsx

Renders the list of the other gerbeur instances an admin trusts or blocks, on /manage, for those who hold federation:manage. Where users of other instances don't sign in, only blocking is left: it keeps this instance's users from signing in to one.

Functions

f
PeerManager(): React.JSX.Element

Renders each listed instance with its status, which a click changes (PUT /api/federation/peers), and a button that takes it off the lists (DELETE /api/federation/peers/:host), then a field that adds one. The server checks that it answers as a gerbeur, and the permission again. Without FEDERATION_INCOMING it lists and adds only blocked instances; the trusted ones are kept for when it is back.

src/components/federation/GrantList.tsx

Renders the other gerbeur instances the signed-in user let their account sign in to, on /settings.

Functions

f
GrantList(): React.JSX.Element

Renders each instance (GET /api/me/grants), when it last signed the user in, and a button that withdraws it (DELETE /api/me/grants/:host), which signs them out there; signing in there again asks them again.

src/components/federation/FederatedSignIn.tsx

Renders the way in for users of another gerbeur instance, under the login form and, with the invite, under the register form.

Functions

f
FederatedSignIn(unnamed 0: FederatedSignInProps): React.JSX.Element

Renders a button per trusted instance (GET /api/federation/peers) and a field for any other's address. Either starts the sign-in (POST /api/federation/logins) and leaves for that instance's /authorize page, which sends the user back to /federation/callback.

Interfaces

I
FederatedSignInProps

Passed to FederatedSignIn by UserLogin and UserRegister.

  • inviteToken: string

    The invite the register page was opened with, which lets a user of any instance join; the login page has none, and offers the trusted instances.

src/components/dump/DumpCardList.tsx

Renders dumps as cards wired to the live votes, one at a time or as a list.

Functions

f
DumpCardList(unnamed 0: DumpCardListProps): React.JSX.Element

Renders dumps as a .dump-feed of LiveDumpCard.

f
LiveDumpCard(unnamed 0: LiveDumpCardProps): React.JSX.Element

Renders dump as a DumpCard, an <li>. The vote counts and calls come from WSContext, a dump missing from its counts showing its fetched voteCount. Voting takes a signed-in user, and a card is its owner's when it is the signed-in user's own dump.

Interfaces

I
DumpCardListProps

Passed to DumpCardList by every plain list of dump cards.

I
LiveDumpCardProps

Passed to LiveDumpCard by DumpCardList, and by PlaylistDumpList, which wraps each card in its drag handle and buttons.

src/components/dump/JournalCard.tsx

Renders a dump as a tile of the "journal" tab's mosaic.

Interfaces

I
JournalCardProps

Passed to JournalCard by JournalFeed, with the vote state Index takes from WSContext, as for DumpCard.

Variables

v
JournalCard: React.MemoExoticComponent<(props: JournalCardProps) => React.JSX.Element>

Renders a dump as an image, a pull-quote or a text tile, depending on whether it has a thumbnail or a note. Clicking an image tile plays its media when it can, a link's or an uploaded audio or video file, and pauses a file once it plays; any other tile opens the dump. It is memoized like DumpCard, so its vote callbacks must be stable.

src/components/dump/create/LocalFilePreview.tsx

Previews the file picked in the dump creation modal, before it is uploaded.

Functions

f
LocalFilePreview(unnamed 0: LocalFilePreviewProps): React.JSX.Element | null

Renders an image, video or audio file from a blob URL, which it revokes when file changes or it unmounts. Any other file renders nothing.

Interfaces

I
LocalFilePreviewProps

Passed to LocalFilePreview by DumpCreateModal.

src/components/dump/create/DumpCreateModal.tsx

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.

Functions

f
DumpCreateModal(unnamed 0: DumpCreateModalProps): React.JSX.Element

Renders the dump creation modal. The first panel takes a link, previewed through GET /api/remote/preview and checked against GET /api/dumps?url=, or a file. The second takes the "why", categories and visibility, and posts to POST /api/dumps. The third adds the new dump to playlists. The new dump is handed to WSContext's injectDump so the feeds show it at once. Everything but the file is saved as a draft (utils/dump/draft.ts) until it is posted.

Interfaces

I
DumpCreateModalProps

Passed to DumpCreateModal by the button that opens it. The parent unmounts the modal on onClose, which the modal also calls after a confirmed dismissal.

src/components/dump/create/StepIndicator.tsx

Shows which panel of the dump creation modal is open, and declares the panels in the order they are walked.

Functions

f
StepIndicator(unnamed 0: StepIndicatorProps): React.JSX.Element

Renders the three panels as numbered steps, the passed ones ticked.

Interfaces

I
StepIndicatorProps

Passed to StepIndicator by DumpCreateModal.

Type Aliases

T
Step = "what" | "details" | "playlist"

A panel of DumpCreateModal, in the order they are walked. Categories and visibility have a panel of their own, "details", because behind a disclosure they went untouched.

src/components/dump/create/TitleField.tsx

Renders the title field of the dump creation modal, read-only until the poster chooses to edit it.

Functions

f
TitleField(unnamed 0: TitleFieldProps): React.JSX.Element

Renders the title of a link or file dump. The suggested title, from the link's preview or the file's name, stays read-only until the ✎ button is pressed, so stray typing can't change it.

Interfaces

I
TitleFieldProps

Passed to TitleField by DumpCreateModal, which owns the editing state: it marks the title as the poster's own on onEdit.

src/components/dump/create/DuplicateNotice.tsx

Tells the poster, in the dump creation modal, that the link they typed was already dumped.

Functions

f
DuplicateNotice(unnamed 0: DuplicateNoticeProps): React.JSX.Element

Renders the "already dumped" hint. It doesn't block posting, since a repost is sometimes on purpose.

Interfaces

I
DuplicateNoticeProps

Passed to DuplicateNotice by DumpCreateModal, with the matches of GET /api/dumps?url=.

src/components/dump/NewDumpButton.tsx

Renders the button that opens the dump creation modal, on the user's own profile and dumps pages.

Functions

f
NewDumpButton(): React.JSX.Element

Renders a "+ New dump" button, as NewPlaylistButton does for playlists, that opens DumpCreateModal. The modal hands the new dump to WSContext itself, so the button takes no callback.

src/components/dump/DumpCard.tsx

Renders the card that shows a dump in the feeds, search results, profiles and playlists.

Interfaces

I
DumpCardProps

Passed to DumpCard by each list of dumps. The vote state comes from the parent, usually WSContext's passed down through the feed's props.

Variables

v
DumpCard: React.MemoExoticComponent<(props: DumpCardProps) => React.JSX.Element>

Renders a dump as a card that opens the dump when clicked. A ctrl-click or middle click opens it in a new tab (useDumpCardLink). The card is an <li>, for a <ul>. It is memoized because feeds re-render on every live event, so its vote callbacks must be stable (WSContext's are).

src/components/dump/RichContentCard.tsx

Shows a link's preview, as scraped by the server, in a card or on a dump's page, and plays it in the global player when it can.

Functions

f
RichContentCard(unnamed 0: RichContentCardProps): React.JSX.Element

Renders a link's preview. Its thumbnail plays the media in the global player when it can be played, and otherwise opens the link in a new tab. Full size, it also shows the site, title and description, linking out.

Interfaces

I
RichContentCardProps

Passed to RichContentCard by DumpCard (compact), Dump, DumpEdit and DumpCreateModal, the last two without a dump to play from.

src/components/dump/FilePreview.tsx

Shows an uploaded file by its type, in a card or on a dump's page. Audio and video play in the global player or inline.

Functions

f
FilePreview(unnamed 0: FilePreviewProps): React.JSX.Element

Renders an uploaded file by its MIME type. Compact, it is a thumbnail or an icon. Full size, it is the image, a player, the embedded PDF, or a download link for anything else. File URLs carry the viewer's media token, so private files load too.

Interfaces

I
FilePreviewProps

Passed to FilePreview by DumpCard (compact), by Dump (global) and by DumpEdit (neither).

  • compact: boolean

    Shows a card's thumbnail, whose play button uses the global player.

  • dump: Dump

    Expected to be a file dump. A link dump has no fileMime, so it shows as a download.

  • global: boolean

    Plays full-size audio and video in the global player, not inline.

src/components/profile/ProfileDumpsTab.tsx

Renders a profile's "dumps" or "upvoted" tab, the whole list of the user's dumps or of those they upvoted, which a category may narrow.

Functions

f
ProfileDumpsTab(unnamed 0: ProfileDumpsTabProps): React.JSX.Element

Renders the list, with the category menu in its title row; the category picked is the URL's ?category=, which switching to the other list keeps and to any other tab drops. The owner's own dumps get a "New dump" button.

Interfaces

src/components/profile/ProfileManageTab.tsx

Renders a profile's "manage" tab, /@name/manage, where those who hold user:manage change another user's role and name. The site's own tools are the /manage page's.

Functions

f
ProfileManageTab(unnamed 0: ProfileManageTabProps): React.JSX.Element

Renders the user's role and name. A rename here has no rate limit, and the profile's URL then takes the new name. A user from another instance is named there, so their name is only shown.

Interfaces

I
ProfileManageTabProps

Passed to ProfileManageTab by UserPublicProfile, which shows the tab on another user's profile to those who hold user:manage.

src/components/profile/ProfileHeader.tsx

Renders the header of a user's profile, where the signed-in user also edits their own avatar and email.

Functions

f
ProfileHeader(unnamed 0: ProfileHeaderProps): React.JSX.Element

Renders the avatar, username, role and inviter, and for a user from another instance, a link to their profile there. Visitors get a follow button. On their own profile, users change their avatar (PUT /api/me/avatar) and email (PATCH /api/me), unless their account comes from another instance, and invite someone; their settings, the site's tools and logging out are in the header's avatar menu.

Interfaces

I
ProfileHeaderProps

Passed to ProfileHeader by UserPublicProfile, once loaded.

src/components/profile/SettingRow.tsx

Renders one labelled setting of the settings page and the profile's "manage" tab.

Functions

f
SettingRow(unnamed 0: SettingRowProps): React.JSX.Element

Renders label beside children, as a row of the .profile-appearance-grid its caller wraps the rows in.

Interfaces

I
SettingRowProps

Passed to SettingRow by Settings, ProfileManageTab and RoleSwitcher.

src/components/profile/ProfileInviteesTab.tsx

Renders the "invitees" tab of a profile, which shows whom the user invited, and whom they invited in turn.

Functions

Interfaces

I
ProfileInviteesTabProps

Passed to ProfileInviteesTab by UserPublicProfile.

src/components/profile/RoleSwitcher.tsx

Shows a user's role on their profile, and lets those who hold user:manage change it.

Functions

f
RoleChip(unnamed 0: { role: Role; }): React.JSX.Element | null

Renders the role badge next to the username in ProfileHeader, for moderators and admins only.

f
RoleSwitcher(unnamed 0: RoleSwitcherProps): React.JSX.Element

Renders the user's role as a Segmented whose clicks save it to PATCH /api/users/:id/role, which checks the permission again.

Interfaces

I
RoleSwitcherProps

Passed to RoleSwitcher by ProfileManageTab, which shows it only to those who hold user:manage, on another user's profile.

src/components/profile/ProfilePlaylistsTab.tsx

Renders a profile's "playlists" tab, the playlists the user made, which a playlist category may narrow. Those they follow are the "follow" tab's.

Functions

f
ProfilePlaylistsTab(props: ProfilePlaylistsTabProps): React.JSX.Element

Renders the tab's list for the playlist category the URL's ?category= picks, or every playlist without one, in the order its ?sort= names. Each category's list in each order is another, which the tab mounts afresh.

Interfaces

I
ProfilePlaylistsTabProps

Passed to ProfilePlaylistsTab by UserPublicProfile.

src/components/profile/ProfileFollowTab.tsx

Renders a profile's "follow" tab, which shows the users the user follows at /@name/follow and the playlists at /@name/follow/playlists.

Functions

f
ProfileFollowTab(unnamed 0: ProfileFollowTabProps): React.JSX.Element

Renders the two views under a row of tabs that switch between their paths, each view loading its list when first shown.

Interfaces

I
ProfileFollowTabProps

Passed to ProfileFollowTab by UserPublicProfile.

src/components/profile/ChangePasswordModal.tsx

Renders the password change dialog, which the settings page opens.

Functions

f
ChangePasswordModal(unnamed 0: ChangePasswordModalProps): React.JSX.Element

Renders the form that changes the user's password, given the current one. The change signs out every other session, in every browser.

Interfaces

I
ChangePasswordModalProps

Passed to ChangePasswordModal by UserPublicProfile.

src/components/profile/ProfileDescription.tsx

Renders a profile's description, which the signed-in user edits in place on their own profile.

Functions

f
ProfileDescription(unnamed 0: ProfileDescriptionProps): React.JSX.Element | null

Renders the description in Markdown, or nothing when there is none. On their own profile, users get a "Who am I?" placeholder instead, and edit it in place, saved to PATCH /api/me. Other browsers see the change through the socket's user_updated event.

Interfaces

I
ProfileDescriptionProps

Passed to ProfileDescription by UserPublicProfile.

src/components/profile/ProfileSection.tsx

Renders a titled section of a profile page.

Functions

f
ProfileSection(unnamed 0: ProfileSectionProps): React.JSX.Element

Renders a .profile-section: its title row, children, then a "View all →" link to viewAllHref.

Interfaces

I
ProfileSectionProps

Passed to ProfileSection by the profile's tabs, and by the settings and manage pages.

src/components/profile/UsernameField.tsx

Renders the field that renames a user, on a profile's "manage" tab, which those with user:manage see: users can't rename themselves.

Functions

f
UsernameField(unnamed 0: UsernameFieldProps): React.JSX.Element

Renders the name in a text input with a "Rename" button, which is enabled once the name differs.

Interfaces

I
UsernameFieldProps

Passed to UsernameField by ProfileManageTab.

src/components/profile/ProfileOverviewTab.tsx

Renders a profile's overview, its bare /@name, which shows the first page of the user's dumps beside those they upvoted.

Functions

f
ProfileOverviewTab(unnamed 0: ProfileOverviewTabProps): React.JSX.Element

Renders the first page of both lists, from useUserDumpFeed, each linking to its tab. The owner also gets a "New dump" button.

Interfaces

I
ProfileOverviewTabProps

Passed to ProfileOverviewTab by UserPublicProfile.

src/components/profile/ProfileOwnActions.tsx

Renders the action of the signed-in user's own profile header, which invites someone.

Functions

f
ProfileOwnActions(): React.JSX.Element

Renders the "Invite someone" button, which opens a panel with an invite link. Each opening of the panel creates a new single-use link, for /register.

src/components/category/CategoryChips.tsx

Renders the categories a dump or playlist is filed under, on its card and its page.

Functions

f
CategoryChips(unnamed 0: CategoryChipsProps): React.JSX.Element | null

Renders each category ids names as a chip linking to its page, a dump category's feed or a playlist category's playlists, in the categories' order. Ids of categories since deleted are skipped, and it renders nothing when none is left.

Interfaces

I
CategoryChipsProps

Passed to CategoryChips by DumpCard and Dump for a dump, and by PlaylistCard and PlaylistHeader for a playlist.

src/components/category/CategorySwitcher.tsx

Renders the menu that narrows a list of dumps or playlists to a category, on the index and playlists pages, on a profile's lists and on the search page.

Functions

f
CategorySwitcher(unnamed 0: CategorySwitcherProps): React.JSX.Element | null

Renders a button naming the category shown, "All" without one, that opens the menu of every category of kind. It renders nothing while the site has none.

Interfaces

I
CategorySwitcherProps

Passed to CategorySwitcher by FeedTabBar, ProfileDumpsTab, ProfilePlaylistsTab, Playlists and Search, which keep the category in their URL.

src/components/category/CategoryManager.tsx

Renders the editor of the site's categories of one kind, dump or playlist, on /manage for a user with the category:manage permission.

Functions

f
CategoryManager(unnamed 0: CategoryManagerProps): React.JSX.Element

Renders the categories of kind as chips that open an inline editor, and a "+" chip that creates one. Only one editor is open at a time. A change refetches the app-wide list, so the category switchers follow. Manage shows it only to those who hold category:manage, and the server checks it again.

Interfaces

I
CategoryManagerProps

Passed to CategoryManager by Manage, once for each kind.

src/components/category/CategorySelect.tsx

Renders the category picker of the dump creation dialog and the dump editor, and of the playlist forms.

Functions

f
CategorySelect(unnamed 0: CategorySelectProps): React.JSX.Element | null

Renders the categories of kind as toggle chips, any number of which may be on. It renders nothing while the site has none.

Interfaces

I
CategorySelectProps

Passed to CategorySelect by DumpCreateModal and DumpEdit, and by PlaylistCreateForm and PlaylistEditForm, which hold the selection and send it as the dump's or playlist's categoryIds.

src/components/layout/PageShell.tsx

Renders the frame of every page but the index and search pages, which render AppHeader themselves.

Functions

f
PageShell(unnamed 0: PageShellProps): React.JSX.Element

Renders AppHeader, then the page's content as the <main>.

Interfaces

I
PageShellProps

Passed to PageShell by a page, around its content.

src/components/layout/UserMenu.tsx

Renders the avatar menu that leads the header's icon row at every width: it holds the profile link, the playlists page (the header's text link leads there too, at the widths that have room for it), the community page, the settings, the site's tools, and logging out.

Functions

f
UserMenu(unnamed 0: UserMenuProps): React.JSX.Element

Renders the signed-in user's avatar, which opens a menu with links to their profile, /playlists, /community and /settings, to /manage for those who hold category:manage or federation:manage, and a Log out button. A click outside or Escape closes it.

Interfaces

I
UserMenuProps

Passed to UserMenu by AppHeader, for a signed-in user.

src/components/layout/DumpFab.tsx

Renders the floating "new dump" button, which App mounts once over every page.

Functions

f
DumpFab(): React.JSX.Element | null

Renders the button that opens the dump creation dialog once the header, and its "+ Dump" button, have scrolled out of view. It renders nothing when signed out. The stylesheet lifts it above the global player.

src/components/layout/SearchBar.tsx

Renders the search form of the header.

Functions

Interfaces

I
SearchBarProps

Passed to SearchBar by AppHeader, which holds the query and decides where a search leads.

src/components/layout/PageHeader.tsx

Renders the title row of the pages the user menu and the bell lead to: settings, manage, notifications and every playlist.

Functions

f
PageHeader(unnamed 0: PageHeaderProps): React.JSX.Element

Renders the page's h1 with its icon, then children, as the first block of a PageShell: a narrow one's PageGroups follow it, or the playlists page's grid.

Interfaces

I
PageHeaderProps

Passed to PageHeader by Settings, Manage, Notifications and Playlists.

src/components/layout/PageGroup.tsx

Renders one labelled group of a page under a PageHeader: a settings section, the category manager, a day of notifications.

Functions

f
PageGroup(unnamed 0: PageGroupProps): React.JSX.Element

Renders a <section> headed by label as its h2.

Interfaces

I
PageGroupProps

Passed to PageGroup by Settings, Manage and Notifications.

src/components/layout/NavIcons.tsx

Draws the line icons of the header's controls (search, its ✕, chat and the bell) and of ChatFab. They stroke in the text colour; the shapes marked nav-icon-tint also fill with --nav-icon-tint, which the header sets to a wash of the accent and the floating button leaves unset. Themes set the line's weight and ends with --nav-icon-stroke and --nav-icon-linecap.

Functions

f
IconBell(props: NavIconProps): React.JSX.Element

Renders a bell, for NotificationBell.

f
IconChat(props: NavIconProps): React.JSX.Element

Renders a speech bubble, for ChatButton and ChatFab.

f
IconClose(props: NavIconProps): React.JSX.Element

Renders the ✕ the search toggle turns into while the search is open.

f
IconSearch(props: NavIconProps): React.JSX.Element

Renders a magnifier, for the header's search toggle and the search bar.

Interfaces

I
NavIconProps

Passed to every icon, so a caller can add its own class (the bell rings).

src/components/layout/AppHeader.tsx

Renders the header at the top of every page, through PageShell or directly from the index and search pages.

Functions

f
AppHeader(unnamed 0: AppHeaderProps): React.JSX.Element

Renders the site's header. It holds the search, which stays open on the results page and slides open under the header on small screens, the link to the playlists page, the signed-in user's menu and buttons or a guest's "Log in", and a banner while live updates are down. It also hosts the dump creation dialog that its "+ Dump" button opens.

Interfaces

I
AppHeaderProps

Passed to AppHeader by the index page. The other pages leave these props out.

src/components/layout/ChatFab.tsx

Renders the floating chat button, which App mounts once over every page.

Functions

f
ChatFab(): React.JSX.Element | null

Renders the chat button that floats above DumpFab once the header, and its ChatButton, have scrolled out of view. It renders nothing when signed out.

src/components/layout/PageError.tsx

Renders the error a page shows in place of its content when what it loads fails or doesn't exist.

Functions

f
PageError(unnamed 0: PageErrorProps): React.JSX.Element

Renders an error card as the whole page, under the header, so the user can still navigate away.

Interfaces

I
PageErrorProps

Passed to PageError by a page whose load failed.

src/components/layout/NotificationBell.tsx

Renders the notification bell of the header, for signed-in users.

Functions

f
NotificationBell(): React.JSX.Element

Renders the bell that leads to /notifications. While the socket counts unread notifications it fills with the accent and wears a dot; the count itself is in its label and the tab's title. It rings when the socket pushes a new one.

src/components/layout/Marquee.tsx

Renders the GeoCities style's marquee: the community feed crawling across the page under the header, as a <marquee> did on a homepage of that era. AppHeader renders it on every page. It draws nothing in any other style, nor for a visitor, the feed being for signed-in users.

Functions

f
Marquee(): React.JSX.Element | null

Renders the marquee for a signed-in user in the GeoCities style, an empty band until the feed is loaded, so that the page under it doesn't move when it comes.

src/components/layout/ChatButton.tsx

Renders the chat button of the header, for signed-in users.

Functions

f
ChatButton(): React.JSX.Element

Renders the button that opens the chat, filled with the accent and dotted while the socket counts unread messages; the count itself is in its label. ChatFab takes over, with the count, once the header scrolls away.

src/components/chat/PendingItem.tsx

Renders a message the user sent to the chat that the server hasn't echoed back yet, at the end of the chat modal's list.

Functions

f
PendingItem(unnamed 0: PendingItemProps): React.JSX.Element

Renders a pending message, dimmed while it is sending. Once it has failed, it shows why and offers to retry it, edit it in the composer, or discard it.

Interfaces

I
PendingItemProps

Passed to PendingItem by ChatModal for each of WSChatContext's pendingChatMessages.

src/components/chat/ChatModal.tsx

Renders the site-wide chat in a modal, for signed-in users. ChatProvider mounts it while the chat is open. It reads the history over GET /api/chat and the live stream from WSChatContext, and writes through the latter.

Functions

f
ChatModal(unnamed 0: ChatModalProps): React.JSX.Element

Renders the chat, opening on focusId's message, or on the "new" divider, when it is more than a screen above the bottom, else at the bottom. Reaching the bottom of a visible tab moves the read marker. The draft and its reply target survive closing and reloading (utils/chat/draft.ts). While it is open in a visible tab, the server sends the user no notification for the mentions they see live.

Interfaces

I
ChatModalProps

Passed to ChatModal by ChatProvider.

  • focusId: string

    The message to open on, from a ?chat=<id> deep link. Ignored when it isn't in the first page of history.

  • onClose: () => void

    Closes the chat, and drops the ?chat= deep link if there is one.

src/components/chat/ReplyRef.tsx

Renders the line above a chat message, sent or pending, that quotes the message it replies to.

Functions

f
ReplyRef(unnamed 0: ReplyRefProps): React.JSX.Element

Renders the replied-to message's author and a one-line snippet of its text, as a button that scrolls to it. When it was deleted, the button is disabled and says so.

Interfaces

I
ReplyRefProps

Passed to ReplyRef by ChatMessageItem, from the message's replyTo* fields, and by PendingItem, from its replyTo.

src/components/chat/ChatMessageItem.tsx

Renders one message of the chat modal's list, with its reply, edit and delete actions.

Functions

f
ChatMessageItem(unnamed 0: ChatMessageItemProps): React.JSX.Element

Renders a chat message, or the editor over its text while editing. Its height eases between the two. Deleting asks for confirmation first.

Interfaces

I
ChatMessageItemProps

Passed to ChatMessageItem by ChatModal for each message row. The modal holds the edit, flash and permission state, and sends the edits and deletions over the socket.

src/components/chat/ChatEditBox.tsx

Renders the box that replaces a chat message's text while its author, or a moderator, edits it in the chat modal.

Functions

f
ChatEditBox(unnamed 0: ChatEditBoxProps): React.JSX.Element

Renders the field every text edited in place gets, InlineEditField, with Enter saving and Shift+Enter starting a new line, as in the composer. It scrolls itself into the list's view.

Interfaces

I
ChatEditBoxProps

Passed to ChatEditBox by ChatMessageItem while it is editing.

src/components/shared/Modal.tsx

Renders the dialog frame of the app's dialogs: creating a dump, the chat, adding to a playlist, changing a password.

Functions

f
Modal(unnamed 0: ModalProps): React.ReactPortal

Renders the dialog in a portal over the page, closed by Escape, the backdrop or its ✕, and locks the page's scroll while it is open. Content that handles Escape itself must call preventDefault on it, or the dialog closes too.

Interfaces

I
ModalProps

Passed to Modal by the component that shows it, which unmounts it in onClose.

src/components/shared/ThumbnailPlaceholder.tsx

Renders the artwork that stands in for a dump with no preview image, in the cards and the player. Thumbnail falls back to it when its image fails.

Functions

f
ThumbnailPlaceholder(unnamed 0: ThumbnailPlaceholderProps): React.JSX.Element

Renders the site's colour washed over the theme's surface, with its favicon on top, or its initial when the favicon is missing or fails to load. It fills whatever box it is placed in, from a small thumbnail to a mosaic tile.

Interfaces

I
ThumbnailPlaceholderProps

Passed to ThumbnailPlaceholder by a card, from the dump and its rich content, or through Thumbnail's placeholder.

src/components/shared/ConfirmModal.tsx

Renders the dialog that confirms a deletion, on the pages and in the dialogs that delete something.

Functions

f
ConfirmModal(unnamed 0: ConfirmModalProps): React.ReactPortal

Renders the dialog in a portal over the page, and locks the page's scroll. Escape closes only the confirmation, even over a Modal.

Interfaces

I
ConfirmModalProps

Passed to ConfirmModal by the component that shows it. The caller decides when it shows and hides it, on either callback.

src/components/shared/Segmented.tsx

Renders a choice among a few values as a row of buttons, in the .visibility-toggle style.

Functions

f
Segmented<V>(unnamed 0: SegmentedProps<V>): React.JSX.Element

Renders the options as buttons, the one equal to value marked active.

Interfaces

I
SegmentedOption

Passed in SegmentedProps.options. Its value is compared with === and must be unique as a string, since it keys the button.

I
SegmentedProps

Passed to Segmented by SegmentedField for a form, and by the profile's settings and role switcher, which save each click at once.

src/components/shared/Avatar.tsx

Renders a user's avatar, or their initial, wherever a user is named: cards, comments, chat, profiles and mention suggestions.

Functions

f
Avatar(unnamed 0: AvatarProps): React.JSX.Element

Renders the image from GET /api/users/:userId/avatar, scaled by the server to the size it is shown at, or the user's initial when they have none or it fails to load. An avatar added, changed or removed since the caller's data was fetched shows at once, through the user_updated messages that WSProvider puts in AvatarOverrideContext.

Interfaces

I
AvatarProps

Passed to Avatar from whatever data the caller has on the user, which may be stale: a live override from AvatarOverrideContext wins over hasAvatar and version.

src/components/shared/ListState.tsx

Renders a loaded list, or in its place the loading, error or empty state.

Functions

f
ListState<L extends object>(unnamed 0: ListStateProps<L>): ReactNode

Renders "Loading…" while state loads, its error in an ErrorCard, then empty or children. Those two are only built once loaded, so they can read the loaded fields without checking the status again.

Interfaces

I
ListStateProps

Passed to ListState by the feeds of the index and the lists of a profile.

src/components/shared/TabBar.tsx

Renders the row of tabs of the index, profile and search pages.

Functions

f
TabBar<T extends string>(unnamed 0: TabBarProps<T>): React.JSX.Element

Renders a row of tabs that scrolls when they don't all fit, keeping the active one in view.

Interfaces

I
Tab

A tab.

I
TabBarProps

The props of TabBar.

src/components/shared/ImagePicker.tsx

Renders an image that is also its own upload button, for a dump's thumbnail and a playlist's cover.

Functions

f
ImagePicker(unnamed 0: ImagePickerProps): React.JSX.Element

Renders the image, or a placeholder, which opens the file picker on a click, Enter or Space. The picker only reads the file, and uploads nothing itself.

Interfaces

I
ImagePickerProps

Passed to ImagePicker by an edit page, which uploads the file and then passes the new image, or a local preview of it, as src.

src/components/shared/Thumbnail.tsx

Renders a dump's preview image from another site, in the cards and the player, falling back to a generated placeholder.

Functions

f
Thumbnail(unnamed 0: ThumbnailProps): React.JSX.Element

Renders the image, and always renders something. It first loads the image with no Referer, since hotlink protection (Cloudflare's is one click) refuses a cross-site referrer but lets an empty one through. It then retries through GET /api/remote/image for the hosts that refuse an empty referrer too, and ends with the ThumbnailPlaceholder. An HTTP image goes through the proxy from the first try, to avoid mixed content.

Interfaces

I
ThumbnailProps

Passed to Thumbnail by a card or the player, from the dump's rich content.

src/components/shared/LoadMoreButton.tsx

Renders the button that loads a list's next page, where the list has no room to load it on scroll.

Functions

f
LoadMoreButton(unnamed 0: LoadMoreButtonProps): React.JSX.Element

Renders "Load more", disabled and reading "Loading…" while it loads.

Interfaces

I
LoadMoreButtonProps

Passed to LoadMoreButton by the lists that load their next page on a click, which render it while there is more to load.

src/components/shared/ErrorCard.tsx

Renders an error as a card, in place of what failed to load or save.

Functions

f
ErrorCard(unnamed 0: ErrorCardProps): React.JSX.Element

Renders the card as an ARIA alert, so a screen reader announces it when it appears.

Interfaces

I
ErrorCardProps

Passed to ErrorCard by a page or a form. The message usually comes from friendlyFetchError.

src/components/shared/Tooltip.tsx

Renders a tooltip on hover. Every caller uses it to give the full date over a relative time or an "edited" mark.

Functions

f
Tooltip(unnamed 0: TooltipProps): React.JSX.Element

Renders children in a <span>, with text above them while the mouse is over them. The tooltip is portaled to <body>, so no container clips it, and it stays where it opened if the page scrolls. It never shows on touch or keyboard focus.

Interfaces

I
TooltipProps

Passed to Tooltip by the component that wraps a label in it.

src/components/social/LikeButton.tsx

Renders the like button of a comment in a dump's thread.

Functions

f
LikeButton(unnamed 0: LikeButtonProps): React.JSX.Element

Renders a comment's like button and its count. Clicking the count, when it isn't zero, opens the list of likers (GET /api/comments/:id/likers) instead.

Interfaces

I
LikeButtonProps

Passed to LikeButton by CommentNode, with the state and the functions of WSContext.

src/components/social/FollowButton.tsx

Renders the Follow buttons of a user's profile and of a playlist's page. Both go through FollowContext, which follows at once and rolls back when the request fails; the button then says so beside it.

Functions

f
FollowPlaylistButton(unnamed 0: FollowPlaylistButtonProps): React.JSX.Element | null

Renders the button that follows or unfollows a playlist. It renders nothing for a visitor or a private playlist, and is disabled until the follows have loaded. Unlike FollowUserButton, it shows on the user's own playlists too.

f
FollowUserButton(unnamed 0: FollowUserButtonProps): React.JSX.Element | null

Renders the button that follows or unfollows a user. It renders nothing for a visitor or on the user's own profile, and is disabled until the follows have loaded.

Interfaces

I
FollowPlaylistButtonProps

Passed to FollowPlaylistButton by PlaylistHeader.

I
FollowUserButtonProps

Passed to FollowUserButton by the UserPublicProfile page.

src/components/social/VoteButton.tsx

Renders the upvote button of a dump, on its card and on its page.

Functions

f
VoteButton(unnamed 0: VoteButtonProps): React.JSX.Element

Renders a dump's upvote button and its count. Clicking the count, when it isn't zero, opens the list of voters (GET /api/dumps/:id/voters) instead.

Interfaces

I
VoteButtonProps

Passed to VoteButton by DumpCard, JournalCard and the Dump page, with the state and the functions of WSContext.

src/components/social/UserCard.tsx

Renders a user as a row of a list of users: their avatar and name, linking to their profile, and a button to follow them.

Functions

f
UserCard(unnamed 0: UserCardProps): React.JSX.Element

Renders user as an <li> of a .user-card-list, with the follow button, which FollowUserButton hides on the viewer's own card.

Interfaces

I
UserCardProps

Passed to UserCard by the profile's follow tab and the community page's new members.

src/components/social/UserListPopover.tsx

Renders the list of users behind a count. VoteButton opens it on a dump's voters, and LikeButton on a comment's likers.

Functions

f
UserListPopover(unnamed 0: UserListPopoverProps): React.ReactPortal | null

Renders the users from fetchUrl in a popover next to anchorRef, with a button that loads the next page. It is fixed and portaled to <body>, so a card's overflow can't clip it. It renders nothing until it has measured the anchor.

Interfaces

I
UserListPopoverProps

Passed to UserListPopover by the button whose count was clicked. The button owns the open state and unmounts the popover in onClose.

src/components/social/CommunityFeed.tsx

Renders the community feed: who is online, the week's figures, and the latest things the site's members did in public, each a line with links to who and what it tells of. The GeoCities style's Marquee scrolls it, and any style may show it: it is a plain list, which the style lays out.

Functions

f
CommunityFeed(unnamed 0: CommunityFeedProps): React.JSX.Element

Renders the feed as a <ul>: a line for who is online, one for the week's figures, which links to /community, one for its most upvoted dump, then one per event, newest first, ending on how long ago it was. A line with nothing to say is left out. The times are computed at render.

Interfaces

I
CommunityFeedProps

Passed to CommunityFeed by whatever shows the feed.

src/components/comment/CommentNode.tsx

Renders a comment of a dump's thread and, recursively, its replies.

Functions

f
CommentNode(unnamed 0: CommentNodeProps): React.JSX.Element

Renders a comment with its like button, and the reply, edit and delete actions for whoever may use them. Its author and moderators may edit and delete it. A deleted comment shows as "[deleted]", with its replies. The element's id, comment-<id>, is the anchor notifications link to.

Interfaces

I
CommentNodeProps

Passed to CommentNode by CommentThread for each top-level comment, and by CommentNode for each reply. The callbacks are CommentThread's, passed down unchanged.

src/components/comment/CommentThread.tsx

Renders the comments under a dump on its page, /@:username/dumps/:slug.

Functions

f
CommentThread(unnamed 0: CommentThreadProps): React.JSX.Element

Renders the comment count, the form that posts a comment when someone is signed in, and the tree of CommentNode.

Interfaces

I
CommentThreadProps

Passed to CommentThread by the Dump page, which holds the comments. The page applies the changes this user makes through the callbacks, and the ones others make from the socket's comment events, so a change can come both ways and must be applied once.

src/components/comment/CommentForm.tsx

Renders the form of a dump's comment thread that posts a comment or a reply. An edit opens InlineEditField in the comment's place.

Functions

f
CommentForm(unnamed 0: CommentFormProps): React.JSX.Element

Renders a comment editor and its submit button, disabled while the text is blank or longer than VALIDATION.COMMENT_BODY_MAX.

Interfaces

I
CommentFormProps

Passed to CommentForm by CommentThread, for a new comment, and by CommentNode, for a reply.

src/main.tsx

Starts the frontend. It loads the stylesheets and the interface's language, sends the first request of the page the visit lands on, then mounts App on index.html's #root.

src/App.tsx

Renders the whole app, its providers around its routes. main.tsx mounts it. Its paths follow one rule (shared/paths.ts): @name is a user, any other word under the root a category or an app page, a view of a page one more segment, and a default view the bare path, with no redirect.

Functions

f
App(): React.JSX.Element

Renders the app. The theme, player, socket, follow and chat providers read the session, and ChatProvider reads the URL, so it sits inside the router. GlobalPlayer sits outside the routes, so playback survives navigation.

f
AppRoutes(): React.JSX.Element

Renders the route table, and the floating buttons over every page. Exported for the tests, which reach a page through it as a visit does.

dev/instances.ts

Lists the dev instances of gerbeur, each an API server and a Vite server of its own, run side by side so that federated sign-in between instances can be tried on one machine. In the devcontainer each is a service of .devcontainer/compose.yaml, which starts with it; outside, deno task dev starts them all, or deno task dev <name>… those named. Both run dev/start.ts. A new instance is an entry here, and a service there.

Interfaces

I
DevInstance

One instance of INSTANCES.

  • apiPort: number

    The port of its API server, GERBEUR_PORT.

  • env: Record<string, string>

    GERBEUR_* settings of its own, over the base .env: a site name, an emoji or a style, to tell the instances apart, or which ways federated sign-in goes.

  • name: string

    Names its database and uploads, and prefixes its output. Lowercase letters, digits and hyphens.

  • webPort: number

    The port of its Vite server, where its pages are.

Variables

v
INSTANCES: readonly DevInstance[]

The instances deno task dev starts, in this order. The first keeps the ports the single dev server always had, and takes no users of the others, while its own sign in to them, so that both switches of federated sign-in can be tried.

dev/start.ts

Starts the dev instances of dev/instances.ts, for deno task dev: for each, an API server that restarts on change and a Vite server, wired to each other and kept apart from the other instances by the rule that module states. Every line they print is prefixed with the instance's name. Ctrl+C stops them all, and so does any of them stopping, such as a Vite server whose port is taken.