aboutsummaryrefslogtreecommitdiffstats
path: root/src/avatar.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-26 17:50:18 +0200
committerDanilo M. <danix@danix.xyz>2026-08-26 17:50:18 +0200
commit9ae43f94f5d822efe582e79b47b2f5407161a38b (patch)
treed9b956fb6bb0034da6ca8c923017235b89bb5926 /src/avatar.cpp
parent8dec28ff4f1176b485d9de722756c677be3a4f1c (diff)
downloadqtmaildir-9ae43f94f5d822efe582e79b47b2f5407161a38b.tar.gz
qtmaildir-9ae43f94f5d822efe582e79b47b2f5407161a38b.zip
fix: correct the avatar initials, the two-tone fill and the fade
Hand-testing item 169 found four defects, three of them visible on every card. The initials were taken from whatever the card's first line held, which is the raw From header on a reply row and notmuch's comma-joined author summary on a thread row. A naive space split therefore gave `T<` for `tsujan <notifications@github.com>` and one letter each from two different people for `Standreas, tsujan`, and a separator counted as a word, so `INE - Expert IT Training` drew `I-`. Avatar::initialsFor() now normalises first: the angle-addr and any quoting go, a comma takes the first entry unless the name is quoted, a bare address is not a name, and a word has to carry a letter or a digit. Avatar::fillFor() uses the same normalisation, so an address in the name's place no longer reads as a person. The two-tone fill built its gradient axis as a radius from the centre, so the 0.5 colour stop landed on the squircle's edge and one hue filled almost the whole face. The axis spans the diameter now. The fade ran left to right, which put its hard stop at 60% of the card and read as a slab rather than a wash. It runs right to left: opaque at the card's right edge, where the only hard stop is the card's own boundary, and gone before it reaches the accent bar that already states the account. And the flat views hashed the user's own address on every row, so every Sent and Drafts card shared one pattern. ThreadSummary::firstMessageRecipient rides the recipient fold, which already parses the To header, and SenderAddressRole prefers it, falling back to the sender when there is no usable To. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P88Q3MCSCSQxKDy7pmXh9F
Diffstat (limited to 'src/avatar.cpp')
-rw-r--r--src/avatar.cpp69
1 files changed, 64 insertions, 5 deletions
diff --git a/src/avatar.cpp b/src/avatar.cpp
index 7aaa6eb..9b292a2 100644
--- a/src/avatar.cpp
+++ b/src/avatar.cpp
@@ -26,6 +26,42 @@
namespace {
+/// The bare display name from whatever the card's first line holds.
+///
+/// That string is the RAW header for a reply row (`Name <addr>`) and a
+/// comma-joined author summary for a thread row, so a naive space split gives
+/// `T<` for one and one letter from each of two different people for the
+/// other. Take the first entry, drop the angle-addr and any quoting, and
+/// report nothing when what remains is itself an address: the caller's address
+/// branch reads that far better than the local part would.
+QString displayNameOf(const QString &raw)
+{
+ QString name = raw.trimmed();
+
+ // `Name <addr>`: everything before the bracket is the name. When there is
+ // nothing before it, the address itself is not a name.
+ const int bracket = name.indexOf(QLatin1Char('<'));
+ if (bracket >= 0)
+ name = name.left(bracket).trimmed();
+
+ // A comma joins either two authors or a `"Rossi, Mario"` quoted name. The
+ // quotes tell them apart, so strip them only after splitting.
+ if (!name.startsWith(QLatin1Char('"'))) {
+ const int comma = name.indexOf(QLatin1Char(','));
+ if (comma >= 0)
+ name = name.left(comma).trimmed();
+ }
+ if (name.size() >= 2 && name.startsWith(QLatin1Char('"'))
+ && name.endsWith(QLatin1Char('"'))) {
+ name = name.mid(1, name.size() - 2).trimmed();
+ }
+
+ // A bare address left standing is not a name.
+ if (name.contains(QLatin1Char('@')))
+ return QString();
+ return name;
+}
+
QString twoFrom(const QString &text)
{
const QString trimmed = text.trimmed();
@@ -43,8 +79,19 @@ namespace Avatar {
QString initialsFor(const QString &displayName, const QString &address,
const QString &accountLabel)
{
- const QStringList words = displayName.split(QLatin1Char(' '),
- Qt::SkipEmptyParts);
+ // Words, and a separator is not one. `INE - Expert IT Training` split on
+ // spaces alone gave `I-`, because the dash counted as the second word.
+ QStringList words;
+ const QStringList parts = displayNameOf(displayName)
+ .split(QLatin1Char(' '), Qt::SkipEmptyParts);
+ for (const QString &part : parts) {
+ // Trim leading punctuation so `(Team)` still contributes its `T`.
+ int at = 0;
+ while (at < part.size() && !part.at(at).isLetterOrNumber())
+ ++at;
+ if (at < part.size())
+ words.append(part.mid(at));
+ }
if (words.size() >= 2) {
return (words.at(0).left(1) + words.at(1).left(1)).toUpper();
}
@@ -83,7 +130,10 @@ Fill fillFor(const QString &displayName, bool isBusinessSender)
// heuristic, or a listed sender could never be pinned.
if (isBusinessSender)
return Fill::TwoTone;
- return displayName.trimmed().isEmpty() ? Fill::TwoTone : Fill::Identicon;
+ // The same normalisation initialsFor() uses: a raw `<addr>` header or a
+ // bare address is not a display name, so it must not read as a person.
+ return displayNameOf(displayName).isEmpty() ? Fill::TwoTone
+ : Fill::Identicon;
}
QColor colourFor(const QString &address)
@@ -150,9 +200,18 @@ QPixmap pixmapFor(const QString &seed, const QString &initials, Fill fill,
// behind the letters stays large and flat, which is the whole reason
// this fill exists beside the identicon.
const int angle = static_cast<quint8>(digest.at(1)) * 360 / 256;
- QLineF axis = QLineF::fromPolar(side, angle);
+ // The axis must span the DIAMETER through the centre, not a radius
+ // from it. fromPolar() starts at the origin, so translating by half
+ // the side put p1 at the centre and the 0.5 colour stop on the
+ // squircle's edge: one hue filled almost the whole face and the fill
+ // read as flat. The diagonal, so the split still crosses the shape at
+ // any angle.
+ const qreal reach = side * 0.71;
+ QLineF axis = QLineF::fromPolar(reach, angle);
axis.translate(side / 2.0, side / 2.0);
- QLinearGradient gradient(axis.p2(), axis.p1());
+ QLineF back = QLineF::fromPolar(reach, angle + 180.0);
+ back.translate(side / 2.0, side / 2.0);
+ QLinearGradient gradient(back.p2(), axis.p2());
gradient.setColorAt(0.0, base);
gradient.setColorAt(0.499, base);
gradient.setColorAt(0.5, base.darker(135));