fix(notifications): a notice keeps what to say and says it in the reader's language when shown
This commit is contained in:
@@ -37,6 +37,7 @@ import VerifyBanner from "./VerifyBanner";
|
||||
// pulls the release-notes loader into the eager App chunk and defeats the lazy ReleaseNotes
|
||||
// split. FRONTEND_VERSION equals the newest release note's version by release convention (both bump together).
|
||||
import { FRONTEND_VERSION } from "../lib/version";
|
||||
import { msg } from "../lib/notifications";
|
||||
|
||||
// The page modules (and their side rails) are code-split per route: each module's definition imports
|
||||
// them lazily (features/navigation/moduleRegistry), and so does the feed's for the channel page. App
|
||||
@@ -94,7 +95,7 @@ export default function App() {
|
||||
// Also from the signed-out login page, where the save 401s — `req` marks that as handled, so
|
||||
// the reporter stays quiet there.
|
||||
saveLanguage(queryClient, code).catch((e: unknown) =>
|
||||
notifyActionError(e, t("errors.writeFailed.prefs")),
|
||||
notifyActionError(e, msg("errors.writeFailed.prefs")),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,13 @@ import { afterEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
|
||||
const notify = vi.fn();
|
||||
vi.mock("../lib/notifications", () => ({ notify: (...args: unknown[]) => notify(...args) }));
|
||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
||||
notify: (...args: unknown[]) => notify(...args),
|
||||
}));
|
||||
|
||||
import i18n from "../i18n";
|
||||
import { msg, sayNotif, type NotifText } from "../lib/notifications";
|
||||
import { clear, events } from "../lib/log";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
|
||||
@@ -31,8 +35,10 @@ it("a crash is reported in words the reader reads; the engine's text goes to the
|
||||
);
|
||||
|
||||
expect(notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: "fatal", message: i18n.t("errors.boundary.notifMessage") }),
|
||||
expect.objectContaining({ level: "fatal", message: msg("errors.boundary.notifMessage") }),
|
||||
);
|
||||
const [shown] = notify.mock.calls[0] as [{ message: NotifText }];
|
||||
expect(sayNotif(shown.message)).toBe(i18n.t("errors.boundary.notifMessage"));
|
||||
expect(JSON.stringify(events().filter((e) => e.event === "ui.crash"))).toContain(
|
||||
"Cannot read properties of undefined",
|
||||
);
|
||||
|
||||
@@ -8,7 +8,10 @@ import { afterEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
|
||||
const notify = vi.fn();
|
||||
vi.mock("../lib/notifications", () => ({ notify: (...args: unknown[]) => notify(...args) }));
|
||||
vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../lib/notifications")>()),
|
||||
notify: (...args: unknown[]) => notify(...args),
|
||||
}));
|
||||
|
||||
import { clear, events } from "../lib/log";
|
||||
import { installStaleBuildReload } from "../lib/staleBuild";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
import i18n from "../i18n";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { msg, notify } from "../lib/notifications";
|
||||
import { CrashBoundary, type CrashCopy } from "../components/ui/CrashBoundary";
|
||||
|
||||
const copy = (): CrashCopy => ({
|
||||
@@ -13,10 +13,10 @@ const copy = (): CrashCopy => ({
|
||||
const onCrash = () =>
|
||||
notify({
|
||||
level: "fatal",
|
||||
title: i18n.t("errors.boundary.notifTitle"),
|
||||
title: msg("errors.boundary.notifTitle"),
|
||||
// Never the raw `error.message`: a JS engine's English ("Cannot read properties of undefined…")
|
||||
// is no message for the reader — the `ui.crash` log carries it for diagnosis.
|
||||
message: i18n.t("errors.boundary.notifMessage"),
|
||||
message: msg("errors.boundary.notifMessage"),
|
||||
requiresInteraction: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import i18n from "../i18n";
|
||||
import { api, HttpError, type Me } from "../lib/api";
|
||||
import * as actionErrors from "../lib/actionErrors";
|
||||
import { sayNotif } from "../lib/notifications";
|
||||
import NavSidebar from "./NavSidebar";
|
||||
|
||||
vi.mock("../features/navigation/navigationContext", () => ({
|
||||
@@ -119,7 +120,7 @@ describe("the account panel", () => {
|
||||
fireEvent.click(row(panel, "Sign out"));
|
||||
|
||||
await vi.waitFor(() => expect(report).toHaveBeenCalledOnce());
|
||||
expect(report.mock.calls[0]![1]).toBe("Couldn't sign you out");
|
||||
expect(sayNotif(report.mock.calls[0]![1])).toBe("Couldn't sign you out");
|
||||
expect(reload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ArrowLeftRight, Info, LogOut, Settings, Shield, type LucideIcon } from
|
||||
import { api, type Me } from "../lib/api";
|
||||
import { displayName, roleKey } from "../lib/api/account";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { getUnreadCount, subscribe } from "../lib/notifications";
|
||||
import { getUnreadCount, msg, subscribe } from "../lib/notifications";
|
||||
import type { Page } from "../lib/urlState";
|
||||
import {
|
||||
moduleDef,
|
||||
@@ -80,7 +80,7 @@ export default function NavSidebar({
|
||||
const signedOut = await api.logout().then(
|
||||
() => true,
|
||||
(err: unknown) => {
|
||||
notifyActionError(err, t("errors.writeFailed.signOut"));
|
||||
notifyActionError(err, msg("errors.writeFailed.signOut"));
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
|
||||
import { clear, events } from "../lib/log";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { notify, raw } from "../lib/notifications";
|
||||
import { reportError } from "../lib/errorDialog";
|
||||
import { createQueryClient } from "../lib/queryClient";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
@@ -23,8 +23,8 @@ const logged = (code: string) => events().filter((e) => e.event === code);
|
||||
|
||||
describe("client log wiring", () => {
|
||||
it("records every notification at its level", () => {
|
||||
notify({ level: "success", title: "Saved", message: "Playlist renamed" });
|
||||
notify({ level: "error", message: "Could not reach someone@mail.example" });
|
||||
notify({ level: "success", title: raw("Saved"), message: raw("Playlist renamed") });
|
||||
notify({ level: "error", message: raw("Could not reach someone@mail.example") });
|
||||
expect(logged("ui.notify").map((e) => [e.level, e.msg, e.attrs])).toEqual([
|
||||
["INFO", "Playlist renamed", { level: "success", title: "Saved" }],
|
||||
["ERROR", "Could not reach [email]", { level: "error" }],
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Me } from "../lib/api";
|
||||
import type { Page } from "../lib/urlState";
|
||||
import { notify, removeByMetaKind } from "../lib/notifications";
|
||||
import { msg, notify, removeByMetaKind } from "../lib/notifications";
|
||||
import { focusAccessRequestsTab } from "../lib/adminUsersTab";
|
||||
|
||||
type SetPage = (next: Page, then?: () => void) => void;
|
||||
@@ -30,8 +30,8 @@ export function useAccessRequestsNudge(me: Me | null | undefined, setPage: SetPa
|
||||
if (pending > 0) {
|
||||
notify({
|
||||
level: "warning",
|
||||
title: t("common.accessRequestsTitle"),
|
||||
message: t("common.accessRequestsMessage", { count: pending }),
|
||||
title: msg("common.accessRequestsTitle"),
|
||||
message: msg("common.accessRequestsMessage", { count: pending }),
|
||||
// Durable inbox link (survives reload, unlike a live action callback); the toast also
|
||||
// gets a click via the action.
|
||||
meta: { kind: "access-requests" },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import type { Me } from "../lib/api";
|
||||
import { clearParams, type Page } from "../lib/urlState";
|
||||
import { useAccountSwitchNotice } from "../lib/useAccountSwitchNotice";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { msg, notify } from "../lib/notifications";
|
||||
import { focusAccessRequestsTab } from "../lib/adminUsersTab";
|
||||
import { focusSettingsTab } from "../lib/settingsTab";
|
||||
|
||||
@@ -46,13 +46,13 @@ export function useEntryParams(
|
||||
if (!result) return;
|
||||
linkAnswered.current = true;
|
||||
if (result === "ok") {
|
||||
notify({ level: "success", message: t("settings.account.googleLink.linked") });
|
||||
notify({ level: "success", message: msg("settings.account.googleLink.linked") });
|
||||
void refetchMe();
|
||||
focusSettingsTab("account");
|
||||
setPage("settings");
|
||||
} else {
|
||||
const key = result === "conflict" || result === "mismatch" ? result : "error";
|
||||
notify({ level: "error", message: t(`settings.account.googleLink.${key}`) });
|
||||
notify({ level: "error", message: msg(`settings.account.googleLink.${key}`) });
|
||||
}
|
||||
clearParams("link");
|
||||
}, [t, setPage, refetchMe]);
|
||||
@@ -66,7 +66,7 @@ export function useEntryParams(
|
||||
if (oauthAnswered.current) return;
|
||||
if (new URLSearchParams(window.location.search).get("oauth") !== "cancelled") return;
|
||||
oauthAnswered.current = true;
|
||||
notify({ level: "info", message: t("settings.account.googleCancelled") });
|
||||
notify({ level: "info", message: msg("settings.account.googleCancelled") });
|
||||
clearParams("oauth");
|
||||
}, [t]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user