refactor(ui): reuse the store, media-query and deleted-tag rules the review found rebuilt, and name a loading pick

E32 review round 1 (F3–F6).

- Channels' open-row store and the chat dock use lib/store's createStore instead of a hand-rolled
  listener set (the chat dock gets a test of its own).
- The Plex backdrop reads prefers-reduced-motion through useMediaQuery; the query string is one
  constant (REDUCED_MOTION) for the hook and the carousel's one-off read.
- A link-restored collection filter showed its raw rating_key in the picker while the titles
  loaded: usePlexCollectionTitles now says it is pending, the picker draws a placeholder
  (labelsPending, as the channel picker does) and so does the chip row.
- The "deleted tag" rule was written twice with two i18n keys and had drifted: the channel manager
  showed NO chip for a pick while the tag list loaded, or ever after a failed load, while the pick
  still narrowed the table. One rule now (lib/tagPicks), one key (common.deletedTag); the manager's
  row shows every pick, as a placeholder while loading and as its id after a failure.
This commit is contained in:
2026-10-04 02:29:03 +02:00
parent e09d613b04
commit e5b155ada9
24 changed files with 245 additions and 129 deletions
@@ -44,7 +44,7 @@ describe("usePlexCollectionTitles", () => {
wrapper: wrap(),
});
await waitFor(() =>
expect(result.current["17045"]).toBe("Pirates of the Caribbean Collection"),
expect(result.current.titles["17045"]).toBe("Pirates of the Caribbean Collection"),
);
// Only the missing ids are asked for — nothing else.
expect(plexCollections).toHaveBeenCalledWith(undefined, undefined, ["17045"]);
@@ -55,7 +55,7 @@ describe("usePlexCollectionTitles", () => {
const { result } = renderHook(() => usePlexCollectionTitles(["17045"], known), {
wrapper: wrap(),
});
expect(result.current["17045"]).toBe("Pirates of the Caribbean Collection");
expect(result.current.titles["17045"]).toBe("Pirates of the Caribbean Collection");
// Give a would-be fetch a chance to happen before asserting it did not.
await new Promise((r) => setTimeout(r, 20));
expect(plexCollections).not.toHaveBeenCalled();
@@ -66,10 +66,10 @@ describe("usePlexCollectionTitles", () => {
() => usePlexCollectionTitles(["17045", "68231"], { "17045": "Already Known" }),
{ wrapper: wrap() },
);
await waitFor(() => expect(result.current["68231"]).toBe("IMDb Top 250"));
await waitFor(() => expect(result.current.titles["68231"]).toBe("IMDb Top 250"));
// The known id is not in the request — and its stored title survives the merge.
expect(plexCollections).toHaveBeenCalledWith(undefined, undefined, ["68231"]);
expect(result.current["17045"]).toBe("Already Known");
expect(result.current.titles["17045"]).toBe("Already Known");
});
it("treats the same ids in a different order as one query", async () => {
@@ -77,12 +77,12 @@ describe("usePlexCollectionTitles", () => {
const a = renderHook(() => usePlexCollectionTitles(["68231", "17045"], undefined), {
wrapper: w,
});
await waitFor(() => expect(a.result.current["17045"]).toBeDefined());
await waitFor(() => expect(a.result.current.titles["17045"]).toBeDefined());
const calls = plexCollections.mock.calls.length;
const b = renderHook(() => usePlexCollectionTitles(["17045", "68231"], undefined), {
wrapper: w,
});
await waitFor(() => expect(b.result.current["17045"]).toBeDefined());
await waitFor(() => expect(b.result.current.titles["17045"]).toBeDefined());
expect(plexCollections.mock.calls.length).toBe(calls);
});
@@ -91,7 +91,21 @@ describe("usePlexCollectionTitles", () => {
const { result } = renderHook(() => usePlexCollectionTitles(["17045"], undefined), {
wrapper: wrap(),
});
// The caller falls back to its own label; it must not be handed a half-built map.
expect(result.current["17045"]).toBeUndefined();
// The caller falls back to its own label; it must not be handed a half-built map — and it is
// told the names are on their way, so it draws a placeholder rather than the raw id.
expect(result.current.titles["17045"]).toBeUndefined();
expect(result.current.pending).toBe(true);
});
it("is not pending when nothing is missing, nor once the lookup answered", async () => {
const known = renderHook(() => usePlexCollectionTitles(["17045"], { "17045": "Known" }), {
wrapper: wrap(),
});
expect(known.result.current.pending).toBe(false);
const cold = renderHook(() => usePlexCollectionTitles(["17045"], undefined), {
wrapper: wrap(),
});
await waitFor(() => expect(cold.result.current.pending).toBe(false));
expect(cold.result.current.titles["17045"]).toBe("Pirates of the Caribbean Collection");
});
});