mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: say when a swipe, bulk action or import fails instead of failing silently
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
const error = vi.fn();
|
||||
vi.mock('../../stores/toast-store', () => ({ toast: { error: (...a: unknown[]) => error(...a) } }));
|
||||
vi.mock('../../stores/locale-store', () => ({
|
||||
useLocaleStore: { getState: () => ({ t: (_k: string, fallback: string) => fallback }) },
|
||||
}));
|
||||
|
||||
import { reportFailure } from '../report-failure';
|
||||
|
||||
describe('reportFailure', () => {
|
||||
it('shows a toast when the action is refused', async () => {
|
||||
reportFailure(Promise.reject(new Error('notUpdated: forbidden')));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(error).toHaveBeenCalledWith('Failed to update email', 'notUpdated: forbidden');
|
||||
});
|
||||
|
||||
it('stays quiet when the action works', async () => {
|
||||
error.mockClear();
|
||||
reportFailure(Promise.resolve());
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { toast } from '../stores/toast-store';
|
||||
import { useLocaleStore } from '../stores/locale-store';
|
||||
|
||||
/**
|
||||
* Runs a store action the screen doesn't wait for (a swipe, a bulk button)
|
||||
* and says when it fails. These used to be `void action()`: a change the
|
||||
* server refused (not a dropped connection, which the outbox queues) went
|
||||
* unnoticed.
|
||||
*/
|
||||
export function reportFailure(action: Promise<unknown>): void {
|
||||
action.catch((err: unknown) => {
|
||||
toast.error(
|
||||
useLocaleStore.getState().t('notifications.error_updating', 'Failed to update email'),
|
||||
err instanceof Error ? err.message : undefined,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import { useSettingsStore, type SwipeAction, type SwipeMode } from '../stores/se
|
||||
import { useKeywordsStore, type KeywordDef } from '../stores/keywords-store';
|
||||
import { useBackWhileFocused } from '../lib/use-back-while-focused';
|
||||
import { listBackStep } from '../lib/list-back';
|
||||
import { reportFailure } from '../lib/report-failure';
|
||||
import { useLayout } from '../lib/use-layout';
|
||||
import { useSendUndoStore } from '../stores/send-undo-store';
|
||||
import { useLocaleStore } from '../stores/locale-store';
|
||||
@@ -535,6 +536,12 @@ export default function EmailListScreen({
|
||||
? t('email_list.import.none', 'No messages were imported.')
|
||||
: t('email_list.import.success', '{count, plural, one {# message imported.} other {# messages imported.}}', { count: imported }),
|
||||
);
|
||||
} catch (err) {
|
||||
// A failed import used to end without a word.
|
||||
Alert.alert(
|
||||
t('email_list.import.title', 'Import'),
|
||||
err instanceof Error ? err.message : t('email_list.import.none', 'No messages were imported.'),
|
||||
);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
@@ -644,8 +651,8 @@ export default function EmailListScreen({
|
||||
case 'archive':
|
||||
if (archiveMailboxId && currentMailboxId !== archiveMailboxId) {
|
||||
const ids = idsForRow(id);
|
||||
if (ids.length > 1) void archiveEmailsBatch(ids);
|
||||
else void archiveEmailAction(id);
|
||||
if (ids.length > 1) reportFailure(archiveEmailsBatch(ids));
|
||||
else reportFailure(archiveEmailAction(id));
|
||||
} else if (!archiveMailboxId) {
|
||||
Alert.alert(
|
||||
t('email_list.error', 'Error'),
|
||||
@@ -654,16 +661,16 @@ export default function EmailListScreen({
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
void deleteIds(idsForRow(id));
|
||||
reportFailure(deleteIds(idsForRow(id)));
|
||||
break;
|
||||
case 'spam':
|
||||
// Your own outgoing mail is never spam (webmail hides the action in
|
||||
// Sent/Drafts); inside Junk the same swipe means "not spam".
|
||||
if (currentRole === 'sent' || currentRole === 'drafts') break;
|
||||
if (inJunk) {
|
||||
void unmarkSpam(idsForRow(id));
|
||||
reportFailure(unmarkSpam(idsForRow(id)));
|
||||
} else if (junkMailboxId) {
|
||||
void markSpam(idsForRow(id));
|
||||
reportFailure(markSpam(idsForRow(id)));
|
||||
} else {
|
||||
Alert.alert(
|
||||
t('email_list.error', 'Error'),
|
||||
@@ -672,14 +679,14 @@ export default function EmailListScreen({
|
||||
}
|
||||
break;
|
||||
case 'read':
|
||||
if (isUnread(email)) void markRead(id);
|
||||
else void markUnread(id);
|
||||
if (isUnread(email)) reportFailure(markRead(id));
|
||||
else reportFailure(markUnread(id));
|
||||
break;
|
||||
case 'star':
|
||||
void toggleStar(id, !isStarred(email));
|
||||
reportFailure(toggleStar(id, !isStarred(email)));
|
||||
break;
|
||||
case 'pin':
|
||||
void togglePin(id, !isPinned(email));
|
||||
reportFailure(togglePin(id, !isPinned(email)));
|
||||
break;
|
||||
case 'move':
|
||||
setPendingMoveId(id);
|
||||
@@ -788,7 +795,11 @@ export default function EmailListScreen({
|
||||
const handleBulkDelete = async () => {
|
||||
const trash = findTrashMailbox(scopedMailboxes);
|
||||
if (!trash || !currentMailboxId) {
|
||||
clearSelection();
|
||||
// Said, as the swipe says it, instead of dropping the selection.
|
||||
Alert.alert(
|
||||
t('email_list.error', 'Error'),
|
||||
t('email_list.no_trash_folder', 'Could not find a Trash folder on the server. Please check your mailbox configuration.'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const ids = selectedMessageIds;
|
||||
@@ -824,11 +835,11 @@ export default function EmailListScreen({
|
||||
const ids = selectedMessageIds;
|
||||
setBatchMoveOpen(false);
|
||||
clearSelection();
|
||||
void moveEmailsToMailbox(ids, toId);
|
||||
reportFailure(moveEmailsToMailbox(ids, toId));
|
||||
};
|
||||
|
||||
const handleBatchTagToggle = (token: string, on: boolean) => {
|
||||
void setKeywordForEmails(selectedMessageIds, token, on);
|
||||
reportFailure(setKeywordForEmails(selectedMessageIds, token, on));
|
||||
};
|
||||
|
||||
// Local input state for uninterrupted typing. The search runs on submit,
|
||||
@@ -1040,7 +1051,7 @@ export default function EmailListScreen({
|
||||
{t('email_list.batch_actions.selected_messages', '{count, plural, one {1 email} other {# emails}} selected', { count: selectedIds.size })}
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() => { void handleBulkStar(); }}
|
||||
onPress={() => reportFailure(handleBulkStar())}
|
||||
style={styles.headerButton}
|
||||
hitSlop={6}
|
||||
accessibilityRole="button"
|
||||
@@ -1053,7 +1064,7 @@ export default function EmailListScreen({
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => { void handleBulkMarkReadToggle(); }}
|
||||
onPress={() => reportFailure(handleBulkMarkReadToggle())}
|
||||
style={styles.headerButton}
|
||||
hitSlop={6}
|
||||
accessibilityRole="button"
|
||||
@@ -1087,7 +1098,7 @@ export default function EmailListScreen({
|
||||
</Pressable>
|
||||
{canSpamSelection && (
|
||||
<Pressable
|
||||
onPress={() => { void handleBulkSpam(); }}
|
||||
onPress={() => reportFailure(handleBulkSpam())}
|
||||
style={styles.headerButton}
|
||||
hitSlop={6}
|
||||
accessibilityRole="button"
|
||||
@@ -1102,7 +1113,7 @@ export default function EmailListScreen({
|
||||
)}
|
||||
{canArchiveSelection && (
|
||||
<Pressable
|
||||
onPress={() => { void handleBulkArchive(); }}
|
||||
onPress={() => reportFailure(handleBulkArchive())}
|
||||
style={styles.headerButton}
|
||||
hitSlop={6}
|
||||
accessibilityRole="button"
|
||||
@@ -1112,7 +1123,7 @@ export default function EmailListScreen({
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={() => { void handleBulkDelete(); }}
|
||||
onPress={() => reportFailure(handleBulkDelete())}
|
||||
style={styles.headerButton}
|
||||
hitSlop={6}
|
||||
accessibilityRole="button"
|
||||
@@ -1772,8 +1783,8 @@ export default function EmailListScreen({
|
||||
setPendingMoveId(null);
|
||||
if (id && currentMailboxId && toId !== currentMailboxId) {
|
||||
const ids = idsForRow(id);
|
||||
if (ids.length > 1) void moveEmailsToMailbox(ids, toId);
|
||||
else void moveToMailboxAction(id, currentMailboxId, toId);
|
||||
if (ids.length > 1) reportFailure(moveEmailsToMailbox(ids, toId));
|
||||
else reportFailure(moveToMailboxAction(id, currentMailboxId, toId));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user