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.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user