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()));
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const q = P.param(prefs.data);
|
||||
const ex = Array.from(dismissed).join(',');
|
||||
@@ -227,17 +234,22 @@
|
||||
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
|
||||
}
|
||||
const sig = briefSig();
|
||||
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 =
|
||||
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) {
|
||||
const freshById = new Map(fetched.items.map((a) => [a.id, a]));
|
||||
const items = view.items.map((it) => freshById.get(it.id) ?? it);
|
||||
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 {
|
||||
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;
|
||||
markDisplayed(brief.items);
|
||||
@@ -441,7 +453,10 @@
|
||||
// shows on a genuine first visit with nothing cached yet.
|
||||
if (selected === 'today') {
|
||||
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;
|
||||
heroIdx = 0;
|
||||
loading = false;
|
||||
|
||||
Reference in New Issue
Block a user