fix(notifications): a notice keeps what to say and says it in the reader's language when shown

This commit is contained in:
2026-10-29 22:37:14 +01:00
parent 6986eb1937
commit 4ef772d37b
125 changed files with 973 additions and 674 deletions
+1 -1
View File
@@ -393,7 +393,7 @@ function PlexBrowseView({ sub }: { sub: ReturnType<typeof useHistorySubview<Sub>
);
}
const toggleWatched = (card: PlexCard) =>
toggleWatchState(card, t, {
toggleWatchState(card, {
optimistic: (next) => optimisticStatus(card.id, next),
resync: () => qc.invalidateQueries({ queryKey: qk.plexLibrary() }),
});
@@ -13,6 +13,7 @@ import { useConfirm } from "../../components/ui/confirmContext";
import { Pressable } from "../../components/ui/Pressable";
import { Input } from "../../components/ui/Input";
import { SURFACE } from "../../components/ui/surfaces";
import { msg } from "../../lib/notifications";
// Admin-only "manage this movie's collections" dialog. Lists the library's EDITABLE
// collections with an in/out toggle (add/remove the movie, written back to Plex), plus a field to
@@ -85,7 +86,7 @@ export default function PlexCollectionEditor({
// 403/404 are left to the caller by `req` (see reportFailure), so without this the tick
// simply never appeared and the user clicked again — a collection deleted on the Plex server
// or flipped read-only failed in silence.
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
}
setBusy(null);
}
@@ -97,7 +98,7 @@ export default function PlexCollectionEditor({
await api.plexSetCollectionEditable(id, true);
refresh();
} catch (e) {
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
}
setBusy(null);
}
@@ -114,7 +115,7 @@ export default function PlexCollectionEditor({
} catch (e) {
// The backend rejects a duplicate name with 409 (Plex conflates same names), structured — so
// it reads in the reader's language (see localizeDetail).
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
}
setBusy(null);
}
@@ -134,7 +135,7 @@ export default function PlexCollectionEditor({
} catch (e) {
// The worst of the three to lose: the user has just confirmed a destructive dialog, and the
// row stayed where it was with nothing said.
notifyActionError(e, t("plex.collection.deleteFailed"));
notifyActionError(e, msg("plex.collection.deleteFailed"));
}
setBusy(null);
}
@@ -44,6 +44,7 @@ vi.mock("../../lib/notifications", async (importOriginal) => ({
}));
import PlexCollectionView from "./PlexCollectionView";
import { msg } from "../../lib/notifications";
import {
api,
HttpError,
@@ -135,7 +136,7 @@ describe("PlexCollectionView — Play-all", () => {
fireEvent.click(await screen.findByText("plex.collection.playAll"));
await waitFor(() =>
expect(h.notify).toHaveBeenCalledWith(
expect.objectContaining({ level: "info", message: "plex.collection.playEmpty" }),
expect.objectContaining({ level: "info", message: msg("plex.collection.playEmpty") }),
),
);
expect(onPlay).not.toHaveBeenCalled();
@@ -8,7 +8,7 @@ import { plexLang } from "../../i18n";
import { api, isIntrinsicReadOnly, type PlexCard, type PlexCollectionDetail } from "../../lib/api";
import { formatRuntime } from "../../lib/format";
import { notifyActionError } from "../../lib/actionErrors";
import { notify } from "../../lib/notifications";
import { msg, notify } from "../../lib/notifications";
import { showEpisodeKeys } from "../../lib/plexQueue";
import { healPlexSort, plexSortOptions } from "../../lib/plexSort";
import { usePref } from "../../lib/usePref";
@@ -154,9 +154,9 @@ export default function PlexCollectionView({
// canPlay gates on "has a show member", but a show can expand to zero episodes (unmatched /
// empty). An enabled Play-all that silently no-ops is a dishonest state, so say so plainly.
if (queue.length > 0) onPlay(queue[0]!, queue);
else notify({ level: "info", message: t("plex.collection.playEmpty") });
else notify({ level: "info", message: msg("plex.collection.playEmpty") });
} catch (e) {
notifyActionError(e, t("plex.collection.playFailed"));
notifyActionError(e, msg("plex.collection.playFailed"));
}
setQueueBusy(false);
}
@@ -168,7 +168,7 @@ export default function PlexCollectionView({
const readOnly = !d?.can_edit && !canEnableEdit; // external / smart auto-list
const toggleWatched = (card: PlexCard) =>
toggleWatchState(card, t, {
toggleWatchState(card, {
optimistic: (next) => {
qc.setQueryData<PlexCollectionDetail>(detailKey, (old) =>
old
@@ -211,7 +211,7 @@ export default function PlexCollectionView({
setRenaming(false);
} catch (e) {
// The backend enforces per-library name uniqueness — a taken name comes back localized.
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
}
setBusy(false);
}
@@ -226,7 +226,7 @@ export default function PlexCollectionView({
invalidate();
onBack();
} catch (e) {
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
setBusy(false);
}
}
@@ -240,7 +240,7 @@ export default function PlexCollectionView({
await api.plexCollectionRemoveItem(collectionId, card.id);
invalidate();
} catch (e) {
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
}
setBusy(false);
}
@@ -251,7 +251,7 @@ export default function PlexCollectionView({
await api.plexSetCollectionEditable(collectionId, true);
invalidate();
} catch (e) {
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
}
setBusy(false);
}
@@ -495,7 +495,7 @@ function AddItemPanel({
setAdded((s) => new Set(s).add(card.id));
onAdded();
} catch (e) {
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
}
}
+1 -1
View File
@@ -172,7 +172,7 @@ export default function PlexDiscover({
: old,
);
const toggleWatched = (card: PlexCard) =>
toggleWatchState(card, t, {
toggleWatchState(card, {
optimistic: (next) => {
const flip = (cards: PlexCard[]) =>
cards.map((c) => (c.id === card.id ? { ...c, status: next } : c));
+3 -2
View File
@@ -36,6 +36,7 @@ import { usePlexCardActions } from "../../features/plexCards/usePlexCardActions"
import { toggleWatchState } from "../../lib/plexWatch";
import { vocab } from "../../lib/vocab";
import RemoteImage from "../../components/ui/RemoteImage";
import { msg } from "../../lib/notifications";
// The rich "media info" surface for a Plex item — poster/art, IMDb score + link, content rating,
// genres, director, cast (with photos), summary. Reused in TWO places: a full-page view opened from
@@ -254,7 +255,7 @@ export default function PlexInfo({
try {
await api.plexSetState(detail.id, status);
} catch (e) {
notifyActionError(e, t("plex.watchedFailed"));
notifyActionError(e, msg("plex.watchedFailed"));
}
qc.invalidateQueries({ queryKey: qk.plexItem(detail.id) });
qc.invalidateQueries({ queryKey: qk.plexLibrary() });
@@ -267,7 +268,7 @@ export default function PlexInfo({
// is; the resync corrects it to the truth either way.
const colMenu = useContextMenu<PlexCard>();
const toggleColWatched = (card: PlexCard) =>
toggleWatchState(card, t, {
toggleWatchState(card, {
optimistic: (next) =>
qc.setQueriesData<PlexItemDetail>({ queryKey: qk.plexItem(detail.id) }, (old) =>
old
@@ -20,7 +20,7 @@ vi.mock("react-i18next", async (importOriginal) => {
import PlexPlayer from "./PlexPlayer";
import { api, HttpError, type PlexItemDetail } from "../../lib/api";
import { qk } from "../../lib/queryKeys";
import { notify } from "../../lib/notifications";
import { notify, raw } from "../../lib/notifications";
import { claimLocalData } from "../../lib/storage";
type Media = HTMLMediaElement & { _t?: number };
@@ -285,7 +285,7 @@ it("shows a toast raised while it is open inside itself, not under its scrim", a
mount({ details: { A: item("A") }, sessions: { A: () => new Promise(() => {}) } });
await screen.findByText("Title A");
act(() => {
notify({ level: "error", message: "Couldn't mark it watched." });
notify({ level: "error", message: raw("Couldn't mark it watched.") });
});
const toast = await screen.findByText("Couldn't mark it watched.");
let host: Element | null = video();
+3 -2
View File
@@ -103,6 +103,7 @@ import { closeThenAct, useAnchoredPopover } from "../../components/ui/anchoredPo
import { Slider } from "../../components/ui/NativeControls";
import { formatUnit } from "../../lib/format";
import { trackLabel } from "../../lib/vocab";
import { msg } from "../../lib/notifications";
// The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page.
const PlexInfo = lazy(() => import("./PlexInfo"));
@@ -716,8 +717,8 @@ export default function PlexPlayer({
});
const reportWatchedError = useCallback(
(e: unknown) => notifyActionError(e, t("plex.watchedFailed")),
[t],
(e: unknown) => notifyActionError(e, msg("plex.watchedFailed")),
[],
);
const go = useCallback(
@@ -29,6 +29,7 @@ vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => h.confi
import PlexPlaylistView from "./PlexPlaylistView";
import { api, type PlexCard } from "../../lib/api";
import { qk } from "../../lib/queryKeys";
import { msg, type NotifText } from "../../lib/notifications";
beforeAll(() => {
class Stub {
@@ -98,8 +99,8 @@ const moreOf = (text: string) =>
rowOf(text).querySelector<HTMLElement>('button[aria-label="plex.playlist.rowMenu"]')!;
const toast = () =>
h.notify.mock.calls
.map((c) => c[0] as { message: string; action?: { label: string; onClick: () => void } })
.filter((n) => n.message?.startsWith("plex.playlist.removedToast"))
.map((c) => c[0] as { message: NotifText; action?: { label: string; onClick: () => void } })
.filter((n) => "key" in n.message && n.message.key === "plex.playlist.removedToast")
.pop();
// The list's keys are live once its effect has registered it (`data-nav-list`), a passive effect that
// can run AFTER the rows are on screen: a key pressed on a slow machine in between did nothing (CI flake).
@@ -181,7 +182,9 @@ describe("PlexPlaylistView · a row's menu", () => {
await pick(() => fireEvent.contextMenu(rowOf("Movie 2")), "plex.playlist.removeFromPlaylist");
await waitFor(() => expect(api.plexPlaylistRemoveItem).toHaveBeenCalledWith(5, "2"));
expect(h.confirm).not.toHaveBeenCalled();
await waitFor(() => expect(toast()?.message).toBe("plex.playlist.removedToast:Movie 2"));
await waitFor(() =>
expect(toast()?.message).toEqual(msg("plex.playlist.removedToast", { title: "Movie 2" })),
);
act(() => toast()!.action!.onClick());
await waitFor(() => expect(add).toHaveBeenCalledWith(5, ["2"], [1]));
});
@@ -208,7 +211,9 @@ describe("PlexPlaylistView · a row's menu", () => {
const bulk = vi.spyOn(api, "plexPlaylistRemoveBulk").mockResolvedValue({} as never);
await pick(() => fireEvent.click(moreOf("The Show")), "plex.playlist.removeShow");
await waitFor(() => expect(bulk).toHaveBeenCalledWith(5, ["e1", "e2"]));
await waitFor(() => expect(toast()?.message).toBe("plex.playlist.removedToast:The Show"));
await waitFor(() =>
expect(toast()?.message).toEqual(msg("plex.playlist.removedToast", { title: "The Show" })),
);
});
it("removes one season from its label's menu", async () => {
@@ -349,7 +354,7 @@ describe("PlexPlaylistView · a failed watched toggle", () => {
await pick(() => fireEvent.contextMenu(rowOf("Movie 2")), "plex.markWatched");
await waitFor(() =>
expect(h.notify).toHaveBeenCalledWith(
expect.objectContaining({ level: "error", title: "plex.watchedFailed" }),
expect.objectContaining({ level: "error", title: msg("plex.watchedFailed") }),
),
);
await act(async () => {});
@@ -58,6 +58,7 @@ import { MEDIA_BADGE } from "../../components/ui/mediaCardStyles";
import { seasonTitle } from "../../lib/plexDetail";
import { cardImage } from "../../lib/plexImage";
import RemoteImage from "../../components/ui/RemoteImage";
import { msg } from "../../lib/notifications";
// A playlist's items, grouped for readability: movies stay standalone, while a run of episodes from the
// same show collapses into a season/show group so a whole series doesn't sprawl into an endless flat
@@ -459,7 +460,7 @@ export default function PlexPlaylistView({
try {
await api.plexRenamePlaylist(playlistId, title);
} catch (e) {
notifyActionError(e, t("plex.playlist.renameFailed")); // the field stays open to try again
notifyActionError(e, msg("plex.playlist.renameFailed")); // the field stays open to try again
return;
}
setRenaming(false);
@@ -479,7 +480,7 @@ export default function PlexPlaylistView({
// Gone already (deleted on another device): what was asked for is true — leave the dead page.
// Anything else is a real failure, and the page stays.
if (!(e instanceof HttpError && e.status === 404)) {
notifyActionError(e, t("plex.playlist.deleteFailed"));
notifyActionError(e, msg("plex.playlist.deleteFailed"));
return;
}
}
@@ -569,7 +570,7 @@ export default function PlexPlaylistView({
onRestart: (c) => onPlay(c.id, order, true),
onInfo: onInfoCard,
onToggleWatched: (c) =>
void toggleWatchState(c, t, {
void toggleWatchState(c, {
optimistic: (next) => setStatus(c.id, next),
// The flip is on the view's LOCAL copy: a failed write's resync refetches an identical
// list, react-query keeps the same `data`, and the copy is never re-seeded from it.
+2 -1
View File
@@ -22,6 +22,7 @@ import { usePlex, usePlexActions } from "./plexContext";
import { DismissibleChip } from "../../components/ui/Chip";
import { Input } from "../../components/ui/Input";
import { useRail } from "../../lib/useRail";
import { msg } from "../../lib/notifications";
// The Plex module's left filter column — now the shared floating SidePanel with its filter groups
// as reorderable "islands" (same system as the feed rail). Movie libraries get the full metadata
@@ -350,7 +351,7 @@ export function PlaylistPicker() {
onOpenPlaylist(pl.id);
} catch (e) {
// Called without an await from the picker, so a refusal was an unhandled rejection.
notifyActionError(e, t("plex.playlistAdd.createFailed"));
notifyActionError(e, msg("plex.playlistAdd.createFailed"));
}
}
@@ -16,6 +16,7 @@ import { Button } from "../../components/ui/Button";
import { IconButton } from "../../components/ui/IconButton";
import { Input } from "../../components/ui/Input";
import { SURFACE } from "../../components/ui/surfaces";
import { msg } from "../../lib/notifications";
// A collection tile's sub-line: item count · total runtime (each dropped when unknown, e.g. show-only
// collections have no movie runtime).
@@ -103,7 +104,7 @@ function NewCollectionPanel({
} catch (e) {
// The backend is authoritative on per-library name uniqueness — a taken name comes back
// structured, and so localized (see localizeDetail).
notifyActionError(e, t("plex.collection.editFailed"));
notifyActionError(e, msg("plex.collection.editFailed"));
setBusy(false);
}
}
+6 -5
View File
@@ -69,6 +69,7 @@ import { PageHeading } from "../../components/ui/Headings";
import { vocab } from "../../lib/vocab";
import { formatDate, formatDecimal } from "../../lib/format";
import RemoteImage from "../../components/ui/RemoteImage";
import { msg } from "../../lib/notifications";
// The Plex sub-router's view branches, split out of PlexBrowse. Each is invoked by PlexBrowse with
// shell callbacks as props (onBack/onPlay/onFilter/…) — no view reaches back into the shell's state,
@@ -209,7 +210,7 @@ export function PlexPersonView({
// (both the movies and shows arrays — an id matches only one), then persist + resync via the shared
// helper; also invalidate the library so its grid agrees.
const toggle = (card: PlexCard) =>
toggleWatchState(card, t, {
toggleWatchState(card, {
optimistic: (next) =>
qc.setQueryData<PlexPersonDetail>(personKey, (old) =>
old
@@ -500,7 +501,7 @@ export function PlexShowView({
try {
await api.plexShowState(showId, w);
} catch (e) {
notifyActionError(e, t("plex.watchedFailed"));
notifyActionError(e, msg("plex.watchedFailed"));
}
setBusy(false);
qc.invalidateQueries({ queryKey: qk.plexShow(showId) });
@@ -510,7 +511,7 @@ export function PlexShowView({
try {
await api.plexSeasonState(seasonRk, w);
} catch (e) {
notifyActionError(e, t("plex.watchedFailed"));
notifyActionError(e, msg("plex.watchedFailed"));
}
qc.invalidateQueries({ queryKey: qk.plexShow(showId) });
qc.invalidateQueries({ queryKey: qk.plexLibrary() });
@@ -836,7 +837,7 @@ export function PlexSeasonView({
try {
await api.plexSeasonState(seasonId, w);
} catch (e) {
notifyActionError(e, t("plex.watchedFailed"));
notifyActionError(e, msg("plex.watchedFailed"));
}
setBusy(false);
qc.invalidateQueries({ queryKey: qk.plexShow(showId) });
@@ -847,7 +848,7 @@ export function PlexSeasonView({
try {
await api.plexSetState(ep.id, next);
} catch (e) {
notifyActionError(e, t("plex.watchedFailed"));
notifyActionError(e, msg("plex.watchedFailed"));
}
qc.invalidateQueries({ queryKey: qk.plexShow(showId) });
qc.invalidateQueries({ queryKey: qk.plexLibrary() });
@@ -17,6 +17,7 @@ vi.mock("react-i18next", async (importOriginal) => ({
import { api, type PlexCard } from "../../lib/api";
import { qk } from "../../lib/queryKeys";
import { usePlayShow } from "./usePlayShow";
import { msg } from "../../lib/notifications";
afterEach(() => {
vi.restoreAllMocks();
@@ -106,7 +107,7 @@ describe("usePlayShow", () => {
vi.spyOn(api, "plexShowNext").mockRejectedValue(new Error("down"));
const { play, openShow, playShow } = mount();
await act(() => playShow(show, false));
expect(notified).toHaveBeenCalledWith(expect.any(Error), "plex.playShowFailed");
expect(notified).toHaveBeenCalledWith(expect.any(Error), msg("plex.playShowFailed"));
expect(play).not.toHaveBeenCalled();
expect(openShow).not.toHaveBeenCalled();
});
+3 -2
View File
@@ -6,6 +6,7 @@ import { qk } from "../../lib/queryKeys";
import { notifyActionError } from "../../lib/actionErrors";
import { plexLang } from "../../i18n";
import { useLatest } from "../../lib/useLatest";
import { msg } from "../../lib/notifications";
/** A show's card plays like any card: the episode in progress, else the first unwatched, else the
* first (`/show/{id}/next`). Asked fresh on the click, since the card's preview may hold an answer
@@ -25,7 +26,7 @@ export function usePlayShow(
const asked = useRef(0);
// Unmounted, nothing of it may navigate any more.
useEffect(() => () => void ++asked.current, []);
const { t, i18n } = useTranslation();
const { i18n } = useTranslation();
const lang = plexLang(i18n.language);
// `undefined` when the question failed (said in the toast).
const next = (id: string) =>
@@ -37,7 +38,7 @@ export function usePlayShow(
})
.then(
(r) => r.next,
(e: unknown) => void notifyActionError(e, t("plex.playShowFailed")),
(e: unknown) => void notifyActionError(e, msg("plex.playShowFailed")),
);
return async (show: PlexCard, fromStart: boolean) => {
const ask = ++asked.current;
@@ -15,7 +15,8 @@ vi.mock("react-i18next", async (importOriginal) => ({
}));
const { notify } = vi.hoisted(() => ({
notify: vi.fn(
(_n: { level: string; message: string; action?: { label: string; onClick: () => void } }) => 0,
(_n: { level: string; message: NotifText; action?: { label: string; onClick: () => void } }) =>
0,
),
}));
vi.mock("../../lib/notifications", async (importOriginal) => ({
@@ -26,6 +27,7 @@ vi.mock("../../lib/notifications", async (importOriginal) => ({
import { usePlexPlaylistEdits } from "./usePlexPlaylistEdits";
import { api, type PlexCard } from "../../lib/api";
import { qk } from "../../lib/queryKeys";
import { msg, type NotifText } from "../../lib/notifications";
afterEach(() => {
cleanup();
@@ -39,7 +41,7 @@ const ids = (items: PlexCard[]) => items.map((i) => i.id);
const errors = () =>
notify.mock.calls
.filter(([n]) => n.level === "error")
.map(([n]) => (n as { title?: string }).title);
.map(([n]) => (n as { title?: NotifText }).title);
function setup(items: PlexCard[]) {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
@@ -61,7 +63,7 @@ describe("usePlexPlaylistEdits · the reorder", () => {
act(() => hook.result.current.commit([movie("3"), movie("1"), movie("2")]));
expect(ids(hook.result.current.items)).toEqual(["3", "1", "2"]); // optimistic
await waitFor(() => expect(errors()).toEqual(["plex.playlist.reorderFailed"]));
await waitFor(() => expect(errors()).toEqual([msg("plex.playlist.reorderFailed")]));
expect(ids(hook.result.current.items)).toEqual(["1", "2", "3"]);
});
@@ -112,7 +114,7 @@ describe("usePlexPlaylistEdits · the reorder", () => {
await waitFor(() => expect(ids(hook.result.current.items)).toEqual(["1", "3"]));
await act(async () => fail(new Error("offline")));
await waitFor(() => expect(errors()).toEqual(["plex.playlist.reorderFailed"]));
await waitFor(() => expect(errors()).toEqual([msg("plex.playlist.reorderFailed")]));
expect(ids(hook.result.current.items)).toEqual(["1", "3"]);
});
});
@@ -124,7 +126,7 @@ describe("usePlexPlaylistEdits · the removal", () => {
vi.spyOn(api, "plexPlaylistRemoveItem").mockRejectedValue(new Error("offline"));
await act(() => hook.result.current.onRemove(["2"], "Movie 2"));
expect(errors()).toEqual(["plex.playlist.removeFailed"]);
expect(errors()).toEqual([msg("plex.playlist.removeFailed")]);
expect(ids(hook.result.current.items)).toEqual(["1", "2"]);
expect(hook.result.current.busy).toBe(false);
// A failed removal offers no Undo: there is nothing to put back.
@@ -144,7 +146,7 @@ describe("usePlexPlaylistEdits · the removal", () => {
// A season whose episodes do not sit together: rows 2 and 4.
await act(() => hook.result.current.onRemove(["4", "2"], "Show · Season 1"));
const toast = notify.mock.calls.map(([n]) => n).find((n) => n.action)!;
expect(toast.message).toBe("plex.playlist.removedToast");
expect(toast.message).toEqual(msg("plex.playlist.removedToast", { title: "Show · Season 1" }));
expect(toast.action!.label).toBe("common.undo");
act(() => toast.action!.onClick());
await waitFor(() => expect(add).toHaveBeenCalledTimes(1));
@@ -180,6 +182,6 @@ describe("usePlexPlaylistEdits · the removal", () => {
await act(() => hook.result.current.onRemove(["1"], "Movie 1"));
const toast = notify.mock.calls.map(([n]) => n).find((n) => n.action)!;
act(() => toast.action!.onClick());
await waitFor(() => expect(errors()).toEqual(["plex.playlist.undoRemoveFailed"]));
await waitFor(() => expect(errors()).toEqual([msg("plex.playlist.undoRemoveFailed")]));
});
});
@@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { plexLang } from "../../i18n";
import { api, type PlexCard } from "../../lib/api";
import { notifyActionError } from "../../lib/actionErrors";
import { notify } from "../../lib/notifications";
import { msg, notify } from "../../lib/notifications";
import { qk } from "../../lib/queryKeys";
import { reinsertAt } from "../../lib/reinsert";
@@ -79,7 +79,7 @@ export function usePlexPlaylistEdits(playlistId: number) {
qc.invalidateQueries({ queryKey: qk.plexPlaylists() });
})
.catch((e: unknown) => {
notifyActionError(e, t("plex.playlist.reorderFailed"));
notifyActionError(e, msg("plex.playlist.reorderFailed"));
putBack();
});
}
@@ -102,7 +102,7 @@ export function usePlexPlaylistEdits(playlistId: number) {
else await api.plexPlaylistRemoveBulk(playlistId, keys);
invalidate();
} catch (e) {
notifyActionError(e, t("plex.playlist.removeFailed"));
notifyActionError(e, msg("plex.playlist.removeFailed"));
putBack();
removed = false;
}
@@ -110,7 +110,7 @@ export function usePlexPlaylistEdits(playlistId: number) {
if (!removed) return;
notify({
level: "success",
message: t("plex.playlist.removedToast", { title }),
message: msg("plex.playlist.removedToast", { title }),
action: {
label: t("common.undo"),
onClick: () =>
@@ -129,7 +129,7 @@ export function usePlexPlaylistEdits(playlistId: number) {
if (back.keys.length)
return api.plexPlaylistAddBulk(playlistId, back.keys, back.positions);
})
.catch((e: unknown) => notifyActionError(e, t("plex.playlist.undoRemoveFailed")))
.catch((e: unknown) => notifyActionError(e, msg("plex.playlist.undoRemoveFailed")))
.finally(invalidate),
},
});