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.
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.
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.
Requires a valid session token in the Authorization header, and puts its
payload in ctx.state.user for the rest of the route.
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.
Set on ctx.state by authMiddleware for the handlers after it.
user carries the caller's live role, not the one in their token.
The context type of a handler behind authMiddleware. Pass the route
path, as in AuthContext<"/:key/role">, to keep ctx.params typed.
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.
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.
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.
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.
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.
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.
Checks the body of PATCH /api/me/settings before routes/me.ts trusts
it.
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.
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.
Checks whether obj is a FederationLoginRequest.
Checks whether obj is a FederationSessionRequest.
Checks whether obj is a SetPeerRequest.
Checks the JSON bodies that the account routes (routes/users.ts,
routes/me.ts, routes/sessions.ts) receive before they read them.
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.
Checks whether obj is a LoginUserRequest. It checks the types
only: a wrong name or password is the login route's answer to give.
Checks whether obj is a valid RegisterUserRequest. When it isn't,
validateRegisterUserRequest says why.
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.
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.
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.
Checks whether obj has the fields of a CreateCategoryRequest.
Checks whether obj has the fields of an UpdateCategoryRequest.
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.
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.
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.
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.
Checks the playlist request bodies, for the playlist routes to answer a 400.
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.
Checks whether obj is a ReorderPlaylistRequest. Whether its ids
match the playlist is reorderPlaylist's to check.
Checks whether obj is an UpdatePlaylistRequest whose title, if
given, isn't blank, with a title and description within the VALIDATION
limits.
Checks the comment request bodies, for the comment routes to answer a 400.
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.
Checks whether obj is an UpdateCommentRequest whose body isn't
blank and fits VALIDATION.COMMENT_BODY_MAX.
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.
The whole server, exported so the tests' helpers can send it requests without listening.
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.
Checks whether obj is an AuthPayload. A media or reset token's
payload, which has a purpose, never is.
Checks whether obj is a PasswordResetPayload. It doesn't check
exp, which jwtVerify has already enforced.
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.
-
exp: number
Seconds since the epoch.
-
iat: number
Seconds since the epoch, compared with
users.tokens_valid_after. - role: Role
- userId: string
- username: string
Signed into a reset link's token by createPasswordResetToken, and returned
by verifyPasswordResetToken to resetPassword.
-
exp: number
Seconds since the epoch.
- purpose: "password-reset"
- userId: string
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.
Converts a row to the ChatMessage that chat-service returns, turning its timestamps into Dates and its nulls into absent fields.
Checks whether obj, a row read by chat-service, is a
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.
- author_avatar_mime: string | null
- author_username: string
- body: string
- created_at: string
- id: string
-
mentions_everyone: number
1 or 0.
- reply_to_author: string | null
-
reply_to_body: string | null
Cut to chat-service's
REPLY_SNIPPET_LENcharacters. - reply_to_id: string | null
- updated_at: string | null
- user_id: string
Checks the rows settings-service.ts reads from user_settings, and turns
them into the UserSettings the settings endpoints answer.
Checks whether obj, a row getSettings selected, is a
UserSettingsRow.
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.
Stored by updateSettings and read back by getSettings. A user without a
row has the defaults.
- color_scheme: string | null
-
digest_monthly: number
0 or 1.
-
digest_weekly: number
0 or 1.
-
style: string | null
A style name, or null for the instance's.
- updated_at: string
- user_id: 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.
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.
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.
Checks whether obj, a row selected with the columns of UserRow, is
one.
Returns what anyone may see of user, without the hash and the email. Every
route that sends another user's profile goes through it.
Returns what user sees of themselves, without the hash, for the routes
that answer the caller's own account.
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.
Converts a row to a User, turning nulls into undefined.
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, ashash.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''.
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.
-
avatar_mime: string | null
Null without an avatar.
- created_at: string
-
description: string | null
Markdown.
- email: string
- id: string
-
invited_by: string | null
Null for the seeded admin, who had no inviter.
-
invited_by_username: string | null
Joined from the inviter's row rather than stored, and undefined when the query doesn't join it. The guard doesn't check it.
-
password_hash: string
''for an account from another instance, which has no password. - role: Role
-
updated_at: string | null
Bumped by any profile or avatar change.
- username: string
Checks the rows invite-service.ts reads from invites to vet an invite
before sign-up.
Checks whether obj, a row validateInvite selected, is an
InviteRow.
Stored by createInvite, and read back by validateInvite when the invite
link is opened or used to sign up.
-
created_at: string
Starts the 7-day window in which the invite works.
-
inviter_id: string
Becomes the new user's
invited_by. -
token: string
The random token the invite link carries, which is the primary key.
-
used_at: string | null
Set when an account is created with the invite.
Turns the rows the category service reads from categories into the Category the category routes send.
Converts row to the Category the category routes send.
Checks whether a row node:sqlite returned is a CategoryRow.
Returned by the category service's SELECTs on categories.
- created_at: string
- id: string
- kind: CategoryKind
- name: string
-
position: number
Sorts the categories, lowest first.
- slug: string
- updated_at: string | null
Checks the rows password-reset-service.ts reads from
password_reset_tokens to make each reset link work once.
Checks whether obj, a row resetPassword selected, is a
PasswordResetTokenRow.
Stored by requestPasswordReset when it emails a reset link, and read back
by resetPassword when the link is used.
-
expires_at: string
Lets
db/startup.tspurge old rows. The token's ownexpis what ends the link. -
token: string
The signed token itself, which the reset link carries.
-
used_at: string | null
Set when the link is used, and reset to null if the reset then fails.
- user_id: string
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.
Converts a Dump back to a row, for a service that writes an edited dump's columns.
Converts a row to the API's Dump, parsing its preview's JSON. It
leaves categoryIds out: the services add them with attachCategoryIds.
Checks whether obj, a row from a query, is a DumpRow. It checks
each column's type, not that kind is url or file.
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.
- comment: string | null
-
comment_count: number
Computed by a subquery, which counts the comments that aren't deleted.
- created_at: string
- custom_thumbnail_mime: string | null
- file_mime: string | null
- file_name: string | null
- file_size: number | null
- id: string
- is_private: number
- kind: string
-
owner_username: string
The poster's current name, computed by a subquery.
-
rich_content: string | null
RichContent as JSON.
-
slug: string
Unique among its poster's dumps, and set on every row since 0021.
- title: string
- updated_at: string | null
-
url: string | null
The link as the poster submitted it.
- user_id: string
- vote_count: number
Lists the stored columns of DumpRow. A column added to the table must be added here, to DumpRow and to isDumpRow.
Selects a whole DumpRow, the stored columns and the computed
comment_count and owner_username.
Selects a whole DumpRow in a query that aliases dumps as d to
join it with other tables.
Checks the rows follow-service.ts reads from follows to answer what a
user follows.
Checks whether obj, a row getFollowStatus selected, is a
FollowRow.
Read by getFollowStatus. Exactly one of followed_user_id and
followed_playlist_id is set, as the table's CHECK constraint requires.
Turns the rows the playlist and follow services read from playlists into
the Playlist the API sends.
Checks whether a row node:sqlite returned is a PlaylistRow.
Converts row to the Playlist the playlist routes send.
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.
- created_at: string
- description: string | null
- id: string
- image_mime: string | null
- is_public: number
- owner_username: string
-
slug: string
Unique among its owner's playlists, and set on every row since 0021.
- title: string
- updated_at: string | null
- user_id: string
Turns the rows the comment service reads from comments into the Comment the comment routes send.
Converts row to the Comment the comment routes send and the
WebSocket broadcasts.
Checks whether a row node:sqlite returned is a CommentRow.
Returned by the comment service's SELECTs on comments, joined with
users for the author_ columns.
- author_avatar_mime: string | null
- author_username: string
- body: string
- created_at: string
-
deleted: number
1 once deleted. The body is emptied but the row stays, so its replies keep their place in the thread.
- dump_id: string
- id: string
- like_count: number
-
parent_id: string | null
It is null on a top-level comment.
- updated_at: string | null
- user_id: string
Turns the rows of notifications, once checked, into the Notification that GET /api/me/notifications answers.
Checks whether obj, a row getNotificationsForUser selected, is a
NotificationRow.
Converts a row to the Notification the client gets.
Stored by the triggers of notification-service.ts, and read back by
getNotificationsForUser.
- created_at: string
-
data: string
The NotificationData as JSON, not checked by the guard.
- id: string
-
read: number
0 or 1.
-
source_key: string | null
Names the event that caused the notification. A unique index keeps a second row with the same
(user_id, source_key)out; null allows duplicates. -
type: string
A NotificationType, not checked by the guard.
- user_id: string
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.
Returns rows turned by toApi, once isRow has accepted each.
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.
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.
-
blocks(tokens: Token[]): string
Draws the body's blocks, gathering each
##section with its content. -
button(): stringlabel: string,href: string
Returns a button linking to
href. -
darkCss(): string
Returns the CSS that recolors every painted element in dark mode, or nothing off the "auto" scheme.
-
darkPalette: EmailPalette | null
The palette that replaces it in dark mode, on the "auto" scheme only.
-
document(unnamed 0: { subject: string; preheader: string; body: string; footer: string; }): string
Returns the whole HTML document: the brand, the card holding
body, and thefooter.preheaderis the hidden first line inboxes show beside the subject. -
eyebrow(html: string): string
Returns the small uppercase line above the heading.
-
font: string
The font stack, for templates that set their own blocks.
-
fs(px: number): string
Returns
px, scaled for the style, as a CSS length. -
heading(): stringhtml: string,marginBottom?: number
Returns the email's heading, in the style's title type.
-
image(): stringsrc: string,alt: string,width: number,height: number,href?: string
Returns an image block, rounded as the style rounds images.
-
inline(): stringtokens: Token[],mode: EmailKit["mode"]
-
link(): stringhtml: string,href: string,strong?: boolean
Returns a link to
hrefaroundhtml, in the link color. -
linkBox(html: string): string
Returns a raw link set in a box, for clients that break buttons.
-
list(list: Tokens.List): string
Draws a list as rows, ranked when it is numbered.
-
marked: Marked
Draws Markdown's inline tokens with the kit's colors.
-
mode: "text" | "footer" | "row" | "sentence"
How inline draws emphasis and links, by where they sit.
-
note(): stringhtml: string,css?: string
Returns a smaller paragraph in the muted color.
-
paint(): stringroles: { fg?: PaletteRole; bg?: PaletteRole; bd?: PaletteRole; extra?: string[]; },css?: string
Returns the
classandstyleattributes of an element colored by roles:fgfor its text,bgfor its fill,bdfor its border. The classes let the dark block recolor it;cssis the rest of its style. -
palette: EmailPalette
The palette inlined into the HTML.
-
para(): stringhtml: string,css?: string
Returns a paragraph.
cssadds to or overrides its style. -
pill(label: string): string
Returns a small uppercase pill, as on one's own dump in a list.
-
quote(html: string): string
Returns a quoted comment or message.
-
render(unnamed 0: { subject: string; preheader: string; markdown: string; }): RenderedEmail
Returns the email written as
markdown: the Markdown itself is the plain-text part, and its blocks, drawn in this kit's style, are the HTML. Each template writes its email once this way. The blocks map as follows. -
section(): stringtitle: string,html: string,first?: boolean
Returns a section of the card: a divider in the style's rule (none before the first), an uppercase title in the accent color, and
html. -
style: EmailStyle
The style being drawn.
-
subhead(html: string): string
Returns a small bold heading inside a section.
Escapes value for HTML text and attributes. Everything users wrote (names,
titles, quotes) goes through it, since none of it may become markup.
Escapes value for Markdown, so that user text in a plain-text part (a
title with * or [, say) stays text.
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.
Returns a Markdown link to url labelled with text, escaped.
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.
Returned by each template to its caller, which hands it to sendEmail
with a recipient.
- html: string
- subject: string
-
text: string
The plain-text part, for clients that show no HTML. It is Markdown, so it reads as text and renders as the email's outline.
Names a color of an EmailPalette, as EmailKit.paint takes it, so that the dark block can recolor the element by the same name.
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.
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.
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.
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.
-
accent: string
Section titles, ranks and button fills (
--color-accent). -
border: string
GeoCities' ridged border.
-
brand: string
Kanagawa's oniViolet wordmark; others use
text. -
card: string
The card holding the content (
--color-surface). -
date: string
Kanagawa's fujiGray dates; others use
muted. -
gradient: string[]
The colors of GeoCities' rainbow or Melange's spice, left to right.
-
link: string
--color-link. -
muted: string
--color-text-muted. -
onAccent: string
Text on an accent fill (
--color-on-accent). -
outer: string
The page around the card (
--color-bg). -
quote: string
Quoted comments and messages (
--color-text-secondaryunless set). -
rule: string
Dividers and hairlines (
--color-border). -
sec: string
--color-text-secondary. -
shadow: string
Melange's warm card shadow, translucent.
-
soft: string
Pills and the reset link's box, as the site's badges (
--color-badge-bg). -
text: string
--color-text.
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.
-
bar: boolean
Draws the palette's gradient as a bar along the card's top.
- dark: EmailPalette
-
font: string
The theme's font stack (
--font-body). Its first family is a web font only Apple's mail apps load, through webFont; other clients fall back along the stack. -
frame: "none" | "hard" | "ridge" | "hairline" | "soft"
How the card stands out from the page.
-
heavy: number
The weight of titles of items, figures and names.
- light: EmailPalette
-
linkUnderline: boolean
Underlines links, for styles whose links are the color of the text.
-
radius: { card: number; img: number; btn: number; pill: number; avatar: number; }
Corner radii, in pixels: the card's as the site's cards (
--radius-xl), the images' as its thumbnails (--radius-md), the buttons' as its buttons (--radius-lg), the pills' as its badges (--radius-sm). The avatars' is the email's own, since the site rounds them with a literal 50% that some styles square. -
rule: "hairline" | "thick" | "gradient"
The divider between sections; "gradient" draws the palette's.
-
scale: number
Multiplies every font size: Times sets its serif larger, Kanagawa its monospace smaller.
-
title: { size: number; weight: number; upper?: boolean; spacing?: string; accent?: boolean; }
The heading: its size in pixels, weight, and Brutalist's uppercase and spacing.
-
webFont: string
The Google Fonts
familyparameter of the web font, if it is one.
Every style's email look, keyed by its name, read once at load.
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.
Translates the emails' messages, in EMAIL_LOCALE. Every catalog is loaded, so a test can activate another language.
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.
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.
Returned by readTokens: the tokens of one style in one scheme, read as the kind of value each is.
-
color(): stringtoken: string,over?: string
Returns the color of
tokenas#rrggbb. A translucent one is laid overover(another token) first, as it shows on the site; withoutoverit staysrgba(), as a shadow's must. -
font(token: string): string
Returns the font stack of
token, single-quoted for astyleattribute. A family the site bundles as "X Variable" is named "X", as a mail client or Google Fonts knows it. -
px(token: string): number
Returns the length of
tokenin pixels. -
stops(token: string): string[]
Returns the colors of the gradient
token, in order, without positions. -
value(token: string): string
Returns the resolved value of
token, or throws when it isn't set.
A scheme a token is resolved for. "auto" is drawn from both.
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.
Returns the reset email of username, whose button opens resetUrl. The
link stays usable for hours, which the email states.
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.
Returns the digest content describes, written as Markdown and drawn in
look, with its subject and hidden preheader.
Passed to renderDigestEmail by digest-service.ts. Each section is
null when it has nothing to show, and is left out; the service sends
nothing when all of them are.
-
activity: { counts: Partial<Record<ActivityKind, number>>; highlights: DigestHighlight[]; total: number; }
| nullWeekly only.
- cadence: DigestCadence
-
community: { members: { username: string; invitedBy: string | null; }[]; membersMore: number; playlists: { title: string; url: string; owner: string; dumps: number; }[]; playlistsMore: number; chatMessages: number; totals: { dumps: number; upvotes: number; comments: number; }; }
| null -
from: Date
The period covered, from
fromtoto. - links: { notifications: string; followed: string; chat: string; community: string; settings: string; unsubscribe: string; }
-
network: { dumps: DigestDump[]; more: number; } | null
Weekly only.
- reader: string
- to: Date
- top: DigestDump[]
A dump in a digest's lists, with its links made absolute.
-
addedTo: string
The followed playlist it was added to, when that's why it's listed.
- comments: number
-
domain: string | null
The link's host, or null for an upload.
-
mine: boolean
Posted by the reader, which the lists mark.
- poster: string
-
thumbnail: string | null
An absolute image URL, or null to show no image.
- title: string
- url: string
- votes: number
One line of the weekly digest's "For you", made from a notification.
- at: Date
- kind: HighlightKind
-
quote: string
The comment or message, flattened and shortened.
-
target: { title: string; url: string | null; }
What they did it on, absent for a follow of the reader or the chat. Its
urlis null once it is deleted, and the title then shows unlinked. -
who: string
Who did it.
| "followers"
| "comments"
| "mentions"
| "likes"
| "playlistFollows"
The kinds of activity a weekly digest counts.
| "mentionDump"
| "mentionPlaylist"
| "mentionChat"
| "mentionEveryone"
| "comment"
| "playlistFollow"
| "userFollow"
| "commentLike"
What a highlight of the weekly digest says happened.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Checks an uploaded image's size and content, and returns its MIME type, which the route stores to serve the image with.
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.
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.
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/.
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.
Checks whether the caller's role grants permission. The role is the live
one authenticate put in the payload, not the token's.
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.
A map whose entries expire ttlMs after they are written, holding
maxEntries at most.
-
clear(): void
Empties the cache, for the tests' reset between cases.
-
delete(): voidkey: string,value?: V
Removes
key, or only whenvalueis still what it holds, so a late failure can't remove the entry that replaced its own. -
get(key: string): V | undefined
Returns the value under
keyunless it is missing or expired. -
set(): Vkey: string,value: V
Stores
valueunderkeyfrom now on, and returns it.
Lets any layer fail a request with an HTTP status, which the error middleware turns into the response.
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.
Passed to an APIException; any other status is a type error.
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).
Writes color as a lowercase #rrggbb, its alpha dropped.
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.
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.
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.
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.
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.
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.
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.
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.
Passed to generateSiteIcons by api/scripts/generate-icons.ts, from
the site's config.
-
bgColor: string
Fills the Apple touch icon, and the manifest's theme and background.
- emoji: string
-
outDir: string
Created if missing.
- siteName: string
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.
& 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.
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.
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.
The fields of a playlist's dump that isMemberVisibleTo needs. A
Dump has them. For its broadcasts, the playlist service reads only these.
- id: string
- isPrivate: boolean
-
userId: string
The dump's poster, not the playlist's owner.
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.
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.
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.
Passed to serveFile by its caller, which knows the file's type and how long it may be cached.
- cacheControl: string
- mime: string
-
ranges: boolean
Answers a single
Range: bytes=…with a 206. The files route sets it, so that audio and video can seek.
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.
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.
Makes outPath with generate unless it exists. Resolves false when it
can't be made, because ffmpeg is missing or failed.
Returns where the still and the waveform generated from dump dumpId's
upload are kept.
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.
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).
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.
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.
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.
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.
Writes a 320-pixel-wide JPEG still of a video, one second in.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Returns the stored private JWK privateJwk as a key to sign with.
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.
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.
Returns the payload of token once its signature checks against jwks,
the keys of issuer, and it is addressed to audience and unexpired.
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.
Returns the user id and issue time of a media token, or null when it is invalid, expired or another kind of token.
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.
Returns the user and cadence of an unsubscribe token, or null when it is invalid or another kind of token.
An instance's key, ready to sign, from importInstanceKey. Its
kid names the public half in the instance's JWKS.
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.
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.
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.
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.
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.
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.
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.
Passed to takeToken by each limited action, which declares its own as a constant.
-
capacity: number
How many tokens a full bucket holds, and so the largest burst.
-
refill: number
Tokens regained per second.
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.
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".
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.
Spells LIKE ? ESCAPE '\', for a likePattern parameter.
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.
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.
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.
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.
A kind of item that has a slug under its owner.
Where each SlugKind's slugs live.
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.
Hashes password with scrypt and a random salt, as hash.salt in hex, for
users.password_hash.
Checks password against a hash made by hashPassword.
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.
Checks whether slug is one of the RESERVED_SLUGS.
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.
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.
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.
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.
A recurring task, exported by its module for main.ts to start.
-
everyMs: number
How long from one run's start to the next.
-
firstAfterMs: number
How long after the start the first run waits,
everyMsby default. -
name: string
Names it in the log, as
[digest]. -
run(): void | Promise<void>
Logs what it did itself, if anything; its errors are logged here.
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.
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.
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.
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.
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.
Returned to the routes by the services' list functions, and sent as
PaginatedData.
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.
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.
Serves the chat's history to the chatbox (ChatModal). Messages are sent,
edited and deleted over the socket instead (routes/ws.ts).
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.
Lets any signed-in user invite someone, from their profile page. The invite
link opens the /register page.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Serves the figures of the client's /community page, which
community-service.ts recomputes on a timer, and the community feed.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Signs users in, from the client's /login page. A session is a token the
client keeps; signing out forgets it, with no request.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Stored in attachments.resource_type by bindAttachments, it names
the kind of resource whose saved text links the attachment.
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.
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.
Makes an invite from inviterId and returns its token, which the client
puts in the invite link.
Makes a claimed invite usable again, when the sign-up that claimed it failed.
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.
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.
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.
Makes followerId follow followedUserId, and notifies the followed user.
Following again changes nothing. The follower's other tabs are told.
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.
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.
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.
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.
Returns the ids of every user and playlist followerId follows, private
playlists included, for GET /api/me/follow.
Makes followerId stop following a playlist, if they did.
Makes followerId stop following followedUserId, if they did.
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.
Returns a page of userId's notifications, newest first, with their refs
joined, for GET /api/me/notifications.
Returns how many of userId's notifications are unread. The WebSocket route
sends it in its welcome message, for the bell's count.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Notifies followedUserId that followerId follows them, once however often
they unfollow and follow again. followUser calls it.
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.
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.
Forgets every cached preview. The tests' stubNetwork calls it, since
previews fetched from the previous stubbed network no longer apply.
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.
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.
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.
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.
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.
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.
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.
Deletes a playlist for DELETE /api/playlists/:playlistId, and broadcasts
playlist_deleted. canModerate lets someone other than the owner do it.
Returns the playlist if requestingUserId may edit it. The cover upload
route checks this before it writes the file, then calls
setPlaylistImage.
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.
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.
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.
| 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
A playlist holding a dump, as playlistsHolding returns it: what
broadcastPlaylistDumpsUpdated needs to choose its audience.
Plays the client in federated sign-in: it lets users of other gerbeur instances sign in here with their account there, their home.
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.
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.
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.
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.
Stores the site-wide chat's messages and each user's read marker.
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.
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.
Deletes a message on a chat_delete. canModerate lets a moderator delete
anyone's message. Replies to it keep their replyToId, without its preview.
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.
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.
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.
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.
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.
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.
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.
-
readAt: string
The
created_atof the newest message the user has read. -
unread: number
Counts only the messages of other users after
readAt.
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.
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.
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.
Returns the figures of period as of the last refresh, for
GET /api/community, refreshing them first if there are none yet.
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.
Recomputes the figures of each period, ending at now.
Refreshes the figures at startup, then every
COMMUNITY_REFRESH_MINUTES. main.ts starts it.
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.
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.
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.
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.
Sends the due digests a minute after the server starts, then every quarter
of an hour. main.ts starts it.
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.
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.
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.
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.
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.
Passed to sendEmail: a rendered email, its recipient, and the
headers some emails add (the digests' List-Unsubscribe).
Plays the home instance in federated sign-in: it lets this instance's users sign in to other gerbeur instances with their account here.
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.
Returns the instance clientId names, for the /authorize page's question
to user userId, and whether they already let it sign them in.
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.
Returns the instance's metadata, served at
/.well-known/openid-configuration.
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.
Returns the instances user userId let their account sign in to, the most
recently used first, for the settings page.
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).
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.
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.
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.
Retries the outbox's failed events every minute.
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.
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.
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.
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.
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.
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.
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.
Stores upvotes on dumps and keeps each dump's vote_count in step with them.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Returns dump dumpId as requestingUserId may see it. The routes resolve
every :dumpId with it before acting on the 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.
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.
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.
Returns a page of the dumps requestingUserId may see, newest first, for the
index feed (GET /api/dumps).
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.
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.
Removes a dump's custom thumbnail and its file, for
DELETE /api/dumps/:dumpId/thumbnail.
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.
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.
Records that a dump has a custom thumbnail of type mime, for
PUT /api/dumps/:dumpId/thumbnail, once the route has written the file.
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).
Keeps the set of open sockets and sends them the server's messages.
Sends everyone categories_changed, after the category manager created,
edited or deleted one, so that every client fetches them again.
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.
Sends chat_message_deleted to every signed-in socket.
Sends chat_message_updated to every signed-in socket.
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.
Sends comment_deleted, as broadcastCommentCreated does.
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).
Sends comment_updated, as broadcastCommentCreated does.
Tells the clients that may see a dump how it changed, as broadcastChange says, for the dump service after every write.
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.
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.
Sends everyone presence_update. The route calls it whenever a socket opens
or closes.
Sends chat_typing_update to every signed-in socket. The route also calls
it when a typing user's socket closes.
Sends everyone user_updated, after a profile edit, a role change or an
avatar upload.
Sends votes_update after the route stored a vote, to everyone, or with
onlyUserId to that user's sockets alone (for a private dump).
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.
Returns each signed-in user with a socket open, once. The route sends it in
welcome, and broadcastPresence in presence_update.
Returns each user who sent chat_typing true within the last
TYPING_TTL, once, for broadcastTyping.
Records a client's pong, which keeps its socket open.
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.
Adds a client once its socket opens. From then on, it receives the broadcasts, and is shown online if it is signed in.
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.
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.
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.
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.
Removes a client when its socket closes. The caller rebroadcasts presence.
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.
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.
Passed to takeToken by each limited action, which declares its own as a constant.
-
capacity: number
How many tokens a full bucket holds, and so the largest burst.
-
refill: number
Tokens regained per second.
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.
- avatarMime: string
-
avatarVersion: number
Set by updateClientAvatar, for the client's avatar URLs.
-
chatOpen: boolean
Set by
chat_focus: the chatbox is open and the tab visible, so the user is reading the chat and gets no notification for a mention in it. -
pongReceived: boolean
Undefined until the first ping, so a new socket isn't closed at once.
-
role: Role
Read again from the database on each chat edit or delete, so a demoted moderator loses the right at once.
- socket: WebSocket
-
typingAt: number
When the last
chat_typingtrue arrived, in ms. - userId: string
- username: string
Pings every client each PING_INTERVAL, closing the ones that didn't
answer the previous ping. main.ts starts it.
Expires the typing users who went quiet without a false, such as by closing
the tab, every 2 seconds. main.ts starts it.
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.
Returns what an admin decided about the instance at issuer, or null when
it is on neither list.
Returns the instances on the lists, by name: every one for /manage, or
only the trusted ones, which the login page offers.
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.
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.
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.
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.
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.
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.
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.
Deletes category id, for DELETE /api/categories/:id. Its dumps or
playlists stay, and lose it from their categoryIds.
Returns category id, for updateCategory.
Returns the category of kind at slug, for listDumpsByCategory
and the list of every public playlist.
Returns the id of the category of kind at slug, or undefined when there
is none.
Returns every category, of both kinds, by kind, position then slug, for
GET /api/categories.
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.
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.
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.
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.
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.
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.
Returns the key the instance signs with now: the newest stored, or a new one, stored, when there is none.
Forgets the loaded key, for the tests, which wipe the table between cases.
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.
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.
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.
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.
Tells clients that dump dumpId, which just turned public, is now related
to the dumps linked to or from it, for the dump service. Its poster saw
those links already; to everyone else they are new.
Replaces the backlinks from one text, a dump's note (dump_comment) or one
of its comments (comment), with the links its current text holds. A link
counts when it is a dump's page on this site (/@owner/dumps/<slug>), or
when it is the URL a dump was posted with, compared by canonicalizeUrl as
reposts are. The dump and
comment services call it on every save, and with an empty text when a
comment is deleted. Clients are told of the dumps it links or no longer
links, as dump_related_changed.
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.
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.
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.
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.
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.
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.
The dump a comment is on, as updateComment and deleteComment return it: what the route needs to choose the broadcast's audience.
- id: string
- isPrivate: boolean
-
userId: string
The dump's poster, the only one who sees a private dump's comments.
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.
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.
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.
Creates a user with the user role, invited by inviterId, for
POST /api/users. The caller has validated request and claimed the
invite.
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).
| 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.
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.
Returns user userId, with their hash and email.
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.
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.
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.
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.
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.
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.
Returns up to limit users whose name starts with query, for the editor's
Returns up to limit users whose name or description contains query, for
the /search page (GET /api/search). They are already public profiles.
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.
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.
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.
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.
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.
Returns the settings of user userId, the defaults if they changed none.
Applies patch to the settings of user userId and returns them all. The
settings it leaves out keep their value.
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.
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.
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.
Returns the id of userId's item of kind whose slug is or was slug, or
undefined when none is.
Previews SoundCloud links from their page, for rich-content-service.ts.
Claims SoundCloud's hosts, short links included, and builds the embed player's URL from the page's address.
Previews Bandcamp links from their page, for rich-content-service.ts.
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.
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.
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.
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.
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.
Matches every URL, so it comes last in the service's list.
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.
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.
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.
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'srich_contentor 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
isValidHttpUrlaccepted; never throws. - name: string
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Fetches a page for its link preview, through fetchWithTimeout. A response that isn't HTML is cancelled unread.
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.
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.
Reads the body of res up to maxBytes; the rest is cancelled, never
downloaded. The image proxy reads through it, and readTextCapped.
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.
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.
The default cap of readTextCapped. The metadata previews read is in
the <head>, and a page past this is a download.
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.
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.
Decodes the entities in str in a single pass, so &quot; reads
". 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.
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.
Reads html, served from pageUrl, in one pass. The generic preview and
the providers built on it read a page through it.
Returned by the JSON-LD reading of parsePage, which the generic preview ranks below the page's OG and Twitter tags.
- description: string
-
thumbnailUrl: string
From
image, or elsethumbnailUrlorthumbnail. -
title: string
From
name, or elseheadline.
Returned by parsePage: what a page says about itself, entities decoded, values trimmed and URLs absolute.
-
bestIcon: string
The largest icon the page links, which the thumbnail placeholder draws.
-
firstContentImage: string
The first image that doesn't look like page furniture.
- jsonLd: JsonLdResult
-
largeImage: string
The first image declared at least 200 pixels both ways.
-
maskIconColor: string
The
colorof the Safari pinned-tab icon, as written. -
meta(key: string): string | undefined
Returns the
contentof the first<meta>whosepropertyornameiskey, in any case: sites writeog:tags under either attribute, and Twitter's too. -
themeColor: string
The
theme-color, as written: pass it to normalizeCssColor. -
title: string
The text of the
<title>element.
Fetches Twemoji glyphs from the jsDelivr CDN, for lib/site-icons.ts to
render the favicons from.
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.
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.
Empties the cache, for the tests' reset between cases.
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.
Returned to GET /api/remote/image by fetchProxiedImage, which sends
it back under contentType.
- bytes: Uint8Array
-
contentType: string
One of the raster types, without parameters.
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.
Returns the playlist listId as read from its page html; null when the
page has no ytInitialData or no track in it.
Returns the tracklist of the playlist listId, from the cache when it was
resolved in the last 6 hours.
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.
Checks that url may be reached: https on a public address, or anything
under GERBEUR_FEDERATION_ALLOW_PRIVATE.
Empties the caches, for the tests' reset between cases.
Trades code for an identity token at the token endpoint of the instance
at origin, and returns the token, unverified.
Returns the image at url, a user's avatar on their home instance, up to
the size an avatar may be.
Returns the public keys of the instance at origin, cached unless fresh
asks to refetch them and they weren't in the last minute.
Returns the metadata of the instance at origin, its issuer, cached. It
must name origin as its issuer and speak this protocol's version.
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.
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.
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.
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.
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.
Holds the images pasted into chat messages, comments, dump notes and descriptions. The startup tasks purge those nothing links to.
Holds each user's avatar.
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.
How often, in minutes, the /community page's figures are recomputed
(GERBEUR_COMMUNITY_REFRESH_MINUTES), 15 by default.
The SQLite database file (GERBEUR_DB_PATH). api/test/setup.ts points it at
a throwaway file before anything opens the database.
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.
The MIME type prefixes a file dump may have. dump-service also accepts the exact types of DUMP_ALLOWED_MIME_TYPES.
The documents and archives a file dump may be, beyond the prefixes.
Holds the file of each file dump.
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.
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.
Which ways federated sign-in goes, each on unless set to 0.
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.
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.
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.
The key lib/jwt.ts signs and verifies tokens with (GERBEUR_JWT_SECRET).
The server refuses to start without it.
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.
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.
The site's name (GERBEUR_SITE_NAME). It is filled into index.html, the OG
tags, the icons' manifest and the emails.
How long an attachment nothing links to is kept before startup purges it.
The page size when a request names none.
The largest page size a request may ask for.
Holds each playlist's cover.
The port the server listens on (GERBEUR_PORT, default 8000).
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.
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.
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.
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.
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.
Holds the thumbnails a poster uploads (<id>-custom) and those the
thumbnails route generates from file dumps.
How long an invite stays usable. invite-service refuses an older one, and the startup tasks purge it.
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.
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.
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.
Applies each migration db hasn't recorded yet, in order, each in its own
transaction.
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.
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.
Serves every query of the server, as its only connection. Its queries are synchronous, so each one blocks the event loop while it runs.
Reserves community at the URL root for the community page, renaming a
category that had it as its slug.
Run once by runMigrations.
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.
Run once by runMigrations. Existing users start with NULL, which revokes no
token.
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.
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.
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.
Run once by runMigrations. Existing comments start with no likes, and a
database made from schema.sql already has both.
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.
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.
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.
Run once by runMigrations. It doesn't scan the notes and comments already
posted, so their links are recorded only once they are edited.
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.
Run once by runMigrations. Existing users get no row, so they start with
the defaults, digests on, like a new account.
Makes room for moderators between users and admins. It adds users.role,
which replaces the is_admin flag, and 0005 then drops the flag.
Run once by runMigrations. Every user becomes a user, except those with
is_admin set, who become admins.
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.
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.
Adds categories, which admins create and posters file their dumps under, as
the categories table and the dump_categories join table.
Run once by runMigrations. Existing dumps start in no category, and a
database made from schema.sql already has both tables.
Adds chat_messages, which holds the messages of the site-wide chat.
Run once by runMigrations. A database made from schema.sql already has the
table.
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).
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.
Adds password_reset_tokens, which holds the single-use tokens that
password-reset-service emails to users who forgot their password.
Run once by runMigrations. A database made from schema.sql already has the
table.
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.
Run once by runMigrations. It fills only the keys still NULL, and leaves
NULL a URL that isn't http(s).
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.
Run once by runMigrations. Existing dumps keep their fetched thumbnails,
and a database made from schema.sql already has the column.
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.
Run once by runMigrations. Every existing category becomes a dump
category, filing the same dumps.
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.
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.
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.
Run once by runMigrations. It leaves alone an embed URL that already has a
start, and a row whose rich_content doesn't parse.
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.
Run once by runMigrations. Existing messages reply to nothing, and a
database made from schema.sql already has the column.
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.
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.
Prepares the chat's @everyone: it adds chat_messages.mentions_everyone,
and frees the name everyone, which no account may have any more.
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.
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.
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.
Drops users.is_admin, now that 0004 has copied it into users.role.
Run once by runMigrations, after 0004. A database made from schema.sql
never had the column.
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.
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.
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).
Run once by runMigrations. A database made from schema.sql already has
these indexes.
Deletes what has expired and seeds the first admin, each time connection.ts opens the database.
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.
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.
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.
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.
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.
Makes the server notify the same users the chat highlights and the editor suggests, by defining once what counts as an @mention.
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.
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).
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.
Types what crosses the wire for federated sign-in, by which a user signs in to one gerbeur instance with their account on another.
Makes the forms check the same limits on what users submit as the server enforces, by holding them in one place.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Renders the signed-in user's settings page, /settings, where they change
their password, their account's settings and this browser's.
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.
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.
Renders /community and /community/:view, from GET /api/community.
A view no period names is NotFound.
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.
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.
Renders the home page, which shows every dump, or one category's, in the "hot", "new", "journal" and "follow" tabs.
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=).
Passed to Index by the router, from the path.
-
categorySlug: string
Without it, the feed holds every dump. An unknown slug renders 404.
-
feedTab: FeedTab
The tab the path names, or none for the viewer's default one.
-
followView: FollowView
The view of the "follow" tab the path names, the users without one.
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.
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.
Renders the page a digest's unsubscribe link opens, where the reader turns that digest off with one click, signed in or not.
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.
Renders the sign-up page, which only works from an invite link. Members copy those links from their own profile page.
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.
Renders a playlist's page, where its owner and moderators also edit it and reorder its dumps.
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.
Passed to PlaylistDetail by App's route.
-
editing: boolean
Set at
/edit, where the editor is open.
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.
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.
Passed to RestrictedLoggedIn by App, around a route's page.
Guards App's routes that only make sense signed out, /login and
/register.
Renders children when no one is signed in, and redirects otherwise: to the
page a ?next= names (nextPath), or to /.
Passed to RestrictedGuest by App, around a route's page.
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.
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.
Renders a user's profile page, which is also where the signed-in user edits their own profile.
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.
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.
Renders /search?q= and /search/:searchTab?q=, for anyone. The results come
from GET /api/search, signed in when the viewer is, so they find their own
private dumps and playlists too. The dumps load a page at a time as the
"dumps" tab scrolls. A new q aborts the search under way. The socket keeps
the dumps and playlists found live, as edited or deleted, but adds none; a
reconnection runs the search again, for every page of dumps loaded.
Renders the page another gerbeur instance sends a user back to once they
let it sign them in here, /federation/callback.
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.
Renders the page where a dump's poster, or a moderator, edits or deletes it.
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.
Renders the sign-in page.
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.
Renders a dump's page, with its preview, comments, related dumps and the playlists holding it.
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.
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.
- categoryIds: string[]
-
comment: string
The poster's note on the dump, in Markdown.
-
commentCount: number
Deleted comments are not counted.
- createdAt: Date
- fileMime: string
- fileName: string
- fileSize: number
- id: string
- isPrivate: boolean
- kind: "url" | "file"
-
ownerUsername: string
The poster's current name, which the dump's path starts with.
- richContent: RichContent
-
slug: string
Names the dump in its page's path,
dumpPath(shared/paths.ts) building it with ownerUsername. Made from the title on creation, it changes only when edited, and is unique among its poster's dumps. -
thumbnailMime: string
Set when the poster uploaded a custom thumbnail.
- title: string
- updatedAt: Date
- url: string
- userId: string
- voteCount: number
Renders the 404 page, for any path App's route table doesn't match and for
a category slug Index doesn't know.
Renders the 404 page, with a link home. Anyone may see it.
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.
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.
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.
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.
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.
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.
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.
Returned by usePlaylistMemberships to AddToPlaylistModal and
DumpCreateModal, for their PlaylistMembershipPanel.
-
add: (membership: PlaylistMembership) => void
Puts first a playlist the panel just created with the dump in it.
- loading: boolean
-
memberships: PlaylistMembership[]
Empty while loading, and when the load failed.
-
toggle: (membership: PlaylistMembership) => Promise<void>
Adds the dump to the playlist or takes it out on the server, and shows it only once the server agrees. A refusal or a dropped connection changes nothing.
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.
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.
Returned by usePlaylist to PlaylistDetail.
-
actionError: string | null
It says why the server refused a removal, a reorder or a delete.
-
applyDetails: (playlist: Playlist) => void
Shows the playlist as
PlaylistEditFormsaved it. -
deletePlaylist: () => Promise<void>
Leaves for home once the playlist is deleted, or sets
actionError. -
fading: Record<string, FadeState>
It holds the dumps being removed, which stay listed while they fade. A dump in it can't be dragged, and during its
cooldownits removal can be undone. -
move: () => voidfrom: number,to: number
Moves the dump at
fromtoto, counted among the dumps not fading, on screen only, as a drag goes along. -
remove: (dumpId: string) => void
Fades the dump out, and takes it out of the playlist on the server when the fade ends, or when the page is left before.
-
saveOrder: () => void
Sends the order
moveleft, as a drag ends. - state: FetchState<PlaylistWithDumps>
-
undoRemove: (dumpId: string) => void
Keeps a dump that
removeis fading out. Nothing was sent yet.
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.
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.
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.
Returned by usePagedList.
-
loadMore: () => void
It goes to
useInfiniteScrollor to aLoadMoreButton. -
resync: () => void
Fetches again every page loaded so far and puts them in the list's place, keeping its length and so the scroll. The live lists call it on
useOnReconnect, for what changed while the socket was down. A page that fails leaves the list as it was. -
setItems: (fn: (items: T[]) => T[]) => void
Does nothing until loaded. Passed to the sync hooks.
- state: FetchState<PagedList<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.
Saves a feed page's state, with its scroll position, as the page is left, and restores the position when the page is back.
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.
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.
Loads a feed's next page when the end of the feed scrolls into view.
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.
Lets a user choose which feed tab the index opens on, remembered in this browser.
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.
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.
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.
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.
Stored as JSON by useFeedCache's saveState, and read back by the
hook when the page is next reached by going back.
- hasMore: boolean
-
items: T[]
It holds every page loaded so far, whose dates JSON turns into strings.
-
page: number
Loading more carries on from this page, the last loaded.
-
savedAt: number
In epoch milliseconds.
- scrollY: number
Returned by useFeedCache to a feed page.
-
cached: Omit<FeedCacheEntry<T>, "savedAt"> | null
It holds the saved feed when the page was reached by going back or forward and the feed was saved less than 10 minutes ago. It changes only with the key.
-
saveState: () => voidlist: SavedList<T>,scrollY: number
Usually called through
useScrollSave, as the page is left.
Passed to useFeedCache's saveState by the feed pages, from the
list they hold, without its loading flag.
Lets a live list put an item that left it, as by turning private, back at its old position when it returns.
Returns list with item at idx, or at the end when the list has since
shrunk below it, unless the list holds it already.
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.
Lets a removal made by mistake be undone before the item leaves the list, by timing the fade-out of items leaving a list.
Returns the class a fading card takes in phase, for DumpCard's
className, or undefined for a card that isn't fading.
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.
Returned by useFading to useUpvotedList and usePlaylist. The
list keeps showing an item while it is in fading.
-
cancelAll: () => void
Called when the list is replaced, as by another user's upvotes.
-
cancelFading: (id: string) => void
Called when the removal is undone, so that the item stays.
- fading: Record<string, FadeState>
-
startFading: (id: string) => void
Does nothing when
idis already fading.
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.
Gives components the theme that ThemeProvider holds.
Returns the style and color scheme, and their setters. The profile page's settings are the only place that changes them.
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.
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.
Passed to useThemeChoice by ThemeProvider, as a constant: one for
the style, one for the scheme.
- allowed: readonly T[]
-
attribute: string
The root element's attribute the stylesheets key on.
-
base: T
The value the stylesheets draw without the attribute, which is then left out.
-
fallback: T
What applies when nobody chose.
-
setting: "style" | "colorScheme"
The account setting that holds the choice.
-
storageKey: string
The localStorage key, which index.html's script reads too.
Returned by useThemeChoice to ThemeProvider.
-
local: T | null
This browser's own choice, which an account without one takes.
-
set: (next: T) => void
Applies
next, stores it here, and saves it to the account when signed in. - value: T
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.
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.
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.
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.
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.
Returns who is online, for the presence row.
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.
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.
Checks whether dump is an uploaded file the global player can play.
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.
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.
Gives MediaPlayer the waveform it draws on its Scrubber.
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.
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.
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.
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.
-
duration: number
In seconds.
- title: string
- videoId: string
Returned by useYouTubePlaylist to GlobalPlayer, which shows the tracklist and its transport when there is more than one row.
-
index: number
The playing row, or -1 while the player hasn't reported one.
- next(): void
- playAt(index: number): void
- previous(): void
-
ready: boolean
Whether the IFrame API drives the embed. Rows are clickable only then.
- rows: PlaylistRow[]
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.
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.
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.
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.
- embedUrl: string
-
siteName: string
Shown under the title in the player's header.
-
thumbnailUrl: string
Shown in the player's header, unless Bandcamp has album art of its own.
- title: string
-
type: string
The provider, as in
RichContent.type. -
url: string
The page, which native Bandcamp playback resolves.
Returned by usePlayRichContent to the cards that play a link.
-
pending: boolean
True while a Bandcamp page is being resolved.
-
playRichContent: () => Promise<boolean>rc: PlayableRichContent,dumpHref?: string
Plays
rc, its title in the player linking to the dump pagedumpHref. Resolves to false when nothing could play, so the caller can open the link instead.
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.
Returns a react-hook-form form made with options, plus submit, which
turns a failed request into the form's root error.
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.isSubmittingtrue, and on a thrown error surfaces a single root error viafriendlyFetchError. Pair it withexpectOkso API-level failures and network failures both become root errors through one code path. Returns theonSubmithandler to spread onto<form onSubmit={...}>.
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.
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.
Returned by useEmojiTrigger to TextEditor.
- closeEmoji: () => void
- emojiMode: EmojiMode
- emojiQuery: string
-
openEmojiBrowser: () => voidstart: number,end: number
Opens a
browsepicker, whose pick will replace[start, end). -
selectEmoji: (native: string) => void
Inserts the emoji
nativethroughreplaceand closes the picker. -
setEmojiQuery: (query: string) => void
Sets the query of the picker's search, in either mode.
-
syncEmojiTrigger: () => voidvalue: string,cursor: number,open: boolean
Follows an inline
:queryand closes the picker once the caret leaves it. The caller calls it on every change withopenset, and on every caret move without, since only typing may open the picker. It leaves abrowsepicker alone.
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.
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.
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.
Returned by useMentionAutocomplete to TextEditor.
- closeMention: () => void
-
handleMentionKeyDown: (e: MentionKey) => boolean
Handles the arrows, Enter, Tab and Escape while the list is shown, and returns whether it took the key. The caller must then leave the key alone.
-
handleMentionSelect: (username: string) => void
Inserts
@usernamethroughreplaceand closes the list. -
mentionOpen: boolean
Set only once a search has found someone.
- mentionResults: UserSuggestion[]
- mentionSelectedIndex: number
-
syncMention: () => voidvalue: string,pos: number,lookup: boolean
Follows the
@queryunder the caret, searches for it 150 ms after the last keystroke, and closes the list once the caret leaves it. The caller calls it on every change withlookupset, and on every caret move without, since only typing may search.
| "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.
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.
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.
Passed to useUserDumpFeed by the profile's overview and tabs.
-
categorySlug: string
Narrows the list to the category at this slug.
-
isOwner: boolean
Keeps the user's own private dumps in their list as they edit them.
-
source: Dump[]
Dumps at hand that a vote of the user's own puts first in their upvoted.
Returned by useUserDumpFeed.
-
cardClassName: (dump: Dump) => string | undefined
It goes to the list's
DumpCardList: on the upvoted list, it fades out a withdrawn upvote.
One of a profile's lists of dumps, a path segment of its API and page.
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.
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.
Passed to useDumpListSync by the lists that need more than edits and deletions.
-
addNew: boolean
Also adds a dump
belongsaccepts 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 bycreatedAt: 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.
Keeps a list of the dumps a user upvoted in step with their votes.
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.
Passed to useUpvotedList by useUserDumpFeed, for an upvoted list.
-
profileUserId: string | null
The list holds this user's upvotes. It is null while their profile loads.
-
setItems: (fn: (prev: Dump[]) => Dump[]) => void
Does nothing until the list is loaded.
-
source: Dump[]
It holds dumps already at hand, which a vote of the signed-in user on their own list puts first at once, before the socket's vote event. The profile passes its first page of the user's dumps.
Returned by useUpvotedList.
-
cardClassName: (dump: Dump) => string | undefined
It goes to
DumpCardList'scardClassName. -
isWithdrawn: (id: string) => boolean
Checks whether the vote on dump
idwas withdrawn since the list loaded. The profile'suseDumpListSynckeeps such a dump from coming back.
Makes a dump's card open the dump as a link would, and mark it visited.
Returns useCardLink's handlers for the card of dump, for DumpCard and
JournalCard: every way they open the dump marks it visited, which clears
its unread dot.
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.
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.
Returned by useProfile to UserPublicProfile.
-
isOwnProfile: boolean
It is false while the profile loads.
-
setUser: (fn: (user: PublicUser) => PublicUser) => void
It shows the header's edits, which the server echoes to other browsers.
- state: FetchState<PublicUser>
Gives components the categories of one kind, of those CategoriesProvider
loads once for the whole app.
Returns the categories of kind, dump or playlist, and a function that
refetches them. Under no CategoriesProvider, the list is empty.
Returned by useCategories: one kind's categories.
-
byId: Map<string, Category>
Resolves a dump's or playlist's
categoryIdsto its category chips. -
categories: Category[]
The categories of the kind, by position then slug.
-
isLoaded: boolean
Whether the first fetch has ended. It is also true after a network error, with no categories.
-
refresh: () => Promise<void>
Refetches both kinds. The category manager calls it after each change.
Keeps a list narrowed to one category, whatever the socket puts in it.
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.
Holds the categories picked in a form's CategorySelect.
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.
Lets a button open the chat, by reading ChatContext.
Returns the chat modal's open state, and the functions that open and close
it. Needs ChatProvider above, which App mounts around every page.
Gives components the session, and the functions that sign in, sign out and make requests with its token.
Returns the session and the functions that act on it. It must be called
under AuthProvider.
Returns the session, for a page that RestrictedLoggedIn guards, such as
DumpEdit, so it needn't check for a user.
Returned by useAuth to any component that needs the session. Its functions are stable until the session's token changes.
-
authFetch: () => Promise<Response>input: RequestInfo,init?: RequestInit
Calls
fetchwith the session's token, and a JSON content type unless the body is FormData. An expired token, or a 401, ends the session. An expired token answers a 401 without calling the server. -
authUpload: () => Promise<Response>url: string,body: FormData,onProgress: (fraction: number | null) => void,method?: string
Works like
authFetchfor a multipart upload that shows progress, as the dump creation dialog's file upload does. It goes through XHR, sincefetchreports nothing until the whole body is sent. It still answers aResponse, so callers useexpectOkas usual. -
login: (response: AuthResponse) => void
Starts a session from a sign-in or sign-up response, in every tab.
-
logout: () => void
Ends the session this render holds. A 401 can land after the session has moved on to fresh tokens (a password change, here or in another tab), and must not sign those out.
-
mediaToken: string | null
The token for file and thumbnail URLs, where it shows in the query string. A session from before media tokens falls back to the session token until AuthProvider fetches one.
-
settings: UserSettings | null
The user's settings, once
AuthProviderhas read them.useSettingsis the way to read and change them. - token: string | null
-
updateSession: (patch: Partial<Session>) => void
Merges
patchinto the session, in memory and in storage. The profile page uses it for a new avatar or email, the password dialog for fresh tokens. - user: SelfUser | null
Returned by useRequiredAuth. It is UseAuthResult with a user that can't be null.
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.
Returns the settings and their setter. It must be called under AuthProvider.
Returned by useSettings to whatever reads or changes settings.
-
settings: UserSettings | null
The settings, or null when signed out or before
AuthProviderhas read them from the server. -
updateSettings: (patch: UpdateSettingsRequest) => Promise<void>
Applies
patchat once, then saves it withPATCH /api/me/settings. A refusal puts the previous settings back and throws.
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.
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.
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.
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.
Measures the width an element has beside another in a shared row, for the
presence avatars in the feed's header (PresenceRow).
Freezes the page's scroll under Modal and ConfirmModal.
Keeps a page's active tab in its path, as one more segment, for the profile and search pages.
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.
Places a floating panel against its anchor with position: fixed, for
TextEditor's emoji picker and mention list.
Gives the profile page's settings the toggle for notification and chat sounds.
Tells a Follow button what the user follows, and follows or unfollows, by
reading FollowContext.
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.
Gives a component the community feed, by reading CommunityFeedContext.
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.
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.
The width of each image, by the place it is shown.
-
compactThumbnail: number
A compact thumbnail at its widest, in a dump card's preview; the rows that show it smaller take the same copy.
-
journal: { feature; wide; tall; square; }
A journal tile's image, by the tile's shape.
-
playerArtwork: number
The artwork in the global player's header.
-
playlistBanner: string
A playlist's cover as the banner of its page.
-
playlistThumb: number
A playlist's cover in a row of a list.
-
playlistTile: string
A playlist's cover as a tile of the grid, two to a row when narrow.
-
richThumbnail: string
A link's thumbnail beside its text, above it on a narrow screen.
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.
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.
Whether the server sends email. Without it the settings page shows the digest switches disabled, with a note why.
Whether users of other instances sign in here. Without it the login and register pages offer no other instance.
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.
Lets a deployment choose between Bandcamp's embed and the site's own player.
It reads the choice for usePlayRichContent.
A Bandcamp player. "embed" is Bandcamp's own iframe, "native" is the site's player on the resolved streams.
This deployment's Bandcamp player, read once when the app loads.
Gives every request of the frontend its base URL, and the feeds their page sizes.
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.
The community feed's endpoint, for CommunityFeedProvider.
The community endpoint, for the community page.
How many items a page of a feed or list asks the API for.
How many notifications a page of /notifications asks the API for.
The search endpoint, for the search page.
The socket's origin, for WSProvider. It is always absolute, since
WebSocket takes no relative URL.
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.
The tab the bare / and /<category> show visitors, and users who haven't
picked one in their settings.
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.
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.
Shares the chosen theme with every component. ThemeProvider fills this
context and useTheme reads it.
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.
The theme's context. Its default is only seen outside ThemeProvider.
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.
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.
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.
Returns a bus with no subscribers.
Tells the user the server refused their chat edit or delete. WSProvider
sets it as chatError, and ChatModal shows it for a few seconds.
-
id: number
Tells apart two errors with the same message.
- message: string
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.
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.
-
comment: Comment
Set when created or updated.
-
commentId: string
Set when deleted.
- dumpId: string
- type: "created" | "deleted" | "updated"
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.
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.
- body: string
-
createdAt: Date
When this client sent it, to find it in the history after a reconnect.
- error: string
-
nonce: string
Sent with
chat_sendand returned with the echo or the error. A retry gets a new one, so a late echo of the first attempt can't retire it. - replyTo: ChatReplyTarget
-
status: "sending" | "failed"
failedwhen the server refused it, the socket was down or closed, or no echo came in 10 s. A failed message may still have reached the server.
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.
-
dumpIds: string[]
Set when the dumps changed. It lists only the dumps this user may see, in order.
-
lastDumpAt: Date
With
dumpIds, when the latest of them was added; undefined when there is none. -
playlist: Playlist
Set when created or updated.
- playlistId: string
- type: "created" | "updated" | "deleted" | "dumps_updated"
-
userId: string
The owner's id, set when deleted.
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.
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.
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.
-
chatError: ChatError | null
Only for edits and deletes. A failed send shows on its pending message.
-
chatMessages: ChatMessage[]
Shows chat messages as they arrive. It holds the ones received this session, at most 200, which
ChatModalmerges with the history it fetches. A reconnect empties it while the chat is closed, sinceChatModalfetches everything again on open. -
chatReadAt: Date | null
Places the "new" divider when the chat opens. It holds the time of the newest message the user has seen, which the server keeps, and is null until the
welcome, and for a visitor. -
deleteChatMessage: (id: string) => void
Sends the deletion if the socket is open, as
editChatMessagedoes. -
deletedChatIds: Set<string>
Takes the messages deleted this session out of the history
ChatModalfetched. - discardChatMessage: (nonce: string) => void
- dismissChatError: () => void
-
editChatMessage: () => voidid: string,body: string
Sends the edit if the socket is open, else drops it. It shows once the server broadcasts it, or fails as
chatError. -
markChatRead: (at: Date) => void
Moves the read marker up to
at, the time of the newest message seen. It zeroesunreadChatCountat once and sends the marker at most once a second. A marker set offline goes out with the nextwelcome. - pendingChatMessages: PendingChatMessage[]
-
reconcileChat: () => voidmessages: ChatMessage[],from: Date,to: Date
Repairs the live buffer after a reconnect. It replaces its messages between
fromandtowithmessages, whichChatModalfetched. A message missing from them was deleted while the socket was down. A failed send found among them did reach the server, and leaves the pending list. - retryChatMessage: (nonce: string) => void
-
sendChatMessage: () => voidbody: string,replyTo?: ChatReplyTarget
Trims
bodyand sends it as a pending message. Ignores a blank body. -
setChatOpen: (open: boolean) => void
Spares the user a notification for a mention they read live. It tells the server whether the chat is open in a visible tab; while it is, the server sends no notification for a mention, and this client doesn't count the message unread.
-
setChatTyping: (typing: boolean) => void
Tells the server whether the user is typing, when the socket is open.
ChatModalresendstrueevery few seconds while typing continues, since the server forgets a typer after 6 s. -
typingUsers: OnlineUser[]
Shows who is typing. It includes the user themselves, whom
ChatModalfilters out. -
unreadChatCount: number
Drives the chat buttons' unread badge. It counts the messages from others after the read marker, which the server keeps, so the count agrees across tabs and devices.
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.
-
castCommentLike: () => voidcommentId: string,shownCount?: number
Likes a comment, as WSContextValue.castVote upvotes a dump.
-
castVote: () => voiddumpId: string,shownCount?: number
Upvotes a dump at once and sends
vote_cast. The server's answer confirms or reverts it, and so does a timeout of 5 s, which covers a closed socket.shownCountis the count on screen, used whenvoteCountshas none yet. Does nothing when the user already voted. -
clearUnreadNotifications: () => void
Zeroes the bell's count, once the notifications page has marked them read on the server.
-
connectionEpoch: number
Tells pages to fetch again what they missed while the socket was down. It goes up on each reconnect, never on the first connection.
-
deletedDumpIds: Set<string>
Takes deleted dumps off every list at once. It holds the dumps deleted this session, or made private by someone else; a dump made public again leaves it.
-
deletedPlaylistIds: Set<string>
Takes deleted playlists off every list at once, as
deletedDumpIdsdoes dumps. -
followsVersion: number
Keeps the Follow buttons right when the user follows or unfollows in another tab. It goes up on every follow change, in any tab, and
FollowProviderfetches the follow status again. -
injectDump: (dump: Dump) => void
Shows a dump this user just posted on the index without waiting for its broadcast.
DumpCreateModalcalls it, and it adds the dump torecentDumps. -
likeCounts: Record<string, number>
As
voteCounts, for comments. -
myLikes: Set<string>
As
myVotes, for comments. -
myVotes: Set<string>
Marks the dumps this user upvoted, for their vote buttons. The server's
welcomeresets it on every connection. -
recentDumps: Dump[]
Shows new dumps on the index as they are posted, merged by date with the feed
Indexfetched. It holds, newest first and at most 100, the ones the server announced this session, which are public, and the ones this user posted, which may be private. -
removeCommentLike: () => voidcommentId: string,shownCount?: number
Withdraws a like, as WSContextValue.castVote casts a vote.
-
removeVote: () => voiddumpId: string,shownCount?: number
Withdraws an upvote, as WSContextValue.castVote casts one.
-
unreadNotificationCount: number
Drives the bell's badge. It comes from the
welcome, then each new notification counts it up, andnotifications_readzeroes it. -
voteCounts: Record<string, number>
Shows vote counts live. It holds, by dump id, the counts the server pushed or this tab changed optimistically; a dump missing from it shows its fetched
voteCount. -
wsErrorMessage: string | null
Set while the socket is down, for the banner that says so.
- wsStatus: "connecting" | "connected" | "disconnected"
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.
-
emit: <K extends keyof WSEvents>() => voidkind: K,event: WSEvents[K]
Calls the handlers of
kindnow, in the order they subscribed. -
subscribe: <K extends keyof WSEvents>() => () => voidkind: K,handler: (event: WSEvents[K]) => void
Calls
handlerwith each event ofkind, until the returned call.
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.
Carries who is online, for PresenceRow. WSProvider provides it through
WSPresenceContext, and components read it with useWSPresence.
-
onlineUsers: OnlineUser[]
The signed-in users with a socket open, each once.
Lets a component render outside WSProvider, with an empty chat.
Lets a component render outside WSProvider, with nobody online.
Lets a component render outside WSProvider, as if the socket were
connecting.
Carries the chat's live stream and read state. Read it with useWSChat.
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.
Carries the bus WSProvider emits its events on, for useWSEvent. Outside
WSProvider, it is a bus no one emits on.
Carries who is online. Read it with useWSPresence.
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.
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.
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.
Set by WSProvider for a user whose profile changed, and read by Avatar,
where it wins over the possibly stale props the page fetched.
- hasAvatar: boolean
-
version: number
The user's
updatedAtin ms, or the arrival time without one. It busts the cached image.
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.
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.
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.
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.
-
autoplay: boolean
Whether the current item starts on mount. A restored session appears paused, since no user gesture is behind it; a fresh
play()autoplays. -
current: PlayerItem | null
Always
queue[queueIndex], or null when nothing plays. - hasNext: boolean
- hasPrevious: boolean
- next(): void
-
onEnded(): void
Called by MediaPlayer at its media's end, to play the next item.
-
onError(): void
Called by MediaPlayer when its media fails, to re-resolve a stream.
-
onPlayStateChange(playing: boolean): void
Called by MediaPlayer when its media starts or stops.
-
onTimeUpdate(): voidtime: number,duration: number
Called by MediaPlayer as its media plays.
-
play(item: PlayerItem): void
Plays
itemalone, replacing the queue. -
playAt(index: number): void
Plays the queue's item at
index, or restarts it when it is current. -
playQueue(): voiditems: PlayerItem[],startIndex?: number
Replaces the queue with
itemsand plays it fromstartIndex. - playing: boolean
-
previous(): void
Restarts the item after its first 3 seconds, else plays the previous one.
-
queue: PlayerItem[]
The queue. Every playback is a queue of at least one item, so single tracks and albums share one code path.
- queueIndex: number
-
resolving: boolean
True while a stream's URLs are re-resolved, which GlobalPlayer shows.
-
seekRef: { current: ((t: number) => void) | null; }
Where the mounted MediaPlayer registers its seek handle, for
seekTo. -
seekTo(time: number): void
Seeks a file or stream to
timeseconds. An embed doesn't move. -
startTime: number
Where MediaPlayer starts the current item, in seconds: not 0 only for an item restored from a previous session, or resumed after a re-resolve.
-
stop(): void
Empties the queue, which closes the player.
-
togglePlay(): void
Pauses or resumes a file or stream. An embed has its own controls.
-
toggleRef: { current: (() => void) | null; }
Where the mounted MediaPlayer registers its play/pause handle.
The fields of every PlayerItem, which GlobalPlayer's header shows.
-
artworkUrl: string
A raw URL, which
Thumbnailproxies when it must. -
dumpHref: string
The dump page the header's title links to.
-
subtitle: string
Shown under the title. It holds the artist, the site's name or the file's name.
- title: string
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.
-
currentTime: number
In seconds.
-
duration: number
In seconds, 0 until the media's metadata loads.
& { kind: "embed"; embedUrl: string; type: string; }
| (
& { kind: "file"; fileUrl: string; mimeType: string; waveformUrl?: string; }
| (
& { kind: "stream"; streamUrl: string; type: string; duration?: number; trackNum?: number; resolveUrl: string; resolveIndex: number; resolvedAt: number; embedUrl?: string; }
Built by the cards and previews that start playback, and held in
PlayerProvider's queue, which localStorage keeps across reloads. An embed
plays in the provider's iframe; a file (an upload) and a stream play in
MediaPlayer.
Given by PlayerTimeContext outside PlayerProvider.
Given by PlayerContext outside PlayerProvider: nothing plays and no control does anything. The tests' render helper starts from it.
The player's state and controls, from PlayerProvider.
The playhead of the current file or stream, from PlayerProvider.
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.
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.
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.
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.
Shares the site's categories, of both kinds, with every component.
CategoriesProvider fills this context and useCategories reads one kind's
from it.
Provided by CategoriesProvider to useCategories, for the category
switchers and pickers, the dump and playlist pages and cards, and the
category manager.
-
categories: Category[]
Every category, dump and playlist ones, in the server's order, by kind, position then slug.
-
isLoaded: boolean
Whether the first fetch has ended. It is also true after a network error, with no categories.
-
refresh: () => Promise<void>
Refetches the list. The category manager calls it after each change.
The categories' context. Its default, an empty list never loaded, is only
seen outside CategoriesProvider.
Lets the header's chat button and the floating one open the same chat modal,
which ChatProvider owns. Read it with useChat.
Provided by ChatProvider through ChatContext. The modal closes
itself, so opening is all its callers need.
-
openChat: () => void
Does nothing for a visitor.
Opens the chat modal. Outside ChatProvider it does nothing.
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.
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.
Shares the session with every component. AuthProvider fills this context
and useAuth reads it; nothing else should touch it.
Provided by AuthProvider to useAuth, which builds the session's API on
it.
- authResponse: Session | null
-
setAuthResponse: Dispatch<SetStateAction<Session | null>>
Replaces the session in memory only. Its callers store the new session themselves, with
storeSession, so other tabs follow.
The session's context. Its default, a signed-out session whose setter does
nothing, is only seen outside AuthProvider, as in some tests.
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.
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.
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.
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.
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.
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.
useCommunityFeedcalls it.
Provided by CommunityFeedProvider, and read with useCommunityFeed.
Given to a consumer outside CommunityFeedProvider: no feed.
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.
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.
Tells the Follow buttons what the signed-in user follows. FollowProvider
provides this context and useFollows reads it.
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.
- followPlaylist: (playlistId: string) => Promise<boolean>
- followUser: (userId: string) => Promise<boolean>
- followedPlaylistIds: Set<string>
- followedUserIds: Set<string>
-
isLoaded: boolean
False until the follows are fetched, and always for a visitor. The sets are empty until then, so a button would show "Follow" for a followed user. A failed fetch keeps it false, and is tried again 10 s later.
- unfollowPlaylist: (playlistId: string) => Promise<boolean>
- unfollowUser: (userId: string) => Promise<boolean>
Given to a consumer outside FollowProvider, for which nothing is
followed.
Tells the Follow buttons what the signed-in user follows. Read it with
useFollows.
Ranks dumps for the "hot" and "journal" tabs of the index page.
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.
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.
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.
Checks whether dump has a non-blank note, which JournalCard shows as a
pull-quote when the dump has no image.
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.
Returned by composeJournal. JournalFeed renders each block as its
own grid, with a rule between two blocks.
-
entries: JournalEntry[]
In reading order.
-
key: string
The id of the block's first dump, stable as more pages load.
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.
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.
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.
Returns the list holding only its first page, items.
Returns a setter of a list's items as a setter of the list, for the sync hooks, which edit items only.
Returns url asking for page page, limit items long.
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.
Returns the page res answers, its items deserialized.
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.
Returns the list as useFeedCache's cached restores it.
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.
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.
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.
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.
- hasMore: boolean
- items: T[]
- loadingMore: boolean
-
page: number
Loading more carries on from this page, the last loaded.
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.
Remembers volume and muted for the next media element, and in
localStorage when it is available.
Returns the volume and mute state last remembered, or full and unmuted.
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.
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.
Passed to resolveBandcampQueue by its callers and copied onto every item of the queue, since the server's tracklist knows nothing of the dump.
- dumpHref: string
-
embedUrl: string
Lets PlayerProvider fall back to Bandcamp's iframe if the streams fail.
-
fallbackArtworkUrl: string
Used only if the page carries no album art of its own.
Formats track lengths for the media player and the global player's tracklist.
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.
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.
Returns the peaks of source, from 0 to 1.
Returns the key loads keeps source's peaks under.
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.
Reads, prepares and lists YouTube playlist embeds, for GlobalPlayer and useYouTubePlaylist to show and drive a playlist's tracklist.
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.
Returns the titles and lengths of the videos of playlist listId, from
GET /api/remote/youtube, since the embed exposes neither.
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.
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.
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.
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.
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.
Remembers the emoji picker's skin tone across pickers and pages. It lives in
localStorage, and EmojiPickerPopup reads and writes it.
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.
Names the skin tone in localStorage. EmojiPickerPopup writes under it.
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.
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.
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.
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.
Removes the stored draft, once the dump is posted or the poster chose to start afresh.
Checks whether draft holds nothing worth restoring. A title taken from the
preview doesn't count, since the preview gives it back.
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.
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.
Saved by DumpCreateModal on every change, and restored when it next opens
without a shared link. Posting the dump clears it.
- categoryIds: string[]
-
comment: string
In Markdown.
- isPublic: boolean
- title: string
-
titleFromUser: boolean
Whether the poster typed
title. When not, the modal takes the title from the link's preview again. -
url: string
As typed, not yet normalized.
The modal's starting values, when no draft is restored.
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.
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.
Checks whether draft can be sent or saved. Like the server, it measures
the trimmed text.
Returns body as one line of plain text, for the reply bar above the
composer and the reply line above a message.
The length from which the composer and the edit box show their character
counter, near VALIDATION.CHAT_MESSAGE_MAX.
Finds the mentions of the signed-in user in a chat message.
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.
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.
Returns the stored draft of userId, or an empty one when there is none,
userId is undefined, or storage is blocked.
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.
Returned by readDraft to ChatModal, which passes it back to
writeDraft.
Formats the chat modal's dates, in the interface's locale, for its day separators and the times beside grouped messages.
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.
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.
Checks whether a and b fall on the same local day.
Checks the signed-in user's permissions, for the pages and components that hide what the server would refuse.
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.
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.
Forgets the sign-in this tab started, once it is being finished.
Keeps the state of the sign-in that authorizeUrl starts.
Returns whether state is that of the sign-in this tab started.
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.
Returns the /login path that comes back to path after signing in, a
path on this site with its query.
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.
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.
Returns the host of handle's home instance, or undefined for a local
user.
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.
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.
Keeps the user signed in across reloads and tabs, in one localStorage entry
every tab shares. AuthProvider and useAuth are its only users.
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.
Checks whether token, a JWT, has expired, without asking the server. An
unreadable one counts as expired.
Returns the stored session, or null. A session that is unreadable, expired or from before roles existed is removed from storage.
Stores session, for every tab.
& { 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.
The localStorage key of the session. AuthProvider listens for changes to it
from other tabs.
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.
Returns the cadence token names, or null when it isn't an unsubscribe
token at all.
Shows a date as the time since it, beside every dump, comment, message and notification.
Formats a file dump's size and name for display, in the file fields, the previews and the dump creation dialog.
Gives every component that loads from the API the same states, so that
ListState renders loading, error and empty alike everywhere.
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.
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.
Routes other sites' images through the API's image proxy,
GET /api/remote/image, for Thumbnail and ThumbnailPlaceholder.
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.
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.
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.
Gives the character counters of the text fields and TextEditor their
warning states.
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.
Fetches the user a profile page is about.
Remembers in localStorage which dumps and playlists this browser has opened, for the unread dot on the dump and playlist cards.
Renders the user's playlists as toggles of whether each holds a dump, in AddToPlaylistModal and in the last step of the dump form.
Renders the playlists as checkboxes, which the keyboard toggles too, and a button that opens PlaylistCreateForm to make one holding the dump.
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.
- dumpId: string
- loading: boolean
- memberships: PlaylistMembership[]
-
onPlaylistCreated: (membership: PlaylistMembership) => void
Called with a playlist created from the panel, which holds the dump.
- onToggle: (membership: PlaylistMembership) => void
Renders the dumps of a playlist's page, which those who may edit it drag into a new order and remove.
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.
Passed to PlaylistDumpList by PlaylistDetail, from usePlaylist.
The calls are given only to those who may edit the playlist.
- dumps: Dump[]
- fading: Record<string, FadeState>
-
onMove: () => voidfrom: number,to: number
Counts only the dumps not fading, which alone can be dragged.
-
onMoved: () => void
Called as a drag ends, to save the order
onMoveleft. - onRemove: (dumpId: string) => void
- onUndoRemove: (dumpId: string) => void
Renders a list of playlists as cards.
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.
Passed to PlaylistCardList by every list of playlist cards.
-
grid: boolean
Lays the cards out as tiles, cover first, several to a row, for a page that is a list of playlists and nothing else: the playlists page.
-
onDelete: (playlist: Playlist) => void
Given, each card shows a delete button, which calls it.
- playlists: Playlist[]
-
privateBadgeOnly: boolean
Passed to each PlaylistCard.
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.
Renders a PlaylistMembershipPanel in a modal, for the playlists
usePlaylistMemberships loads and toggles.
Passed to AddToPlaylistModal by the dump page.
Renders the form that edits a playlist's details, categories and cover, in place of the header of its page.
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.
Passed to PlaylistEditForm by PlaylistDetail.
- onCancel: () => void
-
onRequestDelete: () => void
Opens the page's confirmation, which deletes the playlist.
-
onSaved: (playlist: Playlist) => void
Called with the playlist as the server now has it.
- playlist: PlaylistWithDumps
Renders the form that creates a playlist, in NewPlaylistButton's modal and in PlaylistMembershipPanel, with the playlist categories to file it under.
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.
Passed to PlaylistCreateForm by NewPlaylistButton and PlaylistMembershipPanel.
- className: string
-
dumpId: string
Added to the new playlist, which the submit button says.
- onCancel: () => void
-
onCreated: () => voidplaylist: Playlist,hasDump: boolean
hasDumptells whetherdumpIdmade it into the new playlist.
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.
Renders one tab per entry of tabs, sort being the active one.
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.
-
leading: ReactNode
Goes before the tabs, as the playlists page's category menu does.
- onChange: (sort: T) => void
-
sort: T
The active tab.
-
tabs: readonly T[]
The tabs offered, in order.
Renders the card that shows a playlist in profiles, search results, the playlists page and the dump page's "In collections" section.
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.
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.
Renders the header of a playlist's page, with its cover, title, description, visibility, owner, dates and categories.
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.
Passed to PlaylistHeader by PlaylistDetail.
-
onEdit: () => void
Given to those who may edit it, with
onSaved: it adds an "Edit" button, which opensPlaylistEditForm. -
onSaved: (playlist: Playlist) => void
Called with the playlist as the server has it once its title or description is edited in place, which it lets its editors do.
- playlist: PlaylistWithDumps
Renders the tab bar of the index page, with the category switcher before the tabs.
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.
Passed to FeedTabBar by Index, from the URL it renders.
-
categorySlug: string
The category whose feed is shown, or none for every dump.
-
followView: FollowView
The view of the "follow" tab, which picking a category keeps.
-
tab: FeedTab
The tab shown, the viewer's default one on a bare path.
Shows who is online, as a row of avatars next to the index page's tabs.
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.
Passed to PresenceRow by Index, which renders one in the header
and one below it. Both selectors go to useRoomBeside.
-
fitBeside: string
The tabs, which keep their width before the avatars get any.
-
fitWithin: string
The ancestor that bounds the row the avatars share.
Renders what follows the last item of an infinitely scrolled list.
Renders the "journal" tab of the index page, which lays the feed out as a mosaic of tiles of several sizes.
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.
Renders the "followed" tab of the index page, which shows the dumps of the users and the playlists the signed-in user follows.
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.
Passed to FollowedFeed by Index, which renders the tab for a
signed-in user only. Unlike the other tabs, it loads its own dumps.
-
categorySlug: string
Scopes both views to a category.
-
view: FollowView
The view the path names:
/follow, or/follow/playlists.
Renders the "hot" and "new" tabs of the index page, from the feed Index
loads and passes down as MainFeedProps.
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.
Passed to DumpFeed by Index for the "hot" and "new" tabs. It adds
the active tab, as order, to the state all tabs share.
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.
-
dumps: Dump[]
It holds the loaded dumps, with those
WSContextadded since and without the deleted ones. -
sentinelRef: Ref<HTMLDivElement>
It comes from
useInfiniteScroll, and goes after the last card. -
state: DumpFeedState
The tabs read its status and paging, and show
dumpsinstead of its items.
Held by usePagedList for Index's feed and for each section of
FollowedFeed.
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.
Renders the icon of a video's fullscreen button.
Renders the pause icon, the play button's other state.
Renders the play icon, nudged right so the triangle looks centred.
Renders the mute button's icon, crossed when muted.
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.
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.
Passed to Scrubber by MediaPlayer and FilePreview. Times are in seconds.
- current: number
- duration: number
-
onScrub: (time: number | null) => void
Called with the time under the pointer while it holds the scrubber, and with null once it lets go, so MediaPlayer's clock can follow the drag.
-
onSeek: (time: number) => void
Called once a click, a drag or a key lands somewhere.
-
peaks: number[] | null
The waveform's heights from 0 to 1; without them, a plain bar.
-
slim: boolean
Draws the thin bar of a video's controls instead of the audio track.
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.
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.
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.
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.
Passed to MediaPlayer by GlobalPlayer, which wires the callbacks and the handles to PlayerContext, and by the file previews, which pass only the media.
- autoplay: boolean
- kind: "audio" | "video"
-
onEnded: () => void
Called at the media's end, where GlobalPlayer plays the next item.
-
onError: () => void
Called when the media fails to load or decode, where a stream's URL is re-resolved.
- onPlayStateChange: (playing: boolean) => void
-
onTimeUpdate: () => voidtime: number,duration: number
-
poster: string
Shown before a video plays.
-
seekRef: { current: ((t: number) => void) | null; }
Receives its seek handle while mounted, for PlayerContext's
seekTo. - src: string
-
startTime: number
In seconds. Read on mount only: later seeks go through
seekRef. -
toggleRef: { current: (() => void) | null; }
Receives its play/pause handle while mounted, for
togglePlay. -
waveform: PeaksSource
Where an audio file's waveform comes from. Without it, as for a stream whose bytes another site serves, audio seeks on a plain bar.
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 ✎.
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.
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.
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.
-
countFrom: number
Where the Markdown editor's counter starts, near
maxLength. -
enterSaves: boolean
In the Markdown editor, saves on Enter, a line breaking on Shift+Enter, as the chat's composer does. Ctrl/Cmd+Enter saves in either case.
-
fieldClassName: string
Added to the field: for a one-line one, the class of the text it edits, such as
dump-title, so that it takes on its type in every style. -
label: string
Names a one-line field, such as "Edit title".
-
onClose: () => void
Called once the text is saved, or left: the field is to unmount.
-
onSave: (value: string) => Promise<void> | void
Saves the trimmed text. The field closes once it is done, and stays open under the error it throws. A text left as it was isn't passed to it.
-
returnFocusTo: React.RefObject<HTMLElement | null>
Takes the focus once the field is gone, if it had it and no one has taken it since: the button that opened it, back in its place.
Passed to InlineEdit by the component showing the text, which
renders it as children and saves it in onSave.
- children: ReactNode
-
className: string
Added to the wrapper, which the text and the field both sit in.
-
editable: boolean
Without it,
childrenshow alone. -
fieldClassName: string
Added to the field: see InlineEditFieldProps.
-
kind: "text" | "email" | "markdown"
A one-line field (
text,email), where Enter saves, or the Markdown editor (markdown), where Ctrl/Cmd+Enter does. Escape cancels in both. -
label: string
Names the action, such as "Edit title": the text's tooltip.
- maxLength: number
-
onSave: (value: string) => Promise<void>
Saves the trimmed text, and shows it: the field closes once it resolves. A thrown error, such as
expectOk's, stays open under the field. A text left as it was closes the field without calling it. - placeholder: string
-
required: boolean
Refuses to save an empty text.
-
value: string
The text as saved, which the field opens on.
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.
Renders a Segmented bound to the field name, under its label. It
must sit under a react-hook-form FormProvider.
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.
Passed to SegmentedField by a form, or by VisibilityToggle.
Passed to VisibilityToggle by a dump or playlist form.
- disabled: boolean
- label: string
-
name: Path<T>
A boolean field, true for public.
Renders the file field of the dump forms, a FileDropZone bound to react-hook-form.
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.
Passed to FileField by a dump form. The field it names holds the
File itself, or null.
- disabled: boolean
- hint: string
- label: string
-
maxBytes: number
Refuses a larger file, as a field error, as soon as it is picked.
- name: Path<T>
-
onValueChange: (file: File | null) => void
Called after the form's value is set, for a side effect such as deriving the title from the file's name.
- rules: RegisterOptions<T, Path<T>>
-
showLimit: boolean
States
maxBytesin the drop zone.
Renders a one-line text field bound to react-hook-form, for the account forms, a dump's title and a playlist's.
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.
Passed to TextField by a form that holds a string field.
- autoComplete: string
- autoFocus: boolean
- disabled: boolean
- label: string
-
labelAction: ReactNode
Shown at the right of the label, such as a reset button.
-
maxLength: number
Stops the typing at this length, through the input's own
maxLength. UnlikeRichTextField's, it adds no rule, so a value set from code isn't checked. - name: Path<T>
- placeholder: string
- rules: RegisterOptions<T, Path<T>>
-
showCount: boolean
Shows an
n / maxLengthcounter, only whenmaxLengthis set. - type: "text" | "email" | "password" | "url" | "search"
Renders the row of buttons that ends a form, in the pages and dialogs that edit something.
Renders the Cancel button, if any, then the children, aligned right. The
Cancel button is type="button", so it never submits the form.
Passed to FormActions by a form, with its submit button as child.
-
cancelLabel: ReactNode
Replaces the "Cancel" label.
- children: ReactNode
-
disabled: boolean
Disables the Cancel button only, not the children.
-
onCancel: () => void
Adds a Cancel button, which calls it.
Renders the zone that takes a dump's file, by drop or by picker. FileField
binds it to a form.
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.
Passed to FileDropZone by FileField, from the form's state. The
zone keeps no file of its own.
- disabled: boolean
- file: File | null
-
hint: string
Replaces the "Drop a file here" prompt.
-
maxBytes: number
Stated under the prompt when
showLimitis on.FileFieldrefuses a larger file. -
onChange: (file: File | null) => void
Called with the file dropped or picked, or with null when it is removed.
- showLimit: boolean
Renders a TextEditor bound to react-hook-form, for the forms that take a Markdown text (a dump's note, a playlist's description).
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.
Passed to RichTextField by a form that holds a string field.
- autoResize: boolean
- disabled: boolean
-
editorRef: React.Ref<TextEditorHandle>
The editor's ref, for a caller that focuses it.
- label: string
-
maxLength: number
Counted in the editor and checked on submit, with a message. It overrides a
maxLengthinrules. - name: Path<T>
-
onSubmit: () => void
Called on Ctrl/Cmd+Enter.
- placeholder: string
- rows: number
- rules: RegisterOptions<T, Path<T>>
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.
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.
Passed to SlugField by DumpEdit's and PlaylistEditForm's forms,
which send the slug only when it differs from saved.slug.
-
inputClassName: string
For a form outside
.form, whose inputs take their own class. -
name: Path<T>
The form's slug field.
-
prefix: string
The page's path up to the slug, such as
/@bob/dumps/. -
saved: { title: string; slug: string; }
The item as saved, whose slug a title put back to its own restores.
-
titleName: Path<T>
The form's title field, which the slug follows until it is typed in.
Renders the error of a failed submit, for the forms built with
useApiForm.
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.
Passed to FormError by a form, to say what failed.
-
title: string
Replaces the "Something went wrong" title.
Renders the @mention suggestions that TextEditor portals to <body>.
useMentionAutocomplete fetches them and tracks the highlighted one.
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.
Passed to MentionDropdown by TextEditor, from the state of
useMentionAutocomplete.
-
id: string
The textarea's
aria-controls. Optionigets the id${id}-${i}, which the textarea names as itsaria-activedescendant. - onSelect: (username: string) => void
- results: UserSuggestion[]
- selectedIndex: number
-
style: CSSProperties
From
useAnchoredFloat, or hidden until it has placed the list.
Renders the emoji picker that TextEditor portals to <body>, built on
frimousse. TextEditor owns its state and its keyboard handling.
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).
Passed to EmojiPickerPopup by TextEditor, which keeps the refs to
detect outside presses, focus the search and step the keyboard into the grid.
- floatRef: Ref<HTMLDivElement>
-
inline: boolean
Set for a
:querytyped in the textarea. The search field then only mirrorsqueryand can't take focus. - onClose: () => void
-
onKeyDownCapture: (e: KeyboardEvent<HTMLDivElement>) => void
Runs before frimousse sees the key, so it can take Escape and Enter.
- onQueryChange: (query: string) => void
- onSelect: (emoji: string) => void
- query: string
- searchRef: Ref<HTMLInputElement>
-
style: CSSProperties
From
useAnchoredFloat, or hidden until it has placed the picker. -
viewportRef: Ref<HTMLDivElement>
Focused by
TextEditorto hand the arrow keys to frimousse.
Renders the Markdown that TextEditor writes, wherever a dump's note, a
comment, a chat message or a description is shown.
Passed to Markdown by any component that shows a user's text.
- children: string
- className: string
-
everyone: boolean
Shows
@everyoneas a mention, from a chat message'smentionsEveryone. Without it,@everyoneis 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.
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.
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.
Exposed through the ref of TextEditor, so that the component that opens an editor (a reply, an edit, the chat composer) can focus it.
-
focus(options?: { atEnd?: boolean; preventScroll?: boolean; }): void
Focuses the textarea.
atEndputs the caret after the last character and scrolls the text there.preventScrollleaves the page and its scrollers where they are.
Passed to TextEditor by the component that owns the text, which keeps it in its state (the editor is controlled).
-
aria-describedby: string
Joined with the id of the editor's own upload notice when it shows one.
- aria-invalid: boolean
-
autoResize: boolean
Grows the textarea with its text instead of scrolling it.
- className: string
-
countFrom: number
The length from which the counter shows, 80% of
maxLengthby default: a standing "0 / 5000" is noise, but the counter should turn up well before the warning colour does. - disabled: boolean
- id: string
-
maxLength: number
Shows a counter and marks the textarea invalid past it. It doesn't stop the typing, so the caller must still refuse a longer text.
-
mentionEveryone: boolean
Offers
@everyoneamong the mention suggestions. The chat's composer sets it for a role withchat:everyone, the one place it notifies. -
onCancel: () => void
Called on Escape when no popup of the editor is left to close. Inline editors (an edit, a reply) pass it to mean "never mind".
-
onChange: (value: string) => void
Called on typing, and with the Markdown of the images once uploaded.
-
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
Runs after the editor's own keys (autocompletes, Ctrl+Enter), and never while an IME is composing, so an Enter that confirms a candidate isn't taken for "send".
-
onSubmit: () => void
Called on Ctrl/Cmd+Enter, but not while an image is still uploading.
- placeholder: string
- rows: number
- value: string
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.
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.
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.
Renders the other gerbeur instances the signed-in user let their account
sign in to, on /settings.
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.
Renders the way in for users of another gerbeur instance, under the login form and, with the invite, under the register form.
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.
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.
Renders dumps as cards wired to the live votes, one at a time or as a list.
Renders dumps as a .dump-feed of LiveDumpCard.
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.
Passed to DumpCardList by every plain list of dump cards.
-
cardClassName: (dump: Dump) => string | undefined
Gives a card a class, as
fadeClassNamegives a fading one. - dumps: Dump[]
Passed to LiveDumpCard by DumpCardList, and by
PlaylistDumpList, which wraps each card in its drag handle and buttons.
-
className: string
It comes from
fadeClassNamefor a fading card. - dump: Dump
Renders a dump as a tile of the "journal" tab's mosaic.
Passed to JournalCard by JournalFeed, with the vote state Index
takes from WSContext, as for DumpCard.
- canVote: boolean
-
castVote: () => voidid: string,shownCount?: number
- dump: Dump
-
isOwner: boolean
Hides the unread dot and shows the "private" badge.
-
removeVote: () => voidid: string,shownCount?: number
-
shape: JournalShape
From
composeJournal. It sets the tile's grid span, not its look. -
voteCount: number
The socket's live count if it has one, else
dump.voteCount. - voted: boolean
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.
Previews the file picked in the dump creation modal, before it is uploaded.
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.
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.
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.
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.
-
initialUrl: string
A link shared from Android's share sheet, which
AppHeaderpasses on fromIndex. It replaces any saved draft. - onClose: () => void
Shows which panel of the dump creation modal is open, and declares the panels in the order they are walked.
Renders the three panels as numbered steps, the passed ones ticked.
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.
Renders the title field of the dump creation modal, read-only until the poster chooses to edit it.
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.
Passed to TitleField by DumpCreateModal, which owns the editing
state: it marks the title as the poster's own on onEdit.
- disabled: boolean
- editing: boolean
- onEdit: () => void
-
onReset: () => void
Puts the suggested title back. Without it, no reset button shows.
- registration: UseFormRegisterReturn<"title">
Tells the poster, in the dump creation modal, that the link they typed was already dumped.
Renders the "already dumped" hint. It doesn't block posting, since a repost is sometimes on purpose.
Passed to DuplicateNotice by DumpCreateModal, with the matches of
GET /api/dumps?url=.
-
matches: DumpUrlMatch[]
Non-empty, latest first: the notice links the first one.
-
onNavigate: () => void
Closes the modal when the poster follows the link.
Renders the card that shows a dump in the feeds, search results, profiles and playlists.
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.
- canVote: boolean
-
castVote: () => voidid: string,shownCount?: number
- className: string
- dump: Dump
-
isOwner: boolean
Hides the unread dot and shows the "private" badge.
-
removeVote: () => voidid: string,shownCount?: number
-
voteCount: number
The socket's live count if it has one, else
dump.voteCount. - voted: boolean
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).
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.
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.
Passed to RichContentCard by DumpCard (compact), Dump,
DumpEdit and DumpCreateModal, the last two without a dump to play from.
-
compact: boolean
Shows a card's thumbnail alone, as a play button or a link out.
-
dumpHref: string
The dump's page, which the global player's title links to.
- richContent: RichContent
-
thumbnailOverrideUrl: string
The dump's uploaded thumbnail, shown instead of the provider's and used as the player's artwork.
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.
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.
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.
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.
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.
Passed to ProfileDumpsTab by UserPublicProfile.
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.
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.
Passed to ProfileManageTab by UserPublicProfile, which shows the
tab on another user's profile to those who hold user:manage.
-
onUserSaved: (user: PublicUser) => void
Called with the user as the server left them.
- user: PublicUser
Renders the header of a user's profile, where the signed-in user also edits their own avatar and email.
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.
Passed to ProfileHeader by UserPublicProfile, once loaded.
- isOwnProfile: boolean
-
onUserChange: (user: PublicUser) => void
Called with the user as a new avatar left them.
- user: PublicUser
Renders one labelled setting of the settings page and the profile's "manage" tab.
Renders label beside children, as a row of the
.profile-appearance-grid its caller wraps the rows in.
Passed to SettingRow by Settings, ProfileManageTab and
RoleSwitcher.
-
children: ReactNode
It is the row's control, usually a
Segmented. - label: ReactNode
Renders the "invitees" tab of a profile, which shows whom the user invited, and whom they invited in turn.
Renders the invitees of userId as a tree.
Passed to ProfileInviteesTab by UserPublicProfile.
-
state: FetchState<Invitee[]>
Loaded when the tab is first opened, from
GET /api/users/@:username/invitees. - userId: string
Shows a user's role on their profile, and lets those who hold user:manage
change it.
Renders the role badge next to the username in ProfileHeader, for
moderators and admins only.
Renders the user's role as a Segmented whose clicks save it to
PATCH /api/users/:id/role, which checks the permission again.
Passed to RoleSwitcher by ProfileManageTab, which shows it only to
those who hold user:manage, on another user's profile.
- currentRole: Role
-
onSaved: (role: Role) => void
Called with the role the server kept.
- userId: string
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.
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.
Passed to ProfilePlaylistsTab by UserPublicProfile.
Renders a profile's "follow" tab, which shows the users the user follows at
/@name/follow and the playlists at /@name/follow/playlists.
Renders the two views under a row of tabs that switch between their paths, each view loading its list when first shown.
Passed to ProfileFollowTab by UserPublicProfile.
- user: PublicUser
-
view: FollowView
The view the path names, after
/follow.
Renders the password change dialog, which the settings page opens.
Renders the form that changes the user's password, given the current one. The change signs out every other session, in every browser.
Renders a profile's description, which the signed-in user edits in place on their own profile.
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.
Passed to ProfileDescription by UserPublicProfile.
- description: string | undefined
- isOwnProfile: boolean
-
onSaved: (description: string | undefined) => void
Called with the saved description, undefined once cleared.
Renders a titled section of a profile page.
Renders a .profile-section: its title row, children, then a
"View all →" link to viewAllHref.
Passed to ProfileSection by the profile's tabs, and by the settings and manage pages.
-
actions: ReactNode
They go at the end of the title row.
- children: ReactNode
- title: ReactNode
-
viewAllHref: string
It links to the page with the whole list, of which the section shows the first page. The profile leaves it out while the list is empty.
Renders the field that renames a user, on a profile's "manage" tab, which
those with user:manage see: users can't rename themselves.
Renders the name in a text input with a "Rename" button, which is enabled once the name differs.
Passed to UsernameField by ProfileManageTab.
-
save: (username: string) => Promise<string>
Sends the new name to
PATCH /api/users/:key/username, and resolves to the name the server kept. A refusal rejects with its message, which the field shows. - username: string
Renders a profile's overview, its bare /@name, which shows the first page
of the user's dumps beside those they upvoted.
Renders the first page of both lists, from useUserDumpFeed, each linking
to its tab. The owner also gets a "New dump" button.
Passed to ProfileOverviewTab by UserPublicProfile.
Renders the action of the signed-in user's own profile header, which invites someone.
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.
Renders the categories a dump or playlist is filed under, on its card and its page.
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.
Passed to CategoryChips by DumpCard and Dump for a dump, and by
PlaylistCard and PlaylistHeader for a playlist.
-
className: string
Added to the row's own class, for a card's grid to place it.
-
ids: string[]
The dump's or playlist's
categoryIds. -
kind: CategoryKind
Whether
idsname dump or playlist categories. -
onClick: (e: React.MouseEvent) => void
Called on a click in the row. A clickable card stops it there, or a chip's click would open the card too.
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.
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.
Passed to CategorySwitcher by FeedTabBar, ProfileDumpsTab,
ProfilePlaylistsTab, Playlists and Search, which keep the category in
their URL.
-
categorySlug: string
The category shown, or none for every dump or playlist.
-
kind: CategoryKind
The set it offers: dump categories for dumps, playlist ones for playlists.
-
onSelect: (slug: string | undefined) => void
Called with the slug of the category picked, or undefined for "All".
Renders the editor of the site's categories of one kind, dump or playlist,
on /manage for a user with the category:manage permission.
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.
Passed to CategoryManager by Manage, once for each kind.
-
kind: CategoryKind
The set it edits, and the kind of the categories it creates.
Renders the category picker of the dump creation dialog and the dump editor, and of the playlist forms.
Renders the categories of kind as toggle chips, any number of which may be
on. It renders nothing while the site has none.
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.
- disabled: boolean
-
kind: CategoryKind
The set it offers: dump categories for a dump, playlist ones for a playlist.
- onToggle: (id: string) => void
- selected: Set<string>
Renders the frame of every page but the index and search pages, which
render AppHeader themselves.
Renders AppHeader, then the page's content as the <main>.
Passed to PageShell by a page, around its content.
-
centered: boolean
Centers the content, for a lone card such as a form or an error.
- children: ReactNode
-
narrow: boolean
Narrows the content to the column the user menu's and the bell's pages share, under a
PageHeader.
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.
Renders the floating "new dump" button, which App mounts once over every
page.
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.
Renders the search form of the header.
Renders the search form. AppHeader renders two over one state, one in its
center and one in the row that slides open under it on small screens, so the
query survives crossing the breakpoint. Opening it focuses the input.
Passed to SearchBar by AppHeader, which holds the query and decides
where a search leads.
- expanded: boolean
-
onCancel: () => void
Called when Escape is pressed in the input.
-
onSubmit: (q: string) => void
Called with the trimmed query, never an empty one.
- onValueChange: (v: string) => void
- value: string
Renders the title row of the pages the user menu and the bell lead to: settings, manage, notifications and every playlist.
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.
Passed to PageHeader by Settings, Manage, Notifications and
Playlists.
-
children: ReactNode
It goes at the end of the row, such as the notifications' unread count or the playlists' category menu and order tabs.
-
icon: string
An emoji before the title, hidden from screen readers.
- title: ReactNode
Renders one labelled group of a page under a PageHeader: a
settings section, the category manager, a day of notifications.
Renders a <section> headed by label as its h2.
Passed to PageGroup by Settings, Manage and Notifications.
-
children: ReactNode
Usually a
.page-card, or the notifications' list of cards. -
label: ReactNode
It shows as a small capitalized label, ruled to the column's end.
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.
Renders the header at the top of every page, through PageShell or directly
from the index and search pages.
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.
Passed to AppHeader by the index page. The other pages leave these props out.
-
centerSlot: ReactNode
Shown beside the search. The index page puts its feed tabs there.
-
disableNew: boolean
Set by the index page while its feed fails to load.
-
initialDumpUrl: string
Opens the dump creation dialog at mount, with this link. The index page passes the link shared to the app from Android's share sheet.
Renders the floating chat button, which App mounts once over every page.
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.
Renders the error a page shows in place of its content when what it loads fails or doesn't exist.
Renders an error card as the whole page, under the header, so the user can still navigate away.
Passed to PageError by a page whose load failed.
-
actions: ReactNode
Buttons under the message, such as a retry.
- message: string
-
title: string
Defaults to "Something went wrong".
Renders the notification bell of the header, for signed-in users.
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.
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.
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.
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.
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.
Passed to PendingItem by ChatModal for each of WSChatContext's
pendingChatMessages.
-
grouped: boolean
Set when it continues the user's run, which hides the avatar and name.
- onDiscard: (nonce: string) => void
- onJumpTo: (id: string) => void
-
onRestore: (pending: PendingChatMessage) => void
Takes the failed text back into the composer, and discards it.
- onRetry: (nonce: string) => void
- pending: PendingChatMessage
-
user: PublicUser
The signed-in user, who sent it.
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.
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.
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.
Renders the line above a chat message, sent or pending, that quotes the message it replies to.
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.
Passed to ReplyRef by ChatMessageItem, from the message's
replyTo* fields, and by PendingItem, from its replyTo.
-
author: string
Undefined when the replied-to message was deleted.
-
body: string
Possibly cut short by the server, as
replyToBodyis. - onJumpTo: (id: string) => void
- replyToId: string
Renders one message of the chat modal's list, with its reply, edit and delete actions.
Renders a chat message, or the editor over its text while editing. Its
height eases between the two. Deleting asks for confirmation first.
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.
- canDelete: boolean
- canEdit: boolean
- editing: boolean
- flashed: boolean
-
grouped: boolean
Set when the message continues its author's run. It then has no avatar or author line: its time shows in the avatar's place on hover, and its actions float over its top.
-
mentionsMe: boolean
Highlights a message that @mentions the user,
@everyoneincluded, or replies to them. - message: ChatMessage
-
onDelete: (id: string) => void
Called once the user has confirmed.
-
onEdit: () => voidid: string,body: string
Called only when the text changed.
- onEndEdit: () => void
- onJumpTo: (id: string) => void
- onReply: (message: ChatMessage) => void
- onStartEdit: (id: string) => void
-
replyDeleted: boolean
Set when the replied-to message was deleted live, after the server filled in
replyToAuthor.
Renders the box that replaces a chat message's text while its author, or a moderator, edits it in the chat modal.
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.
Passed to ChatEditBox by ChatMessageItem while it is editing.
- initial: string
-
onClose: () => void
Called once the text is saved, or left, Escape included.
-
onSave: (body: string) => void
Receives the trimmed text, only when it changed and can be sent.
Renders the dialog frame of the app's dialogs: creating a dump, the chat, adding to a playlist, changing a password.
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.
Renders the dialog that confirms a deletion, on the pages and in the dialogs that delete something.
Renders a choice among a few values as a row of buttons, in the
.visibility-toggle style.
Renders a user's avatar, or their initial, wherever a user is named: cards, comments, chat, profiles and mention suggestions.
Renders a loaded list, or in its place the loading, error or empty state.
Renders the row of tabs of the index, profile and search pages.
Renders an image that is also its own upload button, for a dump's thumbnail and a playlist's cover.
Renders a dump's preview image from another site, in the cards and the player, falling back to a generated placeholder.
Renders the button that loads a list's next page, where the list has no room to load it on scroll.
Renders an error as a card, in place of what failed to load or save.
Renders a tooltip on hover. Every caller uses it to give the full date over a relative time or an "edited" mark.
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.
Renders user as an <li> of a .user-card-list, with the follow button,
which FollowUserButton hides on the viewer's own card.
Passed to UserCard by the profile's follow tab and the community page's new members.
-
note: ReactNode
A line under the name, such as who invited them.
- user: PublicUser
Renders the list of users behind a count. VoteButton opens it on a dump's
voters, and LikeButton on a comment's likers.
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.
Passed to UserListPopover by the button whose count was clicked. The
button owns the open state and unmounts the popover in onClose.
- anchorRef: React.RefObject<HTMLElement | null>
-
fetchUrl: string
It answers
PaginatedData<PublicUser>, a page at a time, and the popover addspageandlimitto its query. -
onClose: () => void
Called on an outside click, on Escape, and on a scroll outside it.
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.
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.
Passed to CommunityFeed by whatever shows the feed.
-
feed: CommunityFeedData
From
useCommunityFeed. -
online: OnlineUser[]
From
useWSPresence: the feed opens on them. -
tabIndex: number
Given to every link.
-1takes them out of the tab order, for a second copy of the list such as the one the marquee's loop needs.
Renders a comment of a dump's thread and, recursively, its replies.
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.
Passed to CommentNode by CommentThread for each top-level comment,
and by CommentNode for each reply. The callbacks are CommentThread's,
passed down unchanged.
- comment: Comment
- currentUser: SelfUser | null
-
depth: number
Sets the replies'
--depth: the deeper, the narrower their indent. - dumpId: string
- onCommentCreated: (comment: Comment) => void
- onCommentDeleted: (commentId: string) => void
- onCommentUpdated: (comment: Comment) => void
-
tree: Map<string, Comment[]>
The replies of every comment, by parent id, from
CommentThread.
Renders the comments under a dump on its page, /@:username/dumps/:slug.
Renders the comment count, the form that posts a comment when someone is signed in, and the tree of CommentNode.
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.
-
comments: Comment[]
Every comment on the dump, flat and oldest first, deleted ones included so their replies keep their place.
- currentUser: SelfUser | null
- dumpId: string
- onCommentCreated: (comment: Comment) => void
- onCommentDeleted: (commentId: string) => void
- onCommentUpdated: (comment: Comment) => void
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.
Renders a comment editor and its submit button, disabled while the text is
blank or longer than VALIDATION.COMMENT_BODY_MAX.
Passed to CommentForm by CommentThread, for a new comment, and by
CommentNode, for a reply.
-
cancelWhenEmpty: boolean
Shows Cancel only once there is text to throw away, and doesn't cancel on Escape. For the new-comment form, which has nothing to close.
-
className: string
Applies to the
inlinelayout only. - editorRef: React.Ref<TextEditorHandle>
- errorTitle: string
-
layout: "inline" | "top"
topputsleadingbeside the editor, for the new-comment form.inlineis for replies, inside a comment. - leading: React.ReactNode
-
onCancel: () => void
Without it, the form has no Cancel button.
-
onSubmit: (body: string) => Promise<void>
Sends the text. The form empties itself once it resolves. When it throws, as
expectOkdoes, the form keeps the text and shows the error undererrorTitle. - pendingLabel: React.ReactNode
- placeholder: string
- submitLabel: React.ReactNode
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.
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.
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.
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.
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.
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.
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.
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.