aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md106
1 files changed, 62 insertions, 44 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 69aac44..fb659ec 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
@@ -242,8 +242,10 @@ taking that too literally.
| 166 | Mail you send to your own other account loses `inbox` | defect | S | **done 2026-08-25**, unreleased. `sent_only()` keeps a message only when EVERY file is inside a sent folder, which is what the carve-out's docstring already claimed. No query can express it, measured; the root comes from `database.mail_root`, with a split-index fixture the ordinary layout cannot provide. Verified read-only against the live index: 780 of 807 still stripped, 27 spared, no arrival affected |
| 167 | No way to tell one build of an unreleased version from another | enhancement | XS | **done 2026-08-25**, unreleased. The user chose a counter over a git description: `QTMAILDIR_BUILD_NUMBER`, a cmake option ON by default, increments a counter in the BUILD directory on every build and writes `buildnumber.h`. `QTMAILDIR_VERSION_DISPLAY` carries it; `QTMAILDIR_VERSION` stays clean and is what the window title, `applicationVersion` and the release procedure use |
| 168 | Delete is offered on mail already in the trash, and does nothing | defect | S | **done 2026-08-25**, unreleased. Delete is hidden when every selected row is already in its account's trash, Restore when none is, both keyed on the PATH rather than the `deleted` tag. Delete also drops `unread` now, in the same TagChange so one undo returns the folder and the tag together |
-| 169 | A card shows the account only as a bar, with no fade and no avatar | presentation | M | open, 2026-08-26, from the notes. The accent bar exists (`CardLayout::accentRect`, `CardDelegate::accentLineColour()`); the gradient fade and the sender avatar do not. The avatar's initials source is decided, the vCard half is blocked on item 72 |
+| 169 | A card shows the account only as a bar, with no fade and no avatar | presentation | M | **done** 2026-08-26, unreleased, on `card-avatars`, merged fast-forward. Both halves: a `QLinearGradient` from the account colour to the pane's base across the card, and a squircle avatar with initials, given a rect in `CardLayout` so the geometry is asserted without a painter. **Hand-testing found four defects**, all fixed in 9ae43f9: `Avatar::initialsFor()` normalises the display name first (drops the angle-addr, takes the first comma-separated author, unwraps quotes, treats a bare address as no name, requires a word to carry a letter or digit); the two-tone gradient axis spans the DIAMETER rather than a radius, which was letting one hue fill the whole face; the account fade runs right to left, anchored opaque at the card's right edge; and a flat view hashes `ThreadSummary::firstMessageRecipient` rather than the user's own address. The vCard half stays blocked on item 72 |
| 170 | A row that stops matching the view only leaves it on the Delete path | defect | S | open, 2026-08-26, from the notes, **cause found the same day and the premise is NOT stale**. The optimistic REPAINT is universal; the optimistic MEMBERSHIP is not. `removeThreadsWithoutTag()` has exactly one caller, on the move path, so marking a message read in the Unread view repaints the row and leaves it in a list it no longer belongs to |
+| 171 | A forwarded HTML message reaches the recipient as plain text | defect | S | open, 2026-08-27, from the notes. `ComposeContextBuilder::quoteBody()` reads `ParsedMessage::plainBody` only, so the original's `htmlBody` is dropped whatever the composer's own Send-as-HTML state is. Needs a DECISION on what a forward carries before any code, see the entry |
+| 172 | A draft this application writes is tagged `unread` | defect | XS | **done** 2026-08-27, unreleased. `DraftStore::write()` was called with `"D"`, and `maildir.synchronize_flags` makes notmuch tag anything without `S` as `unread`. Self-healing on the next sync of that folder, which is what made it look intermittent |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -1261,49 +1263,6 @@ id and a draft of a reply carries both.
replaced correctly now, so the fork this would have mitigated no longer
happens by that route.
-## 169. A card shows the account only as a bar, with no fade and no avatar
-
-**Observed (user, from the notes):** "the left border of a card expresses the
-account the mail belongs to. the background color of the card should fade left
-to right from the account color to the current background color we are using (or
-to transparent to work both in light and dark themes). On the left we should
-leave room for an account avatar (a squircle), for now it could be extracted
-from the sender name "From: john doe" becomes "JD" in the avatar. As soon as we
-include khard (or some other vcard provider/manager) we will switch to images if
-the corresponding vCard has one."
-
-**Cause (verified in the code):** not a defect. Half of it shipped. The account
-colour is drawn as a solid bar down the left edge, `CardLayout::accentRect`
-placed by `CardLayout`, filled by `CardDelegate::paint()` with
-`CardDelegate::accentLineColour()`. There is no gradient anywhere on a card, and
-nothing draws an avatar: `CardLayout` reserves no rect for one, so the geometry
-would have to grow before the painting could.
-
-**Approach.** Two separable pieces, and the avatar is the one that changes the
-layout.
-
-- The fade is a `QLinearGradient` fill over the card rect, from the accent
- colour to the pane's background. `accentLineColour()` already records why
- blending toward the background is wrong for a CHIP; a card's background is
- exactly where such a blend belongs, so the constraint does not carry over.
- Both themes come free if the far stop is the palette's own base rather than
- a literal.
-- The avatar needs a rect in `CardLayout`, which is where it becomes testable
- without a painter, and it shifts `contentLeft` for every card. The initials
- come from the display name already carried on the summary; a sender with no
- display name (an address only) needs an answer before this is built.
-
-**Constraints.**
-
-- The vCard half is blocked on item 72, which is itself unspecified. Build the
- initials only; do not design the image path in advance.
-- A gradient behind the text has to keep the text readable at the left edge in
- both themes, which is the same failure mode `accentLineColour()` guards
- against on a dark palette.
-- This is a looks question, so it is settled by the user looking at it rather
- than by a test: assert the geometry in `CardLayout`, and hand the appearance
- over per `tests-only-for-measurable-things`.
-
## 170. A row that stops matching the view only leaves it on the Delete path
**Observed (user, from the notes):** "should we refactor the list UI to be
@@ -1353,3 +1312,62 @@ every tag write passes. Move it, or call it from both send paths.
thread the query never returned.
- Undo goes back through the same funnel, so a removal must not make an undone
mark-read invisible in the view it was undone in.
+
+## 171. A forwarded HTML message reaches the recipient as plain text
+
+**Observed (user, from the notes):** "forwarding an html message doesn't
+maintain the html formatting of the original message. #bug"
+
+**Cause (verified in the code, 2026-08-27).** The forward path builds its body
+through `ComposeContextBuilder::quoteBody()` (`src/composecontext.cpp`), which
+reads `message.plainBody` and nothing else. `MimeParser` parses both halves and
+`ParsedMessage` carries `htmlBody` beside `plainBody` (`src/mimeparser.h:150`),
+so the HTML is available and simply never asked for.
+
+Two consequences follow, and they are not the same severity:
+
+- An original with both parts forwards its text/plain alternative, losing the
+ sender's formatting. Recoverable-looking, since the words survive.
+- An original with an HTML part ONLY has an empty `plainBody`, so the forward
+ carries the attribution line and an empty quote. The message's content is
+ gone, and nothing says so.
+
+`MainWindow::composeReply()` already treats the two kinds differently for the
+composer's own HTML state: `context.seedHtml` is the CONFIG's `sendHtml` for a
+forward and `original.hasHtml()` for a reply, on the stated reasoning that an
+HTML part is a fact about the sender's software. That reasoning is sound for
+how the user WRITES and does not decide what the forward CARRIES, which is the
+question here.
+
+**Approach.** The decision comes first; this is not a changed call site.
+
+A forward is a different act from a reply: the point is to hand somebody else
+what arrived, and quoting is the wrong shape for it. Three candidates, in
+increasing fidelity:
+
+- Render `htmlBody` down to text when `plainBody` is empty, so nothing is
+ silently lost. The smallest fix, and it does not answer the note: formatting
+ is still gone.
+- Carry the original as a `message/rfc822` part, which is what item 130 already
+ describes and what GMime builds natively. Perfect fidelity, and every
+ attachment comes with it, but the recipient sees an attached message rather
+ than a body.
+- Build the forward as `multipart/alternative` with the original's HTML nested
+ in the HTML half, which is what Thunderbird's inline forward does.
+
+**Constraints.**
+
+- **The HTML is input from a stranger and the composer is not the message
+ pane.** The pane's protections (off-the-record profile, JavaScript off, the
+ interceptor blocking every request) are `MessageView`'s, not
+ `ComposeWindow`'s. Any route that puts the original's markup into an outgoing
+ message must decide what it strips, and remote references in particular:
+ forwarding a tracking pixel forwards the tracking to the new recipient.
+- Item 130 overlaps and may subsume this. Decide the two together rather than
+ building `message/rfc822` twice.
+- The markdown body is the composer's source of truth, and markdown has no
+ syntax for arbitrary HTML the user can then edit. A route that keeps the
+ original's markup has to keep it OUTSIDE the editable buffer, which is the
+ same nesting problem item 129 carries.
+- `quoteBody()` is shared with Reply. A change there reaches both; the
+ behaviour asked for is the forward's alone.