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,4 +1,10 @@
import React, { useEffect, useState } from "react";
/**
* The notifications page.
*
* @module
*/
import { useEffect, useState } from "react";
import { Link } from "react-router";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
@@ -27,6 +33,9 @@ import { deserializeNotification } from "../utils/shared/deserialize.ts";
import { PageShell } from "../components/layout/PageShell.tsx";
import { expectOk, friendlyFetchError } from "../utils/shared/api.ts";
/**
* The page's state: loading, failed, or loaded with a page of notifications.
*/
type State =
// `arrived`: sent over the socket before the list came back — the list may
// predate them, and the read-all that follows the load covers them anyway.
@@ -40,6 +49,7 @@ type State =
loadingMore: boolean;
};
/** The icon a notification shows. */
type NotifIconKind =
| "upvote"
| "follow"
@@ -49,6 +59,7 @@ type NotifIconKind =
| "comment"
| "like";
/** Returns the icon for a notification of `type`. */
function notifIconKind(type: Notification["type"]): NotifIconKind {
switch (type) {
case "dump_upvoted":
@@ -70,24 +81,28 @@ function notifIconKind(type: Notification["type"]): NotifIconKind {
}
}
/** Renders the upvote glyph. */
const UpvoteSvg = () => (
<svg viewBox="0 0 10 10" width="11" height="11" fill="currentColor">
<polygon points="5,1 9,9 1,9" />
</svg>
);
/** Renders the follow glyph. */
const FollowSvg = () => (
<svg viewBox="0 0 10 10" width="10" height="10" fill="currentColor">
<polygon points="2,1 9,5 2,9" />
</svg>
);
/** Renders the like glyph. */
const HeartSvg = () => (
<svg viewBox="0 0 10 10" width="10" height="10" fill="currentColor">
<path d="M5 9 C2 6.5 0 5 0 2.8 C0 1.2 1.2 0 2.6 0 C3.6 0 4.5 0.6 5 1.5 C5.5 0.6 6.4 0 7.4 0 C8.8 0 10 1.2 10 2.8 C10 5 8 6.5 5 9 Z" />
</svg>
);
/** Renders the icon of a notification of `type`. */
function NotifIcon({ type }: { type: Notification["type"] }) {
const kind = notifIconKind(type);
const glyphs: Record<NotifIconKind, React.ReactNode> = {
@@ -106,6 +121,7 @@ function NotifIcon({ type }: { type: Notification["type"] }) {
);
}
/** Returns where notification `n` leads. */
function notificationLink(n: Notification): string {
const data = n.data as NotificationData;
switch (n.type) {
@@ -140,6 +156,7 @@ function notificationLink(n: Notification): string {
}
}
/** Returns the sentence notification `n` shows. */
function notificationContent(n: Notification): React.ReactNode {
const data = n.data as NotificationData;
switch (n.type) {
@@ -231,16 +248,19 @@ function notificationContent(n: Notification): React.ReactNode {
}
}
/** Returns how long ago `date` was within a week, else its day. */
function timeAgo(date: Date): string {
const abs = Math.abs(Date.now() - date.getTime()) / 1000;
if (abs < 7 * 86400) return relativeTime(date);
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
}
/** Returns the local midnight that starts the day of `d`, in epoch ms. */
function startOfDay(d: Date): number {
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
}
/** Returns `items` grouped under Today, Yesterday and Earlier. */
function groupByDate(
items: Notification[],
): { label: string; items: Notification[] }[] {
@@ -263,7 +283,12 @@ function groupByDate(
.map((label) => ({ label, items: buckets[label] }));
}
export function Notifications() {
/**
* Renders `/notifications`, for a signed-in user: their notifications by day,
* new ones as they arrive, loading more on demand. Opening the page marks them
* all read.
*/
export function Notifications(): React.JSX.Element {
useDocumentTitle(t`Notifications`);
const { authFetch } = useAuth();
const { clearUnreadNotifications, lastNotification } = useWS();