Deletion Toast

Toast notification for record deletion with undo support.

Appearance

  • Position: Bottom of screen
  • Contains: Trash icon, “Deleted” text, countdown timer
  • “Undo” button to restore record

Behavior

  • Appears when softDeleteRecord() is called: the Delete button of the record editor, the cancel (x) button of a running timer, or the x button of a row in the mini timer window
  • The record is removed from storage right away; the toast is only the undo window, not a deferred delete
  • The record’s image files stay on disk for the undo window (TimeTrackingService.deleteRecord(_:keepingImages: true)) so that Undo restores them with the record
  • Shows a 60-second countdown and disappears when it reaches zero (a 1-second timer in AppState calls expirePendingDeletions)
  • Swiping the toast down more than 50 pt dismisses it early (confirmDelete())
  • The kept image files are purged (purgeImages(of:)) when the deletion becomes final: on expiry, on swipe-dismiss, or when the storage folder is switched (reinitializeStorage() drops all toasts)
  • One pending entry per record: a second delete of the same record while the first is in flight or still pending is ignored

If the app quits inside the 60-second window, the record stays deleted and its image files remain as orphans. minuta doctor reports them in its image integrity check, see the CLI reference. The same happens to a file that cannot be removed during the purge.

Animation

  • Animated entrance (slide up + fade)
  • Animated exit (slide down + fade)
  • Multiple deletions stack vertically

Implementation

Components:

  • DeletionToastView: Single toast
  • DeletionToastContainer: Manages multiple toasts

Undo Flow

  1. User deletes a record (editor Delete button, running timer cancel, mini window x)
  2. Record is deleted from storage and removed from the lists; its image files are kept
  3. Toast appears with countdown
  4. User taps “Undo”
  5. The pending entry is taken out of the list and the record is saved again (trackingService.restoreRecord, not updateRecord: storage no longer holds the record, and updateRecord refuses a record it cannot find while a month file is unreadable), which puts it back in the month of its start time next to the kept image files
  6. Record is put back into todayRecords only if it starts today (the rule of getTodayRecords), and into runningTimers if it was running; toast dismissed
  7. AppState.historyRevision is bumped and ContentView reloads the History list, which dropped the record when it was deleted

Whoever removes the pending entry first decides the outcome: if expiry or a swipe already took it, Undo does nothing. If the save fails, the entry goes back into the list so Undo can be retried and the image files are still purged on expiry.