From 39a0cce152dda3b3d828c6145f02c3db9bbd7449 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 26 Aug 2026 15:33:00 +0200 Subject: feat: choose an avatar fill and derive its colour --- src/avatar.cpp | 22 ++++++++++++++++++++++ tests/test_avatar.cpp | 28 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/avatar.cpp b/src/avatar.cpp index 4cedae2..0e2b25f 100644 --- a/src/avatar.cpp +++ b/src/avatar.cpp @@ -75,4 +75,26 @@ QString initialsFor(const QString &displayName, const QString &address, return QStringLiteral("??"); } +Fill fillFor(const QString &displayName, bool isBusinessSender) +{ + // The list first: it is the user's explicit override and must beat the + // heuristic, or a listed sender could never be pinned. + if (isBusinessSender) + return Fill::TwoTone; + return displayName.trimmed().isEmpty() ? Fill::TwoTone : Fill::Identicon; +} + +QColor colourFor(const QString &address) +{ + // The same construction TagColors::colourFor() uses for a tag with nothing + // configured: hashed so it is stable, at a fixed saturation and lightness + // so it cannot come out neon and cannot lose its contrast with the + // initials. The lightness differs from that function's deliberately: a + // chip carries dark text, a squircle carries white. + const QByteArray digest = + QCryptographicHash::hash(address.toUtf8(), QCryptographicHash::Md5); + const int hue = static_cast(digest.at(0)) * 360 / 256; + return QColor::fromHsl(hue, 110, 95); +} + } // namespace Avatar diff --git a/tests/test_avatar.cpp b/tests/test_avatar.cpp index 225b4f1..b53dfc1 100644 --- a/tests/test_avatar.cpp +++ b/tests/test_avatar.cpp @@ -30,6 +30,9 @@ private slots: void bareAddressTakesLocalAndDomain(); void nothingUsableFallsBackToTheAccountLabel(); void initialsAreAlwaysTwoLetters(); + void aDisplayNameMeansAPerson(); + void theListOverridesADisplayName(); + void aColourIsStablePerAddress(); }; void TestAvatar::twoWordNameTakesOneLetterFromEach() @@ -91,5 +94,30 @@ void TestAvatar::initialsAreAlwaysTwoLetters() } } +void TestAvatar::aDisplayNameMeansAPerson() +{ + // The case the user asked for by name: a corporate address that presents + // itself as a person reads as a person. + QCOMPARE(Avatar::fillFor(QStringLiteral("Ian Farrell"), false), + Avatar::Fill::Identicon); + QCOMPARE(Avatar::fillFor(QString(), false), Avatar::Fill::TwoTone); +} + +void TestAvatar::theListOverridesADisplayName() +{ + // A listed address stays a business even when it sets a friendly name. + QCOMPARE(Avatar::fillFor(QStringLiteral("Cofidis"), true), + Avatar::Fill::TwoTone); +} + +void TestAvatar::aColourIsStablePerAddress() +{ + const QColor first = Avatar::colourFor(QStringLiteral("a@example.org")); + const QColor again = Avatar::colourFor(QStringLiteral("a@example.org")); + QCOMPARE(first, again); + QVERIFY(first.isValid()); + QVERIFY(Avatar::colourFor(QStringLiteral("b@example.org")) != first); +} + QTEST_MAIN(TestAvatar) #include "test_avatar.moc" -- cgit v1.2.3