Home: make instant-paint boundary-aware (Codex)
Codex caught a trust bug: instant-painting a brief saved under OLD boundaries could briefly flash content the reader's current settings should hide — and boundaries are trust-critical for this product. Add a filter signature (prefs param + sorted dismissals) saved alongside the brief; instant-paint and the merge-fallback only reuse a saved brief when the signature still matches the current settings. A mismatch falls through to "Gathering…" + a fresh fetch. Also closes the same latent leak in the merge's `?? it` fallback. Briefs saved before this change lack a sig → won't instant-paint until re-saved (fails safe). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -217,6 +217,13 @@
|
|||||||
return P.param(P.merge(prefs.data, viewFilter()));
|
return P.param(P.merge(prefs.data, viewFilter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signature of the filters that shaped a brief — boundaries/prefs + dismissals.
|
||||||
|
// Instant-paint and the merge only reuse a saved brief when this still matches,
|
||||||
|
// so a boundary change can never briefly resurface content it should now hide.
|
||||||
|
function briefSig() {
|
||||||
|
return P.param(prefs.data) + '|' + Array.from(dismissed).sort().join(',');
|
||||||
|
}
|
||||||
|
|
||||||
async function loadToday(fresh) {
|
async function loadToday(fresh) {
|
||||||
const q = P.param(prefs.data);
|
const q = P.param(prefs.data);
|
||||||
const ex = Array.from(dismissed).join(',');
|
const ex = Array.from(dismissed).join(',');
|
||||||
@@ -227,17 +234,22 @@
|
|||||||
if (brief) return; // already showing a saved brief — a failed background refresh stays invisible
|
if (brief) return; // already showing a saved brief — a failed background refresh stays invisible
|
||||||
throw e; // true first load with nothing painted → let the caller surface the error
|
throw e; // true first load with nothing painted → let the caller surface the error
|
||||||
}
|
}
|
||||||
|
const sig = briefSig();
|
||||||
const view = P.loadJSON(BRIEF_VIEW_KEY, null);
|
const view = P.loadJSON(BRIEF_VIEW_KEY, null);
|
||||||
|
// Reuse the saved arrangement only when BOTH the server brief and the reader's
|
||||||
|
// filter signature are unchanged — otherwise the merge's `?? it` fallback could
|
||||||
|
// re-add a story the current boundaries now hide.
|
||||||
const sameServerBrief =
|
const sameServerBrief =
|
||||||
view && view.generated_at && fetched.generated_at && view.generated_at === fetched.generated_at;
|
view && view.generated_at && fetched.generated_at &&
|
||||||
|
view.generated_at === fetched.generated_at && view.sig === sig;
|
||||||
if (!fresh && sameServerBrief && Array.isArray(view.items) && view.items.length) {
|
if (!fresh && sameServerBrief && Array.isArray(view.items) && view.items.length) {
|
||||||
const freshById = new Map(fetched.items.map((a) => [a.id, a]));
|
const freshById = new Map(fetched.items.map((a) => [a.id, a]));
|
||||||
const items = view.items.map((it) => freshById.get(it.id) ?? it);
|
const items = view.items.map((it) => freshById.get(it.id) ?? it);
|
||||||
brief = { ...fetched, items };
|
brief = { ...fetched, items };
|
||||||
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items });
|
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items, sig });
|
||||||
} else {
|
} else {
|
||||||
brief = fetched;
|
brief = fetched;
|
||||||
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items });
|
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items, sig });
|
||||||
}
|
}
|
||||||
heroIdx = 0;
|
heroIdx = 0;
|
||||||
markDisplayed(brief.items);
|
markDisplayed(brief.items);
|
||||||
@@ -441,7 +453,10 @@
|
|||||||
// shows on a genuine first visit with nothing cached yet.
|
// shows on a genuine first visit with nothing cached yet.
|
||||||
if (selected === 'today') {
|
if (selected === 'today') {
|
||||||
const cached = P.loadJSON(BRIEF_VIEW_KEY, null);
|
const cached = P.loadJSON(BRIEF_VIEW_KEY, null);
|
||||||
if (cached && Array.isArray(cached.items) && cached.items.length) {
|
// Only paint instantly if the saved brief was shaped by the SAME
|
||||||
|
// boundaries/prefs/dismissals — never flash content the current settings
|
||||||
|
// should hide. A mismatch falls through to "Gathering…" + a fresh fetch.
|
||||||
|
if (cached && Array.isArray(cached.items) && cached.items.length && cached.sig === briefSig()) {
|
||||||
brief = cached;
|
brief = cached;
|
||||||
heroIdx = 0;
|
heroIdx = 0;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user