fix(notifications): a notice keeps what to say and says it in the reader's language when shown
This commit is contained in:
@@ -30,10 +30,11 @@ vi.mock("../../lib/downloadQueue", () => ({ showInTray: (id: number) => showInTr
|
||||
const notifyActionError = vi.fn();
|
||||
vi.mock("../../lib/actionErrors", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/actionErrors")>();
|
||||
return { ...actual, notifyActionError: (e: unknown, t: string) => notifyActionError(e, t) };
|
||||
return { ...actual, notifyActionError: (e: unknown, t: NotifText) => notifyActionError(e, t) };
|
||||
});
|
||||
|
||||
import ClipPanel, { type ClipCrop, type ClipSource } from "./ClipPanel";
|
||||
import { msg, type NotifText } from "../../lib/notifications";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -188,7 +189,7 @@ describe("ClipPanel", () => {
|
||||
display_name: "The bit",
|
||||
});
|
||||
expect(notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: "success", message: "clip.queued" }),
|
||||
expect.objectContaining({ level: "success", message: msg("clip.queued") }),
|
||||
);
|
||||
// Its View opens the tray on the new clip.
|
||||
(
|
||||
@@ -214,7 +215,7 @@ describe("ClipPanel", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "clip.cut" }));
|
||||
await waitFor(() => expect(notifyActionError).toHaveBeenCalledTimes(1));
|
||||
expect(notifyActionError.mock.calls[0]![0]).toBe(err);
|
||||
expect(notifyActionError.mock.calls[0]![1]).toBe("clip.failed");
|
||||
expect(notifyActionError.mock.calls[0]![1]).toEqual(msg("clip.failed"));
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
expect((screen.getByRole("button", { name: "clip.cut" }) as HTMLButtonElement).disabled).toBe(
|
||||
false,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Crop, Crosshair, Maximize, Minus, Plus, Repeat, Scissors, X } from "luc
|
||||
import { api, type ClipRequest } from "../../lib/api";
|
||||
import { DISPLAY_NAME_MAX } from "../../lib/api/downloads";
|
||||
import { invalidateDownloads } from "../../lib/downloads";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { msg, notify } from "../../lib/notifications";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { showInTray } from "../../lib/downloadQueue";
|
||||
import { log } from "../../lib/log";
|
||||
@@ -197,14 +197,14 @@ export default function ClipPanel({
|
||||
log.info("clip.queued", "clip queued", { source: src.source, seconds: Math.round(length) });
|
||||
notify({
|
||||
level: "success",
|
||||
message: t("clip.queued"),
|
||||
message: msg("clip.queued"),
|
||||
action: { label: t("clip.view"), onClick: () => showInTray(job.id) },
|
||||
});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
// A refusal the server explains (too long, quota) is already in the error dialog; a lost
|
||||
// connection or a 404 is not, and must not leave the panel looking as if nothing happened.
|
||||
notifyActionError(err, t("clip.failed"));
|
||||
notifyActionError(err, msg("clip.failed"));
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { type Video, type VideoStatus } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
@@ -7,6 +6,7 @@ import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { useStableCallback } from "../../lib/useLatest";
|
||||
import { writeVideoState } from "../video/videoWrites";
|
||||
import { useOpenLinkedPlayer } from "./useOpenLinkedPlayer";
|
||||
import { msg } from "../../lib/notifications";
|
||||
|
||||
const PlayerModal = lazy(() => import("./PlayerModal"));
|
||||
|
||||
@@ -14,7 +14,6 @@ const PlayerModal = lazy(() => import("./PlayerModal"));
|
||||
* channel manager, where the watched-videos dialog played it; any page a link names): the video
|
||||
* alone. Decided at boot, once — `hosted` is whether the page on screen then opens it itself. */
|
||||
export function LinkedPlayer({ hosted }: { hosted: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const [own] = useState(!hosted);
|
||||
const [video, setVideo] = useState<Video | null>(null);
|
||||
@@ -27,7 +26,7 @@ export function LinkedPlayer({ hosted }: { hosted: boolean }) {
|
||||
const write = writeVideoState(qc, id, status);
|
||||
write
|
||||
.then(() => qc.invalidateQueries({ queryKey: qk.feed() }))
|
||||
.catch((e: unknown) => notifyActionError(e, t("errors.writeFailed.videoState")));
|
||||
.catch((e: unknown) => notifyActionError(e, msg("errors.writeFailed.videoState")));
|
||||
return write;
|
||||
});
|
||||
const close = useStableCallback(() => setVideo(null));
|
||||
|
||||
@@ -40,7 +40,7 @@ import { closeKeyboardHelp, toggleKeyboardHelp } from "../../lib/keymap";
|
||||
import { ConfirmProvider } from "../../components/ui/ConfirmProvider";
|
||||
import { useConfirm } from "../../components/ui/confirmContext";
|
||||
import { api, type Me, type Video, type VideoStatus } from "../../lib/api";
|
||||
import { clearAll, notify } from "../../lib/notifications";
|
||||
import { clearAll, notify, raw } from "../../lib/notifications";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { writeVideoState } from "../video/videoWrites";
|
||||
|
||||
@@ -314,7 +314,7 @@ describe("PlayerModal — toasts raised while it is open", () => {
|
||||
onState={() => Promise.resolve()}
|
||||
/>,
|
||||
);
|
||||
act(() => void notify({ level: "error", message: "Couldn't remove the video." }));
|
||||
act(() => void notify({ level: "error", message: raw("Couldn't remove the video.") }));
|
||||
expect(screen.getByTestId("player-modal").textContent).toContain("Couldn't remove the video.");
|
||||
});
|
||||
|
||||
@@ -336,7 +336,7 @@ describe("PlayerModal — toasts raised while it is open", () => {
|
||||
Object.defineProperty(document, "fullscreenElement", { configurable: true, get: () => stage });
|
||||
try {
|
||||
act(() => void document.dispatchEvent(new Event("fullscreenchange")));
|
||||
act(() => void notify({ level: "error", message: "Connection lost" }));
|
||||
act(() => void notify({ level: "error", message: raw("Connection lost") }));
|
||||
await waitFor(() => expect(stage.textContent).toContain("Connection lost"));
|
||||
} finally {
|
||||
delete (document as { fullscreenElement?: unknown }).fullscreenElement;
|
||||
|
||||
@@ -26,6 +26,7 @@ import VideoCard from "../video/VideoCard";
|
||||
import { VideoMenu } from "../video/VideoMenu";
|
||||
import { announceVideoState } from "../video/stateNotice";
|
||||
import { writeVideoState, writeWatchLater } from "../video/videoWrites";
|
||||
import { msg } from "../../lib/notifications";
|
||||
|
||||
/** Which list YouTube fills in — on the video's topic or with what trends in Hungary — is a mood,
|
||||
* decided at the list, and the account keeps the last one (the owner's pick). */
|
||||
@@ -100,7 +101,7 @@ export function RelatedVideos({
|
||||
announceVideoState(id, status, v, () => onState(id, "new"));
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
notifyActionError(e, t("errors.writeFailed.videoState"));
|
||||
notifyActionError(e, msg("errors.writeFailed.videoState"));
|
||||
settle(seq);
|
||||
});
|
||||
};
|
||||
@@ -118,7 +119,7 @@ export function RelatedVideos({
|
||||
.catch((e: unknown) => {
|
||||
notifyActionError(
|
||||
e,
|
||||
t(saved ? "errors.writeFailed.watchLaterAdd" : "errors.writeFailed.watchLaterRemove"),
|
||||
msg(saved ? "errors.writeFailed.watchLaterAdd" : "errors.writeFailed.watchLaterRemove"),
|
||||
);
|
||||
settle(seq);
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useOpenLinkedPlayer } from "./useOpenLinkedPlayer";
|
||||
import { LinkedPlayer } from "./LinkedPlayer";
|
||||
import { api, type Video } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { msg } from "../../lib/notifications";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
|
||||
const vid = (id: string) => ({ id, title: id }) as unknown as Video;
|
||||
@@ -76,7 +77,7 @@ describe("useOpenLinkedPlayer", () => {
|
||||
const open = vi.fn();
|
||||
renderHook(() => useOpenLinkedPlayer(true, () => undefined, open), { wrapper });
|
||||
await waitFor(() =>
|
||||
expect(notifyActionError).toHaveBeenCalledWith(gone, "player.linkedFailed"),
|
||||
expect(notifyActionError).toHaveBeenCalledWith(gone, msg("player.linkedFailed")),
|
||||
);
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
// Said once: the address no longer names it, so a reload or a copied link does not say it again.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useEffectEvent, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, type Video } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { linkedPlayer, PLAYER_KEYS } from "../../lib/playerUrlState";
|
||||
import { clearParams } from "../../lib/urlState";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { msg } from "../../lib/notifications";
|
||||
|
||||
/** Opens the player an address names (a reload, a bookmark — lib/playerUrlState) on the page that
|
||||
* hosts it, once: read as the host mounts, opened once its list is in (`ready`). From the list when the
|
||||
@@ -22,7 +22,6 @@ export function useOpenLinkedPlayer(
|
||||
open: (video: Video, inList: boolean) => void,
|
||||
scope: string | number | null = null,
|
||||
): void {
|
||||
const { t } = useTranslation();
|
||||
const [linked] = useState(() => linkedPlayer()?.watch ?? null);
|
||||
const [linkedScope] = useState(scope);
|
||||
// Opened, said to be gone or given up, once. A ref for the effect (a StrictMode re-run of it must
|
||||
@@ -40,7 +39,7 @@ export function useOpenLinkedPlayer(
|
||||
});
|
||||
const openIt = useEffectEvent(open);
|
||||
const failed = useEffectEvent((err: unknown) => {
|
||||
notifyActionError(err, t("player.linkedFailed"));
|
||||
notifyActionError(err, msg("player.linkedFailed"));
|
||||
clearParams(...PLAYER_KEYS);
|
||||
});
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user