diff --git a/src/lib/__tests__/push-background-task.test.ts b/src/lib/__tests__/push-background-task.test.ts index b628ed9b..571973e0 100644 --- a/src/lib/__tests__/push-background-task.test.ts +++ b/src/lib/__tests__/push-background-task.test.ts @@ -10,10 +10,12 @@ vi.mock('../../api/jmap-client', () => ({ vi.mock('../client-cert', () => ({ secureFetch: vi.fn(async () => ({ ok: false, status: 500 })) })); vi.mock('../oauth', () => ({ refreshOAuthAccessToken: vi.fn(async (t: unknown) => t) })); +import AsyncStorage from '@react-native-async-storage/async-storage'; import { matchAccountsForPush, parseRelayPushData, selectNotifiableEmails, + senderFaviconsAllowed, } from '../push-background-task'; import type { Email } from '../../api/types'; @@ -88,3 +90,22 @@ describe('selectNotifiableEmails', () => { expect(out.map((e) => e.id)).toEqual(['a']); }); }); + +describe('senderFaviconsAllowed', () => { + const KEY = 'webmail:settings:v1'; + + it('follows the "Sender Favicons" setting the settings store persisted', async () => { + await AsyncStorage.setItem(KEY, JSON.stringify({ senderFavicons: false })); + expect(await senderFaviconsAllowed()).toBe(false); + await AsyncStorage.setItem(KEY, JSON.stringify({ senderFavicons: true })); + expect(await senderFaviconsAllowed()).toBe(true); + }); + + it('defaults to on like the store, but skips icons when the settings are unreadable', async () => { + await AsyncStorage.removeItem(KEY); + expect(await senderFaviconsAllowed()).toBe(true); + await AsyncStorage.setItem(KEY, '{not json'); + expect(await senderFaviconsAllowed()).toBe(false); + await AsyncStorage.removeItem(KEY); + }); +}); diff --git a/src/lib/push-background-task.ts b/src/lib/push-background-task.ts index 3e0b47a0..8ca29175 100644 --- a/src/lib/push-background-task.ts +++ b/src/lib/push-background-task.ts @@ -42,6 +42,7 @@ const EMAIL_PROPERTIES = [ interface PushPersistedSettings { emailNotificationsEnabled?: boolean; + senderFavicons?: boolean; } async function emailNotificationsAllowed(): Promise { @@ -55,6 +56,20 @@ async function emailNotificationsAllowed(): Promise { } } +// The "Sender Favicons" setting covers notification icons as well as the +// in-app avatars: with it off the tray shows initials, and the sender's domain +// and the device's IP never reach the icon service. Off when unreadable. +export async function senderFaviconsAllowed(): Promise { + try { + const raw = await AsyncStorage.getItem(SETTINGS_STORAGE_KEY); + if (!raw) return true; // first launch: the store's default is on + const parsed = JSON.parse(raw) as PushPersistedSettings; + return parsed.senderFavicons !== false; + } catch { + return false; + } +} + interface ShowNotificationOptions { notificationId: string; title: string; @@ -429,6 +444,7 @@ async function processAccountForPush(accountId: string, payload: RelayPushData): const groupKey = `bulwark-mail:${accountId}`; const groupTitle = payload.accountLabel ?? accountId.split('@')[0] ?? accountId; + const favicons = await senderFaviconsAllowed(); // Oldest first so the newest ends up on top of the tray. const ordered = [...toNotify].sort( @@ -442,7 +458,7 @@ async function processAccountForPush(accountId: string, payload: RelayPushData): const body = email.subject || '(no subject)'; const initials = getEmailInitials(name, address); const bgColorHex = hslToHex(generateEmailAvatarColor(name, address)); - const faviconDomain = getFaviconDomain(address); + const faviconDomain = favicons ? getFaviconDomain(address) : null; const iconUrl = faviconDomain ? getFaviconUrl(faviconDomain) : undefined; await native.showNotification({