diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-03 16:43:33 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-03 16:43:33 +0200 |
| commit | 7f505624b1f385a89c9ff32e15f4d4e68595e5b8 (patch) | |
| tree | c06185ba0e51aebd61c2a87b4ed6d19c8330db48 | |
| parent | 60b4346a428550a8a97170c245e8c171606586b1 (diff) | |
| download | qtmaildir-7f505624b1f385a89c9ff32e15f4d4e68595e5b8.tar.gz qtmaildir-7f505624b1f385a89c9ff32e15f4d4e68595e5b8.zip | |
docs: add backlog items 15 to 17 from usage notes
Attachments turned out to be a real gap rather than a discoverability
one. MessageView creates the attachment bar and adds it to the pane, but
nothing ever populates it: m_attachmentBar appears nowhere else in the
codebase, so it has never displayed anything. MimeParser already
extracts attachments and Attachment::saveTo() already carries the
path-traversal guard, so the backend needs calling, not writing.
Item 16 makes delete a toggle, with the open question of what a mixed
selection should do. Item 17 needs a new worker call, since there is no
way to list tags today.
Also records that this document's numbering and the user's own notes
have diverged, so a reference to "item 13" stays resolvable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 02bf7c4..0e0a22f 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -7,6 +7,12 @@ release is a separate decision. Source: usage notes taken while running the app, 2026-08-03. +**Numbers here are this document's own.** The user's own notes were numbered +independently and the two sequences drifted apart once items were split: what +those notes called 12 is item 13 here, and item 14 here (the tag column) was +never in them at all. Items 15 to 17 come from a later pass over the same +notes. Cite these numbers, not the notes', and do not renumber to reconcile. + Numbering is stable. New items append with the next free number and never renumber, so a note referring to "item 7" keeps meaning the same thing. An item that is dropped stays in the table marked `dropped` with a one-line reason. @@ -47,6 +53,9 @@ taking that too literally. | 11 | Icon, `.desktop` file, SlackBuild | packaging | M | **partly done**: icon and `.desktop` landed, SlackBuild open | | 13 | No visual feedback that an action stuck | feedback | S | **done** | | 14 | Tag column unreadable, tags need another home | presentation | M | **done** | +| 15 | Attachments are parsed but unreachable from the UI | information | M | open | +| 16 | Delete on an already-deleted thread should undelete | behavior | S | open | +| 17 | No completion for tags in the query bar | workflow | M | open | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -499,6 +508,100 @@ hierarchical tag, and each fell through to its prefix colour. Fixed by reading **Deferred:** clicking a chip to search that tag. Display only for now. +## 15. Attachments are parsed but unreachable from the UI + +**Observed, 2026-08-03, with a screenshot.** A thread known to carry +attachments shows the `attachment` chip and a body that refers to them, but +there is no way to download or open one. The user also has no way to tell a +message has an attachment before opening it. + +**Cause: the attachment bar is an empty placeholder.** +`MessageView` creates `m_attachmentBar` and gives it a layout +(`src/messageview.cpp:130`), adds it to the pane (`:142`), and then **nothing +ever puts anything in it**. `m_attachmentBar` appears nowhere else in the +codebase, and neither `render()` nor `showThread()` reads +`ParsedMessage::attachments`. The bar has never displayed an attachment. + +This is a gap in the UI only. The backend is complete and already hardened: +`MimeParser` fills `attachments`, and `Attachment` has `safeFilename()`, +`saveTo()` and `isPathInsideDirectory()` with the path-traversal guard +`CLAUDE.md` describes. None of that work needs redoing; it needs calling. + +**Approach, two independent pieces.** + +- *Populate the bar.* For each attachment on each rendered message, one button + showing the safe filename and the size. Clicking saves, through a + `QFileDialog` for the target directory, then `Attachment::saveTo()`. Report + the written path in the status bar, since a silent save is the same UX + failure as item 13. +- *A paperclip column in the thread list*, so an attachment is visible before + opening. `ThreadSummary` already carries the thread's tags and notmuch + applies `attachment`, so the column can be driven from the existing tag data + with **no new worker query**. Keep it narrow: an icon column, no text. + +**Constraints.** + +- **Filenames are untrusted.** Display `safeFilename()`, never the raw + `filename`, and never interpolate either into rich text without escaping. + The save path must go through `saveTo()`, which is where the boundary check + lives. +- **Do not add "open in default application" in the same change.** That means + handing a file from a stranger to `xdg-open`, which is a materially + different security decision from writing it to a directory the user picked. + If it is wanted, it is its own item with its own reasoning. +- A thread renders as one document, so the bar must make clear which message + an attachment belongs to once a thread has several. Grouping by message, or + a per-message row inside the HTML, are both plausible; decide when building. + +**Verification:** a message with a known attachment saves a byte-identical +file. A message with an attachment named `../../etc/passwd` writes inside the +chosen directory under a sanitised name and nowhere else. + +## 16. Delete on an already-deleted thread should undelete + +**Observed:** hitting Delete twice on the same message is a natural way to +express "no, put it back", but the second press does nothing visible because +adding a tag that is already present is a no-op. + +**Approach:** make `delete` a toggle, the way `toggle_unread` already is. +Deleting a thread that already carries `deleted` removes it instead. + +**Constraints.** + +- **A multi-row selection must not split.** If some selected threads are + deleted and others are not, toggling each independently leaves the selection + in two states from one keystroke, which is worse than either outcome. Decide + one direction for the whole selection: the natural rule is "if every + selected thread is already deleted, undelete them all; otherwise delete them + all." +- Undo already covers the mistake case, so this is a convenience, not a safety + fix. It must not grow a confirmation dialog. +- The same question applies to `spam` and `flag`. Do not change those in this + item; note whether the answer generalises once `delete` is built. + +## 17. No completion for tags in the query bar + +**Observed:** typing a query means remembering the exact tag name, including +hierarchy (`shopping/amazon`). + +**Approach:** a `QCompleter` on the query bar, offering tag names after +`tag:`. + +**Cause of the work being larger than it looks:** there is **no way to list +tags today**. `NotmuchWorker` has no all-tags call, so this needs a new +worker request and result signal, following the existing generation-counter +pattern. `notmuch_database_get_all_tags()` is the underlying call. + +**Constraints.** + +- The tag list must be fetched on the worker thread like everything else. No + `notmuch_*` pointer crosses the thread boundary; the result is a + `QStringList`. +- Refresh after a sync, since a sync can introduce tags. Do not refetch per + keystroke. +- Completion should trigger on the `tag:` prefix specifically rather than on + every word, or it will offer tag names where a `from:` value belongs. + --- ## Deferred, unsized, or split out |
