mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
fix: show a new mail notification when the push lookup fails (bulwarkmail/webmail#1041)
This commit is contained in:
@@ -218,6 +218,48 @@ describe('pushBackgroundTask notifications', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows a generic notification when the lookup fails, not none (#1041)', async () => {
|
||||||
|
(secureFetch as ReturnType<typeof vi.fn>).mockImplementation(async (url: string) => {
|
||||||
|
if (url.endsWith('/.well-known/jmap')) return { ok: false, status: 503 };
|
||||||
|
throw new Error('unexpected');
|
||||||
|
});
|
||||||
|
await pushBackgroundTask({ kind: 'jmap-email-push', accountLabel: 'alice', accountId: 'jmap-primary', emailIds: '["m1"]' });
|
||||||
|
|
||||||
|
expect(showNotification).toHaveBeenCalledTimes(1);
|
||||||
|
expect((showNotification.mock.calls[0] as unknown[])[0]).toMatchObject({
|
||||||
|
title: 'New email',
|
||||||
|
body: 'alice',
|
||||||
|
emailId: '',
|
||||||
|
threadId: '',
|
||||||
|
groupKey: `bulwark-mail:${LOCAL}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes a method error for a failed lookup, not for "nothing new" (#1041)', async () => {
|
||||||
|
(secureFetch as ReturnType<typeof vi.fn>).mockImplementation(async (url: string) => {
|
||||||
|
if (url.endsWith('/.well-known/jmap')) {
|
||||||
|
return { ok: true, json: async () => ({ apiUrl: 'https://mail.example.com/jmap/', primaryAccounts: { 'urn:ietf:params:jmap:mail': 'jmap-primary' } }) };
|
||||||
|
}
|
||||||
|
return { ok: true, json: async () => ({ methodResponses: [['error', { type: 'serverFail' }, '0']] }) };
|
||||||
|
});
|
||||||
|
await pushBackgroundTask({ kind: 'jmap-state-change', accountLabel: 'alice', changed: '{"jmap-primary":{"EmailDelivery":"s"}}' });
|
||||||
|
|
||||||
|
expect(showNotification).toHaveBeenCalledTimes(1);
|
||||||
|
expect((showNotification.mock.calls[0] as unknown[])[0]).toMatchObject({ title: 'New email', emailId: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stays quiet when the lookup worked and found nothing new', async () => {
|
||||||
|
(secureFetch as ReturnType<typeof vi.fn>).mockImplementation(async (url: string) => {
|
||||||
|
if (url.endsWith('/.well-known/jmap')) {
|
||||||
|
return { ok: true, json: async () => ({ apiUrl: 'https://mail.example.com/jmap/', primaryAccounts: { 'urn:ietf:params:jmap:mail': 'jmap-primary' } }) };
|
||||||
|
}
|
||||||
|
return { ok: true, json: async () => ({ methodResponses: [['Email/get', { list: [{ id: 'm1', keywords: { $seen: true } }] }, '0']] }) };
|
||||||
|
});
|
||||||
|
await pushBackgroundTask({ kind: 'jmap-email-push', accountLabel: 'alice', accountId: 'jmap-primary', emailIds: '["m1"]' });
|
||||||
|
|
||||||
|
expect(showNotification).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("says a message has no subject in the app's language", async () => {
|
it("says a message has no subject in the app's language", async () => {
|
||||||
await AsyncStorage.setItem('webmail:locale:v1', JSON.stringify({ override: 'de' }));
|
await AsyncStorage.setItem('webmail:locale:v1', JSON.stringify({ override: 'de' }));
|
||||||
(secureFetch as ReturnType<typeof vi.fn>).mockImplementation(async (url: string) => ({
|
(secureFetch as ReturnType<typeof vi.fn>).mockImplementation(async (url: string) => ({
|
||||||
|
|||||||
@@ -331,6 +331,31 @@ async function openDetachedSession(accountId: string): Promise<DetachedSession |
|
|||||||
return { apiUrl: rewriteApiUrl(session, baseUrl), authHeader, jmapAccountId };
|
return { apiUrl: rewriteApiUrl(session, baseUrl), authHeader, jmapAccountId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lookup for a push failed: the server, the session or a method answered
|
||||||
|
* with an error. That is not "nothing new" (webmail #1041): the push said
|
||||||
|
* something arrived, so a generic notification is shown instead of none.
|
||||||
|
*/
|
||||||
|
export class PushLookupError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'PushLookupError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The first response, or a PushLookupError when the method failed. */
|
||||||
|
function methodResult(
|
||||||
|
responses: Array<[string, Record<string, any>, string]>,
|
||||||
|
method: string,
|
||||||
|
): Record<string, any> {
|
||||||
|
const [name, body] = responses[0] ?? [];
|
||||||
|
if (name !== method) {
|
||||||
|
const type = name === 'error' && typeof body?.type === 'string' ? body.type : name ?? 'no response';
|
||||||
|
throw new PushLookupError(`${method} failed: ${type}`);
|
||||||
|
}
|
||||||
|
return body ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
async function jmapPost(
|
async function jmapPost(
|
||||||
session: DetachedSession,
|
session: DetachedSession,
|
||||||
methodCalls: JMAPMethodCall[],
|
methodCalls: JMAPMethodCall[],
|
||||||
@@ -353,8 +378,7 @@ async function detachedGetEmails(
|
|||||||
const responses = await jmapPost(session, [
|
const responses = await jmapPost(session, [
|
||||||
['Email/get', { accountId, ids, properties: EMAIL_PROPERTIES }, '0'],
|
['Email/get', { accountId, ids, properties: EMAIL_PROPERTIES }, '0'],
|
||||||
]);
|
]);
|
||||||
const [name, body] = responses[0] ?? [];
|
const body = methodResult(responses, 'Email/get');
|
||||||
if (name !== 'Email/get') return [];
|
|
||||||
return (body.list as Email[]) ?? [];
|
return (body.list as Email[]) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,8 +390,7 @@ async function detachedNewestUnreadInboxIds(
|
|||||||
const mailboxResponses = await jmapPost(session, [
|
const mailboxResponses = await jmapPost(session, [
|
||||||
['Mailbox/get', { accountId, properties: ['id', 'role'] }, '0'],
|
['Mailbox/get', { accountId, properties: ['id', 'role'] }, '0'],
|
||||||
]);
|
]);
|
||||||
const [mbName, mbBody] = mailboxResponses[0] ?? [];
|
const mbBody = methodResult(mailboxResponses, 'Mailbox/get');
|
||||||
if (mbName !== 'Mailbox/get') return [];
|
|
||||||
const inbox = ((mbBody.list as Mailbox[]) ?? []).find((m) => m.role === 'inbox');
|
const inbox = ((mbBody.list as Mailbox[]) ?? []).find((m) => m.role === 'inbox');
|
||||||
if (!inbox) return [];
|
if (!inbox) return [];
|
||||||
const queryResponses = await jmapPost(session, [
|
const queryResponses = await jmapPost(session, [
|
||||||
@@ -382,8 +405,7 @@ async function detachedNewestUnreadInboxIds(
|
|||||||
'0',
|
'0',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
const [qName, qBody] = queryResponses[0] ?? [];
|
const qBody = methodResult(queryResponses, 'Email/query');
|
||||||
if (qName !== 'Email/query') return [];
|
|
||||||
return (qBody.ids as string[]) ?? [];
|
return (qBody.ids as string[]) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +463,8 @@ export async function pushBackgroundTask(data: unknown): Promise<void> {
|
|||||||
accountId,
|
accountId,
|
||||||
err instanceof Error ? err.message : err,
|
err instanceof Error ? err.message : err,
|
||||||
);
|
);
|
||||||
|
// Something arrived; say so even though the lookup couldn't say what.
|
||||||
|
if (!match.ambiguous) await showLookupFailedNotification(accountId, payload).catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -612,6 +636,29 @@ async function showPlainNotification(payload: RelayPushData): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New mail in `accountId` that couldn't be looked up (webmail #1041): the
|
||||||
|
* account's name and no message, so a tap opens the app as it is. One per
|
||||||
|
* account; the next failure replaces it rather than stacking up.
|
||||||
|
*/
|
||||||
|
async function showLookupFailedNotification(accountId: string, payload: RelayPushData): Promise<void> {
|
||||||
|
const native = NativeModules.BulwarkFcm as BulwarkFcmNative | undefined;
|
||||||
|
if (!native?.showNotification) return;
|
||||||
|
const locale = await notificationLocale();
|
||||||
|
const label = payload.accountLabel ?? accountId.split('@')[0] ?? accountId;
|
||||||
|
await native.showNotification({
|
||||||
|
notificationId: `mail-unknown:${accountId}`,
|
||||||
|
title: translate(locale, 'notifications.new_email', 'New email'),
|
||||||
|
body: label,
|
||||||
|
initials: getEmailInitials(label, ''),
|
||||||
|
bgColorHex: hslToHex(generateEmailAvatarColor(label, '')),
|
||||||
|
emailId: '',
|
||||||
|
threadId: '',
|
||||||
|
groupKey: `bulwark-mail:${accountId}`,
|
||||||
|
groupTitle: label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function hslToHex(hsl: string): string {
|
function hslToHex(hsl: string): string {
|
||||||
const match = hsl.match(/hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/);
|
const match = hsl.match(/hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/);
|
||||||
if (!match) return '#2563eb';
|
if (!match) return '#2563eb';
|
||||||
|
|||||||
Reference in New Issue
Block a user