fix: ask before deleting an event, a task or a calendar feed

This commit is contained in:
Linus Rath
2026-10-15 02:51:54 +02:00
parent 90170ea0e7
commit 0460da51e5
4 changed files with 47 additions and 7 deletions
+1
View File
@@ -128,6 +128,7 @@
"add_placeholder": "Aufgabe hinzufügen…", "add_placeholder": "Aufgabe hinzufügen…",
"add_time": "Uhrzeit hinzufügen", "add_time": "Uhrzeit hinzufügen",
"clear_due_date": "Fälligkeitsdatum entfernen", "clear_due_date": "Fälligkeitsdatum entfernen",
"delete_confirm": "Diese Aufgabe löschen?",
"delete_error": "Aufgabe konnte nicht gelöscht werden", "delete_error": "Aufgabe konnte nicht gelöscht werden",
"not_found": "Diese Aufgabe ist nicht mehr verfügbar.", "not_found": "Diese Aufgabe ist nicht mehr verfügbar.",
"save_error": "Aufgabe konnte nicht gespeichert werden", "save_error": "Aufgabe konnte nicht gespeichert werden",
+1
View File
@@ -131,6 +131,7 @@
"add_placeholder": "Add a task…", "add_placeholder": "Add a task…",
"add_time": "Add time", "add_time": "Add time",
"clear_due_date": "Clear due date", "clear_due_date": "Clear due date",
"delete_confirm": "Delete this task?",
"delete_error": "Failed to delete task", "delete_error": "Failed to delete task",
"not_found": "This task is no longer available.", "not_found": "This task is no longer available.",
"save_error": "Failed to save task", "save_error": "Failed to save task",
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { import {
View, Text, StyleSheet, ScrollView, Pressable, Modal, Animated, Easing, TextInput, ActivityIndicator, View, Text, StyleSheet, ScrollView, Pressable, Modal, Animated, Easing, TextInput, ActivityIndicator, Alert,
} 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 { X, Plus, RefreshCw, Trash2, Rss, AlertTriangle } from 'lucide-react-native'; import { X, Plus, RefreshCw, Trash2, Rss, AlertTriangle } from 'lucide-react-native';
@@ -279,7 +279,24 @@ export function ICalSubscriptionSheet({ visible, onClose }: Props) {
)} )}
</Pressable> </Pressable>
<Pressable <Pressable
onPress={() => { void removeSubscription(sub.id); }} // Unsubscribing also deletes the calendar and its events.
onPress={() => Alert.alert(
t('calendar.subscription.unsubscribe', 'Unsubscribe'),
t('calendar.subscription.confirm_delete', 'Unsubscribe from "{name}"? The calendar and all its events will be removed.', { name: sub.name }),
[
{ text: t('common.cancel', 'Cancel'), style: 'cancel' },
{
text: t('calendar.subscription.unsubscribe', 'Unsubscribe'),
style: 'destructive',
onPress: () => {
removeSubscription(sub.id).catch((err: unknown) => Alert.alert(
t('calendar.subscription.delete_error', 'Failed to remove subscription'),
err instanceof Error ? err.message : undefined,
));
},
},
],
)}
hitSlop={6} hitSlop={6}
style={styles.subBtn} style={styles.subBtn}
accessibilityRole="button" accessibilityRole="button"
+26 -5
View File
@@ -634,15 +634,32 @@ export default function CalendarScreen() {
[t], [t],
); );
// A single event is gone for good once deleted; ask first, as the
// recurrence-scope dialog already does for a series.
const confirmDeleteEvent = React.useCallback((event: CalendarEvent) => {
Alert.alert(
t('calendar.detail.delete_confirm', 'Delete this event?'),
event.title || undefined,
[
{ text: t('common.cancel', 'Cancel'), style: 'cancel' },
{
text: t('calendar.recurrence_scope.delete', 'Delete'),
style: 'destructive',
onPress: () => { deleteEvent(event.id).catch(reportError); },
},
],
);
}, [deleteEvent, reportError, t]);
const handleDeleteFromDetail = React.useCallback((event: CalendarEvent) => { const handleDeleteFromDetail = React.useCallback((event: CalendarEvent) => {
if (isReadOnlyEvent(event)) { setDetailEvent(null); return; } if (isReadOnlyEvent(event)) { setDetailEvent(null); return; }
setDetailEvent(null); setDetailEvent(null);
if (isRecurringSeriesMember(event)) { if (isRecurringSeriesMember(event)) {
setPendingAction({ kind: 'delete', event }); setPendingAction({ kind: 'delete', event });
} else { } else {
deleteEvent(event.id).catch(reportError); confirmDeleteEvent(event);
} }
}, [deleteEvent, isReadOnlyEvent, reportError]); }, [confirmDeleteEvent, isReadOnlyEvent]);
// "This and following": end the master at the occurrence and hand back the // "This and following": end the master at the occurrence and hand back the
// master plus its untouched rules so the caller can start a new series (or // master plus its untouched rules so the caller can start a new series (or
@@ -815,9 +832,9 @@ export default function CalendarScreen() {
setPendingAction({ kind: 'delete', event }); setPendingAction({ kind: 'delete', event });
return; return;
} }
await deleteEvent(event.id).catch(reportError); confirmDeleteEvent(event);
}, },
[deleteEvent, reportError], [confirmDeleteEvent],
); );
const onRefresh = React.useCallback(() => refresh(), [refresh]); const onRefresh = React.useCallback(() => refresh(), [refresh]);
@@ -838,12 +855,16 @@ export default function CalendarScreen() {
const handleDeleteTask = React.useCallback( const handleDeleteTask = React.useCallback(
(id: string) => { (id: string) => {
deleteTask(id).catch((err: unknown) => { const run = () => deleteTask(id).catch((err: unknown) => {
Alert.alert( Alert.alert(
t('calendar.tasks.delete_error', 'Failed to delete task'), t('calendar.tasks.delete_error', 'Failed to delete task'),
err instanceof Error ? err.message : undefined, err instanceof Error ? err.message : undefined,
); );
}); });
Alert.alert(t('calendar.tasks.delete_confirm', 'Delete this task?'), undefined, [
{ text: t('common.cancel', 'Cancel'), style: 'cancel' },
{ text: t('calendar.tasks.delete', 'Delete'), style: 'destructive', onPress: () => { void run(); } },
]);
}, },
[deleteTask, t], [deleteTask, t],
); );