fix(player): a related video played is tracked, and its list is honest about its state
A related video played in place is the player's item now: its title and its own watched / playlist / download controls, its progress checkpointed and resumed, marked watched at its end — and its end does not step the queue. Back returns to the opened video, where it was left. The related list: - its marks announce through the shared notice (the Notification Center finds them), join the host's player session (the feed refetches on close), and stay per write, so an earlier answer no longer undoes a later mark still on its way; - every host's state write patches the cached related lists, which are never refetched; - kept for the session (gcTime), a retry asks for the page that failed, a list read so far with nothing unseen says so, a refusal a retry cannot answer offers none, and the followed mark names itself on hover. Turned off on the instance (a limit of 0), the tab, the drawer button and the live-search buttons are left out. Keys: the player's keys and its focus reclaim defer to a layer above it (the drawer); going wide moves the drawer's list into the panel's tab; `j` goes on from the slot of a row removed inside the top layer; the help sheet lists the list keys over a layer that holds a list.
This commit is contained in:
@@ -54,10 +54,12 @@ vi.mock("../../features/player/PlayerModal", async () => {
|
||||
default: function Player({
|
||||
video,
|
||||
onState,
|
||||
onWrite,
|
||||
onClose,
|
||||
}: {
|
||||
video: { id: string };
|
||||
onState: (id: string, status: string) => void;
|
||||
onState: (id: string, status: string, v?: unknown) => void;
|
||||
onWrite?: (write: Promise<unknown>) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
useEffect(
|
||||
@@ -70,6 +72,19 @@ vi.mock("../../features/player/PlayerModal", async () => {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => onState(video.id, "watched")}>mark</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onState("rel1", "watched", {
|
||||
id: "rel1",
|
||||
title: "Related one",
|
||||
channel_id: "UCr",
|
||||
channel_title: "Rel channel",
|
||||
})
|
||||
}
|
||||
>
|
||||
mark related
|
||||
</button>
|
||||
<button onClick={() => onWrite?.(Promise.resolve())}>related list wrote</button>
|
||||
<button onClick={onClose}>close</button>
|
||||
</div>
|
||||
);
|
||||
@@ -81,6 +96,7 @@ import Feed from "./Feed";
|
||||
import { api, DEFAULT_TAG_MODES, type FeedFilters, type Video } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { patchCachedProgress } from "../../lib/progressCache";
|
||||
import { clearAll, getNotifications } from "../../lib/notifications";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
@@ -99,8 +115,24 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
player.watchOnUnmount = false;
|
||||
player.flush = null;
|
||||
clearAll();
|
||||
});
|
||||
|
||||
// A cached related list (the player's, kept for the session) holding these videos.
|
||||
const seedRelated = (qc: QueryClient, ...ids: string[]) =>
|
||||
qc.setQueryData(qk.related("SRC", "topical"), {
|
||||
pages: [
|
||||
{ items: ids.map((id) => ({ id, status: "new" })), has_more: false, next_cursor: null },
|
||||
],
|
||||
pageParams: [null],
|
||||
});
|
||||
const relatedStatus = (qc: QueryClient, id: string) =>
|
||||
(
|
||||
qc.getQueryData(qk.related("SRC", "topical")) as {
|
||||
pages: { items: { id: string; status: string }[] }[];
|
||||
}
|
||||
).pages[0]!.items.find((v) => v.id === id)?.status;
|
||||
|
||||
const FILTERS: FeedFilters = {
|
||||
tags: [],
|
||||
tagModes: DEFAULT_TAG_MODES,
|
||||
@@ -227,6 +259,39 @@ describe("Feed — a video marked watched in the player", () => {
|
||||
await waitFor(() => expect(feed.mock.calls.length).toBeGreaterThan(before));
|
||||
});
|
||||
|
||||
it("refetches when the player's related list wrote", async () => {
|
||||
const { feed } = mount();
|
||||
fireEvent.click(await screen.findByText("open v1"));
|
||||
fireEvent.click(await screen.findByText("related list wrote"));
|
||||
const before = feed.mock.calls.length;
|
||||
fireEvent.click(screen.getByText("close"));
|
||||
await waitFor(() => expect(feed.mock.calls.length).toBeGreaterThan(before));
|
||||
});
|
||||
|
||||
it("names a video that is none of its own in the notice", async () => {
|
||||
mount();
|
||||
fireEvent.click(await screen.findByText("open v1"));
|
||||
fireEvent.click(await screen.findByText("mark related"));
|
||||
await waitFor(() =>
|
||||
expect(getNotifications().map((n) => n.meta)).toContainEqual({
|
||||
kind: "video-watched",
|
||||
videoId: "rel1",
|
||||
title: "Related one",
|
||||
channelId: "UCr",
|
||||
channelName: "Rel channel",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("puts the mark into the player's related lists, which are never refetched", async () => {
|
||||
const { qc } = mount();
|
||||
seedRelated(qc, "v1", "other");
|
||||
fireEvent.click(await screen.findByText("open v1"));
|
||||
fireEvent.click(await screen.findByText("mark"));
|
||||
await waitFor(() => expect(relatedStatus(qc, "v1")).toBe("watched"));
|
||||
expect(relatedStatus(qc, "other")).toBe("new");
|
||||
});
|
||||
|
||||
it("does not refetch on a close with nothing changed", async () => {
|
||||
const { feed } = mount();
|
||||
fireEvent.click(await screen.findByText("open v1"));
|
||||
|
||||
Reference in New Issue
Block a user