From 4583de009571aaa674e7d161d31ec640860787e1 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 18 Aug 2026 12:13:51 +0200 Subject: fix(delete): repair seven defects in the move-to-trash path Item 103's implementation was committed unreviewed and never hand-tested. Reviewing it, and then hand-testing it against real mail, found seven defects. Six of them lose or corrupt state and none was caught by the suite, which was green throughout. **Undo pushed a command instead of consuming one.** onMessagesMoved() pushed a MoveCommand for every confirmed move, including the move an undo had just made, so undoText went "Delete", "Undo Delete", "Undo Undo Delete". A second press of undo re-deleted the message the first had rescued. PendingMove carries a fromUndo flag, which has to survive the queued round trip and so cannot be a window-wide "am I undoing" flag. **Held moves were invisible to the quit guard.** pendingEditCount() summed the held tag edits and not the held moves, so a Delete pressed during a sync left the count at zero: the indicator stayed hidden and closeEvent()'s guard never fired, discarding the move on quit with no prompt. That is item 106's data loss with a worse shape, because a dropped move leaves the file in the folder the user asked it out of. **Two moves to one folder dropped the second's tags.** m_pendingMoves was keyed on the destination, so two Deletes in one account before the first confirmation both named `acct/Trash` and the second insert overwrote the first. That file reached the trash carrying neither `deleted` nor `deleted-from:`, unrestorable and invisible to a `tag:deleted` query. It is a FIFO now: the worker moves one batch at a time and emits in request order, so position alone matches a confirmation to its request. **Second Delete left the origin tag behind.** The restore passed the origin PLACEHOLDER in its removal list, and onMessagesMoved() resolves that from the folder the worker reports, which on a restore is the trash. It asked to remove `deleted-from:Trash`, a tag never written, while the real `deleted-from:inbox` was never named. A restore does not need the placeholder: it already read the origin to decide where to send the file. originTagFor() is now the one derivation both sides use. **Ctrl+Z left it behind too**, for a different reason: MoveCommand was constructed with the unresolved pending.add. The command carries the resolved tags now, and is pushed per origin group rather than once per batch, because the placeholder resolves to a different tag per origin. **A thread root re-deleted itself.** everySelectedRowHasTag() asked a thread row about its THREAD's tags, which notmuch gives as a union. Delete the root of a three-message thread and the replies are untouched, so the union carries no `deleted` and a second press ran Delete again: the message moved trash-to-trash and came out with `deleted`, `deleted-from:inbox` AND `deleted-from:Trash`, with no way back. The union was a documented approximation, called bounded because the worst case for a TAG toggle was re-applying a tag the message already had. A MOVE re-applies the move. Resolved through messageById(), NOT through ThreadSummary::firstMessageTags, which is the value the query delivered and is never refreshed by an optimistic update: after a delete the node reads `deleted` while the summary still reads `unread`. **Delete thread never moved anything.** It was left calling tagSelected() when Delete became a move, so a whole conversation sat in the inbox wearing a `deleted` chip. It moves every message now, each with its own origin, so a thread spanning folders reassembles on restore. A reply row resolves to its own thread through selectedThreadIds(): scopeFor() reports a reply under messageIds and leaves threadIds empty, which made a thread action on a reply row do nothing at all. **And the root card did not repaint** until it was clicked, while its replies did. sendMove() had no optimistic update at all, so nothing moved until the worker answered; and applyMessageTagChange() deliberately leaves a multi-message thread's SUMMARY alone, which is correct for a one-message edit and wrong for a thread-scoped one. The replies have nodes and repainted; the root card reads the summary. The thread paths repaint synchronously with applyTagChange() before the worker is asked, which also keeps the toggle's direction readable for the next press. Every fix carries a test and every test was mutation-checked. Three false greens were found while writing them and are recorded at their assertions: a disjunction that emptied on the wrong term, a QTRY_VERIFY(rowCount() == 0) satisfied by the interval before the worker answers, and a query issued before the confirming write had landed. Absence is asked of notmuch directly through a new notmuchCount() helper for that reason. Two bare-window tests moved off assertions about synchronous pending writes onto the model, since the thread actions now round-trip through the worker. Co-Authored-By: Claude Opus 5 --- translations/qtmaildir_it_IT.ts | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) (limited to 'translations') diff --git a/translations/qtmaildir_it_IT.ts b/translations/qtmaildir_it_IT.ts index 89886d0..cc854b2 100644 --- a/translations/qtmaildir_it_IT.ts +++ b/translations/qtmaildir_it_IT.ts @@ -132,20 +132,6 @@ Unsynced changes Modifiche non sincronizzate - - %n tag change(s) have not been synced, and no sync command is configured. Quit anyway? - - %n modifica alle etichette non è stata sincronizzata e non è configurato alcun comando di sincronizzazione. Uscire comunque? - %n modifiche alle etichette non sono state sincronizzate e non è configurato alcun comando di sincronizzazione. Uscire comunque? - - - - %n tag change(s) have not been synced. - - %n modifica alle etichette non è stata sincronizzata. - %n modifiche alle etichette non sono state sincronizzate. - - Sync before quitting? Sincronizzare prima di uscire? @@ -262,6 +248,10 @@ Add or remove the deleted tag Aggiunge o rimuove l'etichetta deleted + + Changes made here that a sync has not yet carried to the mail store. An external notmuch run can clear them without this count noticing. + Modifiche fatte qui che nessuna sincronizzazione ha ancora trasferito all'archivio di posta. Un'esecuzione esterna di notmuch può azzerarle senza che questo conteggio se ne accorga. + %n message(s) could not be deleted: no trash folder is configured for their account. @@ -277,6 +267,20 @@ Delete Elimina + + %n change(s) have not been synced, and no sync command is configured. Quit anyway? + + %n modifica non è stata sincronizzata e non è configurato alcun comando di sincronizzazione. Uscire comunque? + %n modifiche non sono state sincronizzate e non è configurato alcun comando di sincronizzazione. Uscire comunque? + + + + %n change(s) have not been synced. + + %n modifica non è stata sincronizzata. + %n modifiche non sono state sincronizzate. + + Mark &spam Segna come &spam @@ -361,10 +365,6 @@ Undelete thread Ripristina conversazione - - Delete thread - Elimina conversazione - Mark thread as &spam Segna conversazione come &spam @@ -850,10 +850,6 @@ %n modifiche non sincronizzate - - Tag changes made here that a sync has not yet carried to the mail store. An external notmuch run can clear them without this count noticing. - Modifiche alle etichette fatte qui che nessuna sincronizzazione ha ancora trasferito all'archivio di posta. Un'esecuzione esterna di notmuch può azzerarle senza che questo conteggio se ne accorga. - &Whole thread &Intera conversazione -- cgit v1.2.3