v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 50s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 22s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-28 07:23:03 +00:00
parent b85a71d86e
commit 563daa720c
171 changed files with 3921 additions and 851 deletions

View File

@@ -1,8 +1,14 @@
/**
* The session, and requests made with it.
*
* @module
*/
import { useCallback, useContext } from "react";
import { AuthContext } from "../../contexts/auth/AuthContext.ts";
import type { AuthResponse } from "../../../shared/user.ts";
import type { AuthResponse, SelfUser } from "../../../shared/user.ts";
import type { Session } from "../../utils/auth/storage.ts";
import {
forgetStoredSession,
@@ -10,7 +16,56 @@ import {
storeSession,
} from "../../utils/auth/storage.ts";
export const useAuth = () => {
/** What {@link useAuth} returns. */
export interface UseAuthResult {
/** The signed-in user, null when signed out. */
user: SelfUser | null;
/** The session token. */
token: string | null;
/**
* The token for file and thumbnail URLs, where it is visible in the query
* string. Sessions from before media tokens fall back to the session token
* until AuthProvider fetches one.
*/
mediaToken: string | null;
/** Starts a session from a sign-in or sign-up response, in every tab. */
login: (response: AuthResponse) => 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.
*/
logout: () => void;
/** Merges `patch` into the current session, in memory and in storage. */
updateSession: (patch: Partial<Session>) => void;
/**
* `fetch` with the session's token, and a JSON content type unless the body
* is FormData. An expired token, or a 401, ends the session; the former
* answers a synthetic 401 without calling the server.
*/
authFetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
/**
* `authFetch` for multipart uploads that need a progress bar.
*
* `fetch` reports nothing until the whole body is on the wire, which for a 50
* MB dump on a phone is a long silence — so this goes through XHR, whose
* `upload.progress` events fire as bytes leave. The result is wrapped back
* into a real `Response` so callers keep using `expectOk` unchanged.
*
* `onProgress` receives a 0–1 fraction, or `null` once the body is fully sent
* and we are waiting on the server (the length is unknown for chunked bodies,
* and "100%, still waiting" reads as a stall).
*/
authUpload: (
url: string,
body: FormData,
onProgress: (fraction: number | null) => void,
method?: string,
) => Promise<Response>;
}
/** Returns the session and what acts on it. */
export function useAuth(): UseAuthResult {
const { authResponse, setAuthResponse } = useContext(AuthContext);
const login = useCallback((response: AuthResponse) => {
@@ -27,7 +82,6 @@ export const useAuth = () => {
forgetStoredSession(sessionToken);
}, [setAuthResponse, sessionToken]);
/** Merge into the current session, in memory and in storage. */
const updateSession = useCallback((patch: Partial<Session>) => {
setAuthResponse((prev) => {
if (!prev) return prev;
@@ -70,18 +124,6 @@ export const useAuth = () => {
return res;
}, [authResponse?.token, logout]);
/**
* `authFetch` for multipart uploads that need a progress bar.
*
* `fetch` reports nothing until the whole body is on the wire, which for a
* 50 MB dump on a phone is a long silence — so this goes through XHR, whose
* `upload.progress` events fire as bytes leave. The result is wrapped back
* into a real `Response` so callers keep using `expectOk` unchanged.
*
* `onProgress` receives a 0–1 fraction, or `null` once the body is fully sent
* and we are waiting on the server (the length is unknown for chunked bodies,
* and "100%, still waiting" reads as a stall).
*/
const authUpload = useCallback((
url: string,
body: FormData,
@@ -133,9 +175,6 @@ export const useAuth = () => {
return {
user: authResponse?.user ?? null,
token: authResponse?.token ?? null,
// For file and thumbnail URLs, where the token is visible in the query
// string. Sessions from before media tokens fall back to the session token
// until AuthProvider fetches one.
mediaToken: authResponse?.mediaToken ?? authResponse?.token ?? null,
login,
logout,
@@ -143,9 +182,20 @@ export const useAuth = () => {
authFetch,
authUpload,
};
};
}
export const useRequiredAuth = () => {
/** What {@link useRequiredAuth} returns: {@link UseAuthResult} with a user. */
export interface UseRequiredAuthResult extends UseAuthResult {
/** The signed-in user. */
user: SelfUser;
}
/**
* Returns the session, for a page behind a sign-in.
*
* @throws {Error} When no one is signed in: the route should have guarded it.
*/
export function useRequiredAuth(): UseRequiredAuthResult {
const {
user,
token,
@@ -172,4 +222,4 @@ export const useRequiredAuth = () => {
authFetch,
authUpload,
};
};
}