commit d1794d7c891f4e04740dcccec7fb5000170ad48f
parent da4a6589d70a8f220e1051834156cc809dc76296
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 16:21:31 -0400
common: the filter snapshot stores notr (Transcripts unticked) and lc (Live chat ticked)
Read, compared and written beside nop, each only off its default, so a
profile saved before the row reads transcripts and posts as it always did.
parseSnapshot is exported for the round-trip test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 99 insertions(+), 3 deletions(-)
diff --git a/common/components/exportFilterStorage.test.ts b/common/components/exportFilterStorage.test.ts
@@ -0,0 +1,83 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import {
+ emptySnapshot,
+ emptyStoredState,
+ loadStoredState,
+ parseSnapshot,
+ saveStoredState,
+ snapshotsEqual,
+} from "./exportFilterStorage";
+
+// The "Search in" row's two new keys (release 16): `notr` (Transcripts
+// unticked) and `lc` (Live chat ticked), stored beside `nop` only when off the
+// default, in the working snapshot and in every profile.
+
+test("notr and lc survive a JSON round trip through the parser", () => {
+ const snap = { ...emptySnapshot(), notr: true, nop: true, lc: true };
+ const back = parseSnapshot(JSON.parse(JSON.stringify(snap)));
+ assert.ok(back);
+ assert.equal(back.notr, true);
+ assert.equal(back.nop, true);
+ assert.equal(back.lc, true);
+ assert.ok(snapshotsEqual(snap, back));
+});
+
+test("absent reads as the default, and the default is not written", () => {
+ const back = parseSnapshot({ channels: { included: [], excluded: [] } });
+ assert.ok(back);
+ assert.equal("notr" in back, false);
+ assert.equal("lc" in back, false);
+ // A profile saved before the row existed equals one that spells the
+ // defaults out.
+ assert.ok(snapshotsEqual(back, { ...emptySnapshot(), notr: false, lc: false }));
+});
+
+test("a value that is not a boolean is dropped, not coerced", () => {
+ const back = parseSnapshot({
+ channels: { included: [], excluded: [] },
+ notr: "yes",
+ lc: 1,
+ });
+ assert.ok(back);
+ assert.equal(back.notr, undefined);
+ assert.equal(back.lc, undefined);
+});
+
+test("snapshotsEqual tells each of the row's keys apart", () => {
+ const base = emptySnapshot();
+ assert.ok(!snapshotsEqual(base, { ...base, notr: true }));
+ assert.ok(!snapshotsEqual(base, { ...base, lc: true }));
+ assert.ok(!snapshotsEqual({ ...base, notr: true }, { ...base, lc: true }));
+});
+
+test("the working snapshot and a profile keep the row through storage", () => {
+ const store = new Map<string, string>();
+ const g = globalThis as { window?: unknown };
+ const before = g.window;
+ g.window = {
+ localStorage: {
+ getItem: (k: string) => store.get(k) ?? null,
+ setItem: (k: string, v: string) => void store.set(k, v),
+ removeItem: (k: string) => void store.delete(k),
+ },
+ };
+ try {
+ const state = emptyStoredState();
+ state.working = { ...emptySnapshot(), lc: true };
+ state.profiles = { chatOnly: { ...emptySnapshot(), notr: true, nop: true, lc: true } };
+ state.activeProfileName = "chatOnly";
+ saveStoredState(state);
+ const loaded = loadStoredState();
+ assert.ok(loaded);
+ assert.equal(loaded.working.lc, true);
+ assert.equal(loaded.working.notr, undefined);
+ assert.deepEqual(
+ [loaded.profiles.chatOnly.notr, loaded.profiles.chatOnly.nop, loaded.profiles.chatOnly.lc],
+ [true, true, true],
+ );
+ assert.equal(loaded.activeProfileName, "chatOnly");
+ } finally {
+ g.window = before;
+ }
+});
diff --git a/common/components/exportFilterStorage.ts b/common/components/exportFilterStorage.ts
@@ -26,9 +26,17 @@ const VERSION = 1;
export type FilterSnapshot = {
channels: { included: string[]; excluded: string[] };
nov?: boolean;
- // Exclude the social-post corpus — the third media kind beside videos and
- // livestreams (see SearchSessionContext.passesFilter).
+ // The "Search in" row (release 16): what a plain query — a "transcripts"
+ // leaf — reads. Each is stored only off its default, so a profile saved
+ // before the row existed reads transcripts and posts, as it always did.
+ // notr — Transcripts unticked (the transcript cues are not read)
+ // nop — Posts unticked (the social-post corpus; before the row this was
+ // the Posts box in the Type row, and it still keeps the posts
+ // corpus out of the scope a "Posts" leaf reads)
+ // lc — Live chat ticked (the live_chat track is read; off by default)
+ notr?: boolean;
nop?: boolean;
+ lc?: boolean;
nol?: boolean;
naa?: boolean;
nar?: boolean;
@@ -124,7 +132,8 @@ function isStringArray(v: unknown): v is string[] {
return Array.isArray(v) && v.every((x) => typeof x === "string");
}
-function parseSnapshot(raw: unknown): FilterSnapshot | null {
+// Exported for the round-trip test; the app reads through loadStoredState.
+export function parseSnapshot(raw: unknown): FilterSnapshot | null {
if (!raw || typeof raw !== "object") return null;
const r = raw as Record<string, unknown>;
const ch = r.channels as Record<string, unknown> | undefined;
@@ -140,6 +149,8 @@ function parseSnapshot(raw: unknown): FilterSnapshot | null {
// `nop` and `nu` were written by committedSnapshot but never read back here,
// so both were silently dropped on every reload.
if (typeof r.nop === "boolean") snap.nop = r.nop;
+ if (typeof r.notr === "boolean") snap.notr = r.notr;
+ if (typeof r.lc === "boolean") snap.lc = r.lc;
if (typeof r.nol === "boolean") snap.nol = r.nol;
if (typeof r.naa === "boolean") snap.naa = r.naa;
if (typeof r.nar === "boolean") snap.nar = r.nar;
@@ -257,6 +268,8 @@ export function snapshotsEqual(a: FilterSnapshot, b: FilterSnapshot): boolean {
}
if ((a.nov ?? false) !== (b.nov ?? false)) return false;
if ((a.nop ?? false) !== (b.nop ?? false)) return false;
+ if ((a.notr ?? false) !== (b.notr ?? false)) return false;
+ if ((a.lc ?? false) !== (b.lc ?? false)) return false;
if ((a.nol ?? false) !== (b.nol ?? false)) return false;
if ((a.naa ?? false) !== (b.naa ?? false)) return false;
if ((a.nar ?? false) !== (b.nar ?? false)) return false;