feat(video): Reset view on every YouTube video menu

The row that sets a video back to fresh (its state row and resume point both
go) was only on the feed's cards. It is now on Home's shelves, a Playlists
row's menu and the player's related list too, and is named "Reset view" /
"Megtekintés visszaállítása".
This commit is contained in:
2026-11-01 01:20:20 +01:00
parent 15a3603513
commit e06c0688d9
11 changed files with 115 additions and 12 deletions
@@ -163,6 +163,15 @@ describe("RelatedVideos", () => {
await waitFor(() => expect(titles()).toEqual(["Video A"]));
});
it("resets a started video's view from its menu", async () => {
vi.spyOn(api, "related").mockResolvedValue(page([row("A", { position_seconds: 95 })]));
const reset = vi.spyOn(api, "clearState").mockResolvedValue(undefined as never);
setup();
fireEvent.contextMenu((await title("Video A")).closest("[data-video-id]")!);
fireEvent.click(menuItem("card.resetState"));
await waitFor(() => expect(reset).toHaveBeenCalledWith("A"));
});
it("reads further with the cursor the last page gave", async () => {
const asked = vi
.spyOn(api, "related")
+17 -2
View File
@@ -25,7 +25,7 @@ import Tooltip from "../../components/ui/Tooltip";
import VideoCard from "../video/VideoCard";
import { VideoMenu } from "../video/VideoMenu";
import { announceVideoState } from "../video/stateNotice";
import { writeVideoState, writeWatchLater } from "../video/videoWrites";
import { resetVideoState, 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,
@@ -106,6 +106,21 @@ export function RelatedVideos({
});
};
const onResetState = (id: string) => {
const seq = begin(id, { status: "new", position_seconds: 0 });
const write = resetVideoState(qc, id);
onWrite?.(write);
write
.then(() => {
settle(seq);
void qc.invalidateQueries({ queryKey: qk.feed(), refetchType: "none" });
})
.catch((e: unknown) => {
notifyActionError(e, msg("errors.writeFailed.videoReset"));
settle(seq);
});
};
const onToggleSave = (id: string, saved: boolean) => {
const seq = begin(id, { saved });
const write = writeWatchLater(qc, id, saved);
@@ -231,7 +246,7 @@ export function RelatedVideos({
)}
</>
)}
<VideoMenu menu={menu} handlers={{ onState, onToggleSave, onOpen: open }} />
<VideoMenu menu={menu} handlers={{ onState, onToggleSave, onResetState, onOpen: open }} />
</section>
);
}
@@ -39,7 +39,7 @@ function downloadRow(state: string | undefined) {
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
);
const { result } = renderHook(
() => useVideoActions({ onState: vi.fn(), onToggleSave: vi.fn() }, true),
() => useVideoActions({ onState: vi.fn(), onToggleSave: vi.fn(), onResetState: vi.fn() }, true),
{ wrapper },
);
return () => result.current.items(video, null).find((i) => i.id === "download")!;
@@ -74,6 +74,7 @@ describe("the video menu's channel-filter row", () => {
{
onState: vi.fn(),
onToggleSave: vi.fn(),
onResetState: vi.fn(),
channelFilter: ids ? { ids, set } : undefined,
},
true,
@@ -38,8 +38,8 @@ import { raw } from "../../lib/notifications";
export interface VideoActionHandlers {
onState: (id: string, status: VideoStatus) => void;
onToggleSave: (id: string, saved: boolean) => void;
/** Omitted where a surface cannot reset a video's state (Home's shelves). */
onResetState?: (id: string) => void;
/** Back to fresh: its state row and resume point both go. */
onResetState: (id: string) => void;
/** Omitted where the host menu leads with its own Play row (Home's shelf card resolves `next`). */
onOpen?: (v: Video, startAt?: number | null) => void;
/** The feed's channel filter, where the menu can narrow it: the library feed. Omitted on a channel
@@ -189,7 +189,7 @@ export function useVideoActions(handlers: VideoActionHandlers, open: boolean) {
separatorBefore: true,
onSelect: () => onState(v.id, hidden ? "new" : "hidden"),
},
...(onResetState && isResettable(v)
...(isResettable(v)
? [
{
id: "reset",