mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
fix: say that a send failed because the device is offline and the message is still there
This commit is contained in:
@@ -15,7 +15,7 @@ import { sendEmail, patchKeywordsForEmails, SendUnconfirmedError } from '../../a
|
|||||||
import { jmapClient } from '../../api/jmap-client';
|
import { jmapClient } from '../../api/jmap-client';
|
||||||
import { pruneEmailSubmissions } from '../../api/submissions';
|
import { pruneEmailSubmissions } from '../../api/submissions';
|
||||||
import { buildReplyRecipients } from '../../lib/reply-recipients';
|
import { buildReplyRecipients } from '../../lib/reply-recipients';
|
||||||
import { partlyRefusedWarning, sendFailureMessage } from '../../lib/send-failure-message';
|
import { composerSendFailureMessage, partlyRefusedWarning } from '../../lib/send-failure-message';
|
||||||
import { buildReplySubject } from '../../lib/subject-prefix';
|
import { buildReplySubject } from '../../lib/subject-prefix';
|
||||||
import { computeReplyThreadingHeaders } from '../../lib/email-threading';
|
import { computeReplyThreadingHeaders } from '../../lib/email-threading';
|
||||||
import { resolveReplyIdentity } from '../../lib/reply-identity';
|
import { resolveReplyIdentity } from '../../lib/reply-identity';
|
||||||
@@ -255,7 +255,7 @@ export function QuickReplyBox({ email, jmapAccountId, onMoreOptions, onSent, onF
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Alert.alert(t('email_composer.send_failed', 'Failed to send'), sendFailureMessage(err, t));
|
Alert.alert(t('email_composer.send_failed', 'Failed to send'), composerSendFailureMessage(err, t));
|
||||||
} finally {
|
} finally {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { RequestTimeoutError } from '../../api/jmap-client';
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { NetworkError, RequestTimeoutError } from '../../api/jmap-client';
|
||||||
|
import { useNetworkStore } from '../../stores/network-store';
|
||||||
import {
|
import {
|
||||||
JMAPMethodError, NoLongerScheduledError, RecipientsRefusedError, ScheduleTooLateError, SubmissionQuotaError,
|
JMAPMethodError, NoLongerScheduledError, RecipientsRefusedError, ScheduleTooLateError, SubmissionQuotaError,
|
||||||
formatRefusedRecipients,
|
formatRefusedRecipients,
|
||||||
} from '../../api/jmap-result';
|
} from '../../api/jmap-result';
|
||||||
import { partlyRefusedWarning, rescheduleFailureNotice, sendFailureMessage } from '../send-failure-message';
|
import {
|
||||||
|
composerSendFailureMessage, partlyRefusedWarning, rescheduleFailureNotice, sendFailureMessage,
|
||||||
|
} from '../send-failure-message';
|
||||||
|
|
||||||
// The English fallback stands in for the catalog.
|
// The English fallback stands in for the catalog.
|
||||||
const t = (key: string, fallback?: string, params?: Record<string, unknown>) =>
|
const t = (key: string, fallback?: string, params?: Record<string, unknown>) =>
|
||||||
@@ -97,3 +102,34 @@ describe('recipients the server refused', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('composerSendFailureMessage (offline)', () => {
|
||||||
|
const OFFLINE = /You seem to be offline\. The message is still here/;
|
||||||
|
|
||||||
|
it('says offline instead of the transport error', () => {
|
||||||
|
expect(composerSendFailureMessage(new NetworkError('Network request failed'), t)).toMatch(OFFLINE);
|
||||||
|
expect(composerSendFailureMessage(new TypeError('Network request failed'), t)).toMatch(OFFLINE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('says offline when the device reports no connection', () => {
|
||||||
|
useNetworkStore.setState({ online: false });
|
||||||
|
try {
|
||||||
|
expect(composerSendFailureMessage(new Error('boom'), t)).toMatch(OFFLINE);
|
||||||
|
} finally {
|
||||||
|
useNetworkStore.setState({ online: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the server's words and the timeout advice when online", () => {
|
||||||
|
expect(composerSendFailureMessage(new Error('Relay denied'), t)).toBe('Relay denied');
|
||||||
|
expect(composerSendFailureMessage(new RequestTimeoutError(30_000), t)).toMatch(/Check your Sent folder/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is what the composer and quick reply show', () => {
|
||||||
|
for (const rel of ['../../screens/ComposeScreen.tsx', '../../components/email/QuickReplyBox.tsx']) {
|
||||||
|
const source = fs.readFileSync(path.resolve(__dirname, rel), 'utf8');
|
||||||
|
expect(source).toMatch(/Alert\.alert\(t\('email_composer\.send_failed', '[^']+'\), composerSendFailureMessage\((e|err), t\)\);/);
|
||||||
|
expect(source).not.toMatch(/[^r]sendFailureMessage\(/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RequestTimeoutError } from '../api/jmap-client';
|
import { NetworkError, RequestTimeoutError } from '../api/jmap-client';
|
||||||
|
import { useNetworkStore } from '../stores/network-store';
|
||||||
import {
|
import {
|
||||||
NoLongerScheduledError, RecipientsRefusedError, ScheduleTooLateError, SubmissionQuotaError, formatRefusedRecipients,
|
NoLongerScheduledError, RecipientsRefusedError, ScheduleTooLateError, SubmissionQuotaError, formatRefusedRecipients,
|
||||||
type RefusedRecipient,
|
type RefusedRecipient,
|
||||||
@@ -62,6 +63,27 @@ export function sendFailureMessage(error: unknown, t: Translate): string {
|
|||||||
: t('notifications.error_sending', 'Failed to send email');
|
: t('notifications.error_sending', 'Failed to send email');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sendFailureMessage for a message the user is writing (composer, quick
|
||||||
|
* reply): a send that never reached the server because the device is
|
||||||
|
* offline says so, and that the text is still there, instead of "Network
|
||||||
|
* request failed" (webmail email-composer.tsx send_offline). The server is
|
||||||
|
* unreachable when the transport failed (the client's NetworkError, or a
|
||||||
|
* bare fetch TypeError) or the device reports no connection.
|
||||||
|
*/
|
||||||
|
export function composerSendFailureMessage(error: unknown, t: Translate): string {
|
||||||
|
const offline = error instanceof NetworkError
|
||||||
|
|| error instanceof TypeError
|
||||||
|
|| !useNetworkStore.getState().online;
|
||||||
|
if (offline && !(error instanceof RequestTimeoutError)) {
|
||||||
|
return t(
|
||||||
|
'email_composer.send_offline',
|
||||||
|
'You seem to be offline. The message is still here, send it again once you are back online.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sendFailureMessage(error, t);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The title and message for a "Send now" or reschedule that failed: most
|
* The title and message for a "Send now" or reschedule that failed: most
|
||||||
* failures get `failTitle` and sendFailureMessage (webmail
|
* failures get `failTitle` and sendFailureMessage (webmail
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
} from '../api/email';
|
} from '../api/email';
|
||||||
import { jmapClient, RequestTimeoutError, ScheduleTooLateError } from '../api/jmap-client';
|
import { jmapClient, RequestTimeoutError, ScheduleTooLateError } from '../api/jmap-client';
|
||||||
import { pruneEmailSubmissions } from '../api/submissions';
|
import { pruneEmailSubmissions } from '../api/submissions';
|
||||||
import { partlyRefusedWarning, sendFailureMessage } from '../lib/send-failure-message';
|
import { composerSendFailureMessage, partlyRefusedWarning } from '../lib/send-failure-message';
|
||||||
import { buildReplyRecipients, type ReplySource } from '../lib/reply-recipients';
|
import { buildReplyRecipients, type ReplySource } from '../lib/reply-recipients';
|
||||||
import { buildReplySubject, buildForwardSubject } from '../lib/subject-prefix';
|
import { buildReplySubject, buildForwardSubject } from '../lib/subject-prefix';
|
||||||
import { computeReplyThreadingHeaders, generateMessageId, stripMessageIdBrackets } from '../lib/email-threading';
|
import { computeReplyThreadingHeaders, generateMessageId, stripMessageIdBrackets } from '../lib/email-threading';
|
||||||
@@ -2559,7 +2559,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Alert.alert(t('email_composer.send_failed', 'Send failed'), sendFailureMessage(e, t));
|
Alert.alert(t('email_composer.send_failed', 'Send failed'), composerSendFailureMessage(e, t));
|
||||||
} finally {
|
} finally {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user