mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: keep the composer open when its draft can't be saved
"Save draft" returned early without saving when the identities hadn't loaded (offline when the composer opened) or the account had no Drafts folder, and the close dialog then closed the composer as if the save had worked, dropping the text. The close dialog now offers only Cancel and Discard in that case and says why the draft can't be saved; the save path refuses to close too. The close guard moves from a bare `beforeRemove` listener to `usePreventRemove`, which also stops the iOS modal swipe-down that the listener could not prevent.
This commit is contained in:
@@ -116,6 +116,8 @@
|
|||||||
"recipient_move_to": "Move to {field}",
|
"recipient_move_to": "Move to {field}",
|
||||||
"recipient_remove": "Remove",
|
"recipient_remove": "Remove",
|
||||||
"save_draft": "Save Draft",
|
"save_draft": "Save Draft",
|
||||||
|
"save_failed_no_drafts": "There is no Drafts folder to save this draft to.",
|
||||||
|
"save_failed_no_identity": "Your sender identities could not be loaded, so this draft cannot be saved. Check your connection, or copy your text before closing.",
|
||||||
"schedule_custom": "Pick date & time…",
|
"schedule_custom": "Pick date & time…",
|
||||||
"schedule_in_1h": "In 1 hour",
|
"schedule_in_1h": "In 1 hour",
|
||||||
"schedule_in_3h": "In 3 hours",
|
"schedule_in_3h": "In 3 hours",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Keyboard, Dimensions, Platform, ActivityIndicator, Alert, Modal, Switch,
|
Keyboard, Dimensions, Platform, ActivityIndicator, Alert, Modal, Switch,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { usePreventRemove } from '@react-navigation/native';
|
||||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
import {
|
import {
|
||||||
X, Send, Paperclip, ChevronDown, Bold, Italic, Underline, Strikethrough,
|
X, Send, Paperclip, ChevronDown, Bold, Italic, Underline, Strikethrough,
|
||||||
@@ -1279,11 +1280,25 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
|
|
||||||
// ── Close guard ──────────────────────────────────────────────────────
|
// ── Close guard ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Why a draft can't be saved right now: the identities didn't load (e.g.
|
||||||
|
// offline when the composer opened) or the account has no Drafts folder.
|
||||||
|
const draftUnsavableReason = !primaryIdentity
|
||||||
|
? t('email_composer.save_failed_no_identity', 'Your sender identities could not be loaded, so this draft cannot be saved. Check your connection, or copy your text before closing.')
|
||||||
|
: !draftsMailbox
|
||||||
|
? t('email_composer.save_failed_no_drafts', 'There is no Drafts folder to save this draft to.')
|
||||||
|
: null;
|
||||||
|
|
||||||
const saveAndClose = async (proceed: () => void) => {
|
const saveAndClose = async (proceed: () => void) => {
|
||||||
if (saveTimerRef.current) {
|
if (saveTimerRef.current) {
|
||||||
clearTimeout(saveTimerRef.current);
|
clearTimeout(saveTimerRef.current);
|
||||||
saveTimerRef.current = null;
|
saveTimerRef.current = null;
|
||||||
}
|
}
|
||||||
|
// Closing after a save that couldn't run would drop the text as if it
|
||||||
|
// had been saved: stay open and say why.
|
||||||
|
if (draftUnsavableReason) {
|
||||||
|
Alert.alert(t('email_composer.save_failed', 'Failed to save'), draftUnsavableReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSavingDraft(true);
|
setSavingDraft(true);
|
||||||
try {
|
try {
|
||||||
await saveDraft({ live: true });
|
await saveDraft({ live: true });
|
||||||
@@ -1322,6 +1337,17 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showCloseDialog = (proceed: () => void) => {
|
const showCloseDialog = (proceed: () => void) => {
|
||||||
|
if (draftUnsavableReason) {
|
||||||
|
Alert.alert(
|
||||||
|
t('email_composer.discard_draft_title', 'Discard draft?'),
|
||||||
|
draftUnsavableReason,
|
||||||
|
[
|
||||||
|
{ text: t('email_composer.cancel', 'Cancel'), style: 'cancel' },
|
||||||
|
{ text: t('email_composer.discard', 'Discard'), style: 'destructive', onPress: () => discardAndClose(proceed) },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t('email_composer.close_draft_title', 'Save or discard draft?'),
|
t('email_composer.close_draft_title', 'Save or discard draft?'),
|
||||||
t('email_composer.close_draft_message', 'You have unsaved changes. Would you like to save this as a draft or discard it?'),
|
t('email_composer.close_draft_message', 'You have unsaved changes. Would you like to save this as a draft or discard it?'),
|
||||||
@@ -1332,19 +1358,18 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const showCloseDialogRef = React.useRef(showCloseDialog);
|
|
||||||
showCloseDialogRef.current = showCloseDialog;
|
|
||||||
|
|
||||||
// The OS back gesture / hardware back goes through the same guard as the
|
// The OS back gesture / hardware back goes through the same guard as the
|
||||||
// header X.
|
// header X. `usePreventRemove` (not a bare `beforeRemove` listener) also
|
||||||
React.useEffect(() => {
|
// blocks the iOS modal swipe-down, which is dismissed natively otherwise.
|
||||||
return navigation.addListener('beforeRemove', (e) => {
|
// Re-dispatching the intercepted action doesn't come back here.
|
||||||
if (allowLeaveRef.current) return;
|
usePreventRemove(isDirty || needsSave, ({ data }) => {
|
||||||
if (!latestRef.current.isDirty && !latestRef.current.needsSave) return;
|
if (allowLeaveRef.current) {
|
||||||
e.preventDefault();
|
navigation.dispatch(data.action);
|
||||||
showCloseDialogRef.current(() => navigation.dispatch(e.data.action));
|
return;
|
||||||
});
|
}
|
||||||
}, [navigation]);
|
showCloseDialog(() => navigation.dispatch(data.action));
|
||||||
|
});
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
|
|||||||
Reference in New Issue
Block a user