Skip to content

Commit f51ec50

Browse files
shukivrathlinus
authored andcommitted
feat(settings): add "Refresh cached data" recovery action
When the mailbox view gets into a stale or wrong state, the only escape was the browser's "clear site data" — which also wipes the saved account list, forcing a re-login of every account. Add a non-destructive "Refresh cached data" button under Settings → Data. It clears the server-derived caches (contacts, calendars, identities, per-account snapshots) and reloads so they re-fetch fresh, while preserving accounts, sessions, settings, themes and user content (templates, S/MIME). Two-click confirm to avoid an accidental reload. English strings added across all locales (translation follow-up); unit tests cover the cache-clear (keeps account-registry/auth/prefs) and the reload.
1 parent f2703bc commit f51ec50

23 files changed

Lines changed: 216 additions & 0 deletions

components/settings/about-data-settings.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useUpdateStore } from '@/stores/update-store';
1111
import { ExternalLink } from 'lucide-react';
1212
import { cn } from '@/lib/utils';
1313
import { getPathPrefix } from '@/lib/browser-navigation';
14+
import { clearCachedData } from '@/lib/clear-cached-data';
1415
import { SpamSiegeGame } from './spam-siege-game';
1516

1617
const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
@@ -54,6 +55,7 @@ export function AboutDataSettings() {
5455
useSettingsStore();
5556
const { settingsSyncEnabled } = useConfig();
5657
const [showResetConfirm, setShowResetConfirm] = useState(false);
58+
const [showRefreshConfirm, setShowRefreshConfirm] = useState(false);
5759
const fileInputRef = useRef<HTMLInputElement>(null);
5860
const { isFeatureEnabled } = usePolicyStore();
5961
const [showGame, setShowGame] = useState(false);
@@ -105,6 +107,15 @@ export function AboutDataSettings() {
105107
reader.readAsText(file);
106108
};
107109

110+
const handleRefreshCache = () => {
111+
if (showRefreshConfirm) {
112+
clearCachedData(); // reloads the page
113+
} else {
114+
setShowRefreshConfirm(true);
115+
setTimeout(() => setShowRefreshConfirm(false), 5000);
116+
}
117+
};
118+
108119
const handleReset = () => {
109120
if (showResetConfirm) {
110121
resetToDefaults();
@@ -187,6 +198,16 @@ export function AboutDataSettings() {
187198
</SettingItem>
188199
)}
189200

201+
<SettingItem label={t('refresh_cache.label')} description={t('refresh_cache.description')}>
202+
<Button
203+
variant={showRefreshConfirm ? 'default' : 'outline'}
204+
size="sm"
205+
onClick={handleRefreshCache}
206+
>
207+
{showRefreshConfirm ? tCommon('yes') : t('refresh_cache.button')}
208+
</Button>
209+
</SettingItem>
210+
190211
<SettingItem label={t('reset_settings.label')} description={t('reset_settings.description')}>
191212
<Button
192213
variant={showResetConfirm ? 'destructive' : 'outline'}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2+
import { clearCachedData } from '../clear-cached-data';
3+
4+
describe('clearCachedData', () => {
5+
let reload: ReturnType<typeof vi.fn>;
6+
let originalLocation: Location;
7+
8+
beforeEach(() => {
9+
localStorage.clear();
10+
reload = vi.fn();
11+
originalLocation = window.location;
12+
Object.defineProperty(window, 'location', {
13+
configurable: true,
14+
value: { ...originalLocation, reload },
15+
});
16+
});
17+
18+
afterEach(() => {
19+
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
20+
});
21+
22+
it('clears re-fetchable caches but keeps accounts, sessions and prefs', () => {
23+
localStorage.setItem('contact-storage', '1');
24+
localStorage.setItem('calendar-storage', '1');
25+
localStorage.setItem('identity-storage', '1');
26+
localStorage.setItem('calendar-notification-storage', '1');
27+
// Must survive — losing these is exactly the pain we're avoiding.
28+
localStorage.setItem('account-registry', 'accounts');
29+
localStorage.setItem('auth-storage', 'session');
30+
localStorage.setItem('settings-storage', 'prefs');
31+
localStorage.setItem('template-storage', 'my templates');
32+
33+
clearCachedData();
34+
35+
expect(localStorage.getItem('contact-storage')).toBeNull();
36+
expect(localStorage.getItem('calendar-storage')).toBeNull();
37+
expect(localStorage.getItem('identity-storage')).toBeNull();
38+
expect(localStorage.getItem('calendar-notification-storage')).toBeNull();
39+
40+
expect(localStorage.getItem('account-registry')).toBe('accounts');
41+
expect(localStorage.getItem('auth-storage')).toBe('session');
42+
expect(localStorage.getItem('settings-storage')).toBe('prefs');
43+
expect(localStorage.getItem('template-storage')).toBe('my templates');
44+
});
45+
46+
it('reloads so data is re-fetched fresh', () => {
47+
clearCachedData();
48+
expect(reload).toHaveBeenCalledOnce();
49+
});
50+
});

lib/clear-cached-data.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { evictAll } from '@/lib/account-state-manager';
2+
3+
// localStorage keys holding server-derived, re-fetchable caches. The "Refresh
4+
// cached data" action clears these so a stale or wrong-account view can be
5+
// fixed WITHOUT signing out (which would drop the whole account list).
6+
//
7+
// Deliberately excluded:
8+
// - 'account-registry' / 'auth-storage' → keep accounts + sessions
9+
// - 'settings-storage' / 'theme-storage' / 'locale-storage' → user prefs
10+
// - 'template-storage' / 'smime-preferences' → user-created content
11+
const CACHE_STORAGE_KEYS = [
12+
'identity-storage',
13+
'contact-storage',
14+
'calendar-storage',
15+
'calendar-notification-storage',
16+
];
17+
18+
/**
19+
* Clear cached, server-derived data (contacts, calendars, identities, and the
20+
* in-memory per-account snapshots), then reload so everything is re-fetched
21+
* fresh for the active account. Accounts and sessions are preserved — this is
22+
* the non-destructive alternative to the browser's "clear site data", which
23+
* also wipes the account list.
24+
*/
25+
export function clearCachedData(): void {
26+
// Drop the in-memory per-account store snapshots so a reload can't restore
27+
// stale cached state for any account.
28+
try {
29+
evictAll();
30+
} catch {
31+
/* snapshots are best-effort */
32+
}
33+
34+
if (typeof window === 'undefined') return;
35+
36+
for (const key of CACHE_STORAGE_KEYS) {
37+
try {
38+
window.localStorage.removeItem(key);
39+
} catch {
40+
/* ignore storage access errors */
41+
}
42+
}
43+
44+
window.location.reload();
45+
}

locales/cs/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,11 @@
16281628
"description": "Zobrazit dostupné klávesové zkratky",
16291629
"button": "Zobrazit zkratky"
16301630
},
1631+
"refresh_cache": {
1632+
"label": "Refresh cached data",
1633+
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
1634+
"button": "Refresh"
1635+
},
16311636
"reset_settings": {
16321637
"label": "Resetovat nastavení",
16331638
"description": "Obnovit všechna nastavení na výchozí hodnoty",

locales/da/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,6 +1631,11 @@
16311631
"description": "Se tilgængelige tastaturgenveje",
16321632
"button": "Vis genveje"
16331633
},
1634+
"refresh_cache": {
1635+
"label": "Refresh cached data",
1636+
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
1637+
"button": "Refresh"
1638+
},
16341639
"reset_settings": {
16351640
"label": "Nulstil indstillinger",
16361641
"description": "Gendan alle indstillinger til standardværdier",

locales/de/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,11 @@
16281628
"description": "Verfügbare Tastaturkürzel anzeigen",
16291629
"button": "Tastaturkürzel anzeigen"
16301630
},
1631+
"refresh_cache": {
1632+
"label": "Refresh cached data",
1633+
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
1634+
"button": "Refresh"
1635+
},
16311636
"reset_settings": {
16321637
"label": "Einstellungen zurücksetzen",
16331638
"description": "Alle Einstellungen auf Standardwerte zurücksetzen",

locales/en/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,6 +1637,11 @@
16371637
"description": "View available keyboard shortcuts",
16381638
"button": "View Shortcuts"
16391639
},
1640+
"refresh_cache": {
1641+
"label": "Refresh cached data",
1642+
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
1643+
"button": "Refresh"
1644+
},
16401645
"reset_settings": {
16411646
"label": "Reset Settings",
16421647
"description": "Restore all settings to default values",

locales/es/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,11 @@
16281628
"description": "Ver atajos de teclado disponibles",
16291629
"button": "Ver Atajos"
16301630
},
1631+
"refresh_cache": {
1632+
"label": "Refresh cached data",
1633+
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
1634+
"button": "Refresh"
1635+
},
16311636
"reset_settings": {
16321637
"label": "Restablecer Configuración",
16331638
"description": "Restaurar toda la configuración a los valores predeterminados",

locales/fa/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,11 @@
16351635
"description": "مشاهده میانبرهای صفحه کلید موجود",
16361636
"button": "مشاهده میانبرها"
16371637
},
1638+
"refresh_cache": {
1639+
"label": "Refresh cached data",
1640+
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
1641+
"button": "Refresh"
1642+
},
16381643
"reset_settings": {
16391644
"label": "بازنشانی تنظیمات",
16401645
"description": "بازگردانی همه تنظیمات به مقادیر پیش‌فرض",

locales/fr/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,11 @@
16281628
"description": "Voir les raccourcis clavier disponibles",
16291629
"button": "Voir les raccourcis"
16301630
},
1631+
"refresh_cache": {
1632+
"label": "Refresh cached data",
1633+
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
1634+
"button": "Refresh"
1635+
},
16311636
"reset_settings": {
16321637
"label": "Réinitialiser les paramètres",
16331638
"description": "Restaurer tous les paramètres par défaut",

0 commit comments

Comments
 (0)