mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 08:07:11 +00:00
fix: turn sender favicons off on upgrade unless they were switched on by choice
This commit is contained in:
@@ -44,6 +44,54 @@ describe('explicitSettings', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('settings saved before a default changed', () => {
|
||||||
|
// Older versions saved the full snapshot, so senderFavicons: true (the old
|
||||||
|
// default) sits in it whether or not the user ever turned it on.
|
||||||
|
it('does not count an old default as chosen', () => {
|
||||||
|
expect(legacyExplicitSettings({ senderFavicons: true, showBirthdayCalendar: true, density: 'compact' })).toEqual(['density']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves an old default to the new one on upgrade, and lets a policy default apply', async () => {
|
||||||
|
await AsyncStorage.setItem('webmail:settings:v1', JSON.stringify({
|
||||||
|
senderFavicons: true,
|
||||||
|
includeGroupInUnified: false,
|
||||||
|
autoSelectReplyIdentity: true,
|
||||||
|
showBirthdayCalendar: true,
|
||||||
|
density: 'compact',
|
||||||
|
}));
|
||||||
|
await useSettingsStore.getState().hydrate();
|
||||||
|
expect(useSettingsStore.getState()).toMatchObject({
|
||||||
|
senderFavicons: false,
|
||||||
|
includeGroupInUnified: true,
|
||||||
|
autoSelectReplyIdentity: false,
|
||||||
|
showBirthdayCalendar: false,
|
||||||
|
density: 'compact',
|
||||||
|
explicitSettings: ['density'],
|
||||||
|
});
|
||||||
|
|
||||||
|
resetPolicyRulesForTests();
|
||||||
|
await AsyncStorage.setItem('webmail:settings:v1', JSON.stringify({ senderFavicons: true, density: 'compact' }));
|
||||||
|
useSettingsStore.setState({ hydrated: false });
|
||||||
|
useSettingsStore.getState().applyPolicyRules(rules({ defaults: { senderFavicons: true, density: 'comfortable' } }));
|
||||||
|
await useSettingsStore.getState().hydrate();
|
||||||
|
// The organisation's default reaches the setting nobody chose.
|
||||||
|
expect(useSettingsStore.getState()).toMatchObject({ senderFavicons: true, density: 'compact', explicitSettings: ['density'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps settings saved with explicitSettings as they are', async () => {
|
||||||
|
await AsyncStorage.setItem('webmail:settings:v1', JSON.stringify({ senderFavicons: true, explicitSettings: ['senderFavicons'] }));
|
||||||
|
await useSettingsStore.getState().hydrate();
|
||||||
|
expect(useSettingsStore.getState()).toMatchObject({ senderFavicons: true, explicitSettings: ['senderFavicons'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats an old default in an imported file the same way', () => {
|
||||||
|
useSettingsStore.setState({ hydrated: true });
|
||||||
|
expect(useSettingsStore.getState().importSettings(JSON.stringify({ senderFavicons: true, dateFormat: 'full' }))).toBe(true);
|
||||||
|
expect(useSettingsStore.getState()).toMatchObject({ senderFavicons: false, dateFormat: 'full' });
|
||||||
|
expect(useSettingsStore.getState().explicitSettings).not.toContain('senderFavicons');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('applyPolicyRules', () => {
|
describe('applyPolicyRules', () => {
|
||||||
it('does not write the defaults over stored settings it has not read yet', async () => {
|
it('does not write the defaults over stored settings it has not read yet', async () => {
|
||||||
await AsyncStorage.setItem('webmail:settings:v1', JSON.stringify({ theme: 'dark', explicitSettings: ['theme'] }));
|
await AsyncStorage.setItem('webmail:settings:v1', JSON.stringify({ theme: 'dark', explicitSettings: ['theme'] }));
|
||||||
|
|||||||
@@ -858,17 +858,54 @@ function policyContext(explicit: readonly string[]): PolicyPatchContext {
|
|||||||
return { builtIn: POLICY_BUILT_IN, explicit: new Set(explicit), isValid: isValidPolicyValue };
|
return { builtIn: POLICY_BUILT_IN, explicit: new Set(explicit), isValid: isValidPolicyValue };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults an earlier version had, for the settings whose default changed
|
||||||
|
* since. Versions before explicitSettings saved the whole snapshot, defaults
|
||||||
|
* included, so a value equal to the old default was most likely never
|
||||||
|
* picked: senderFavicons was on until sender domains stopped going to the
|
||||||
|
* favicon service by default; the other three changed when the defaults were
|
||||||
|
* aligned with the webmail (0.1.59).
|
||||||
|
*/
|
||||||
|
const LEGACY_DEFAULTS: Readonly<Partial<Record<keyof PersistedSettings, unknown>>> = {
|
||||||
|
senderFavicons: true,
|
||||||
|
includeGroupInUnified: false,
|
||||||
|
autoSelectReplyIdentity: true,
|
||||||
|
showBirthdayCalendar: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isLegacyDefault(source: Record<string, unknown>, key: string): boolean {
|
||||||
|
return key in LEGACY_DEFAULTS && source[key] === LEGACY_DEFAULTS[key as keyof PersistedSettings];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings from before explicitSettings existed: a value other than the
|
* Settings from before explicitSettings existed: a value other than the
|
||||||
* build default can only have come from the user, so it counts as chosen
|
* build default can only have come from the user, so it counts as chosen
|
||||||
* (webmail legacyExplicitSettings).
|
* (webmail legacyExplicitSettings). A value equal to that setting's old
|
||||||
|
* default doesn't (webmail migrateNotificationSoundDefault).
|
||||||
*/
|
*/
|
||||||
export function legacyExplicitSettings(source: Record<string, unknown>): string[] {
|
export function legacyExplicitSettings(source: Record<string, unknown>): string[] {
|
||||||
return Object.keys(source).filter(
|
return Object.keys(source).filter(
|
||||||
(key) => isPolicySettingKey(key) && source[key] !== undefined && source[key] !== POLICY_BUILT_IN[key],
|
(key) =>
|
||||||
|
isPolicySettingKey(key) &&
|
||||||
|
source[key] !== undefined &&
|
||||||
|
source[key] !== POLICY_BUILT_IN[key] &&
|
||||||
|
!isLegacyDefault(source, key),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a setting still at its old default, in settings from before
|
||||||
|
* explicitSettings existed, to the current default: the user never picked
|
||||||
|
* it (webmail migrateNotificationSoundDefault, migrateTintListRowsDefault).
|
||||||
|
*/
|
||||||
|
export function migrateLegacyDefaults<T extends Record<string, unknown>>(source: T): T {
|
||||||
|
const out: Record<string, unknown> = { ...source };
|
||||||
|
for (const key of Object.keys(LEGACY_DEFAULTS)) {
|
||||||
|
if (isLegacyDefault(out, key)) out[key] = DEFAULT_PERSISTED[key as keyof PersistedSettings];
|
||||||
|
}
|
||||||
|
return out as T;
|
||||||
|
}
|
||||||
|
|
||||||
/** Whether the organisation policy pins `key`, so the user can't change it. */
|
/** Whether the organisation policy pins `key`, so the user can't change it. */
|
||||||
export function isSettingLockedByPolicy(key: string): boolean {
|
export function isSettingLockedByPolicy(key: string): boolean {
|
||||||
return isLockedSetting(policyRules, key);
|
return isLockedSetting(policyRules, key);
|
||||||
@@ -942,9 +979,10 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
|||||||
const raw = await AsyncStorage.getItem(STORAGE_KEY);
|
const raw = await AsyncStorage.getItem(STORAGE_KEY);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed = JSON.parse(raw) as Partial<PersistedSettings>;
|
const parsed = JSON.parse(raw) as Partial<PersistedSettings>;
|
||||||
const merged = mergeWithDefaults(parsed);
|
let merged = mergeWithDefaults(parsed);
|
||||||
if (!Array.isArray(parsed.explicitSettings)) {
|
if (!Array.isArray(parsed.explicitSettings)) {
|
||||||
merged.explicitSettings = legacyExplicitSettings(merged as unknown as Record<string, unknown>);
|
merged.explicitSettings = legacyExplicitSettings(merged as unknown as Record<string, unknown>);
|
||||||
|
merged = migrateLegacyDefaults(merged as unknown as Record<string, unknown>) as unknown as PersistedSettings;
|
||||||
}
|
}
|
||||||
// The policy may have arrived (from its cache) before the settings.
|
// The policy may have arrived (from its cache) before the settings.
|
||||||
const patch = currentPolicyPatch(merged);
|
const patch = currentPolicyPatch(merged);
|
||||||
@@ -1089,12 +1127,15 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
|
||||||
const incoming = fromExportShape(parsed as Record<string, unknown>);
|
let incoming = fromExportShape(parsed as Record<string, unknown>);
|
||||||
// A file without explicitSettings (older clients): what it sets to
|
// A file without explicitSettings (older clients): what it sets to
|
||||||
// something other than the default was chosen.
|
// something other than the default was chosen, and what is still at an
|
||||||
const chosen = Array.isArray(incoming.explicitSettings)
|
// old default moves to the current one.
|
||||||
? incoming.explicitSettings.filter((key): key is string => typeof key === 'string')
|
const legacy = !Array.isArray(incoming.explicitSettings);
|
||||||
|
const chosen = !legacy
|
||||||
|
? (incoming.explicitSettings as unknown[]).filter((key): key is string => typeof key === 'string')
|
||||||
: legacyExplicitSettings(incoming as Record<string, unknown>);
|
: legacyExplicitSettings(incoming as Record<string, unknown>);
|
||||||
|
if (legacy) incoming = migrateLegacyDefaults(incoming as Record<string, unknown>) as typeof incoming;
|
||||||
// Validate against the current state so keys absent from the file keep
|
// Validate against the current state so keys absent from the file keep
|
||||||
// their value instead of snapping back to the default.
|
// their value instead of snapping back to the default.
|
||||||
const merged = mergeWithDefaults({
|
const merged = mergeWithDefaults({
|
||||||
|
|||||||
Reference in New Issue
Block a user