diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 17495419f..8e7b0b80c 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -220,6 +220,7 @@ target_link_libraries(neochat PUBLIC KF6::ItemModels KF6::I18nQml KirigamiApp + KirigamiAddonsComponents QuotientQt6 Login Rooms diff --git a/src/app/notificationsmanager.cpp b/src/app/notificationsmanager.cpp index 75a425f85..e350b609e 100644 --- a/src/app/notificationsmanager.cpp +++ b/src/app/notificationsmanager.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -144,16 +145,9 @@ void NotificationsManager::processNotificationJob(QPointer co body = notification["event"_L1]["content"_L1]["body"_L1].toString(); } - QImage avatar_image; - if (!sender.avatarUrl().isEmpty()) { - avatar_image = room->member(sender.id()).avatar(128, 128, {}); - } else { - avatar_image = room->avatar(128); - } postNotification(dynamic_cast(room), - sender.displayName(), + room->member(sender.id()), body, - avatar_image, notification["event"_L1].toObject()["event_id"_L1].toString(), true, pair.first); @@ -195,9 +189,8 @@ bool NotificationsManager::shouldPostNotification(QPointer co } void NotificationsManager::postNotification(NeoChatRoom *room, - const QString &sender, + const RoomMember &member, const QString &text, - const QImage &icon, const QString &replyEventId, bool canReply, qint64 timestamp) @@ -222,11 +215,11 @@ void NotificationsManager::postNotification(NeoChatRoom *room, if (room->isDirectChat()) { entry = text.toHtmlEscaped(); } else { - entry = i18n("%1: %2", sender, text.toHtmlEscaped()); + entry = i18n("%1: %2", member.displayName(), text.toHtmlEscaped()); } notification->setText(entry); - notification->setPixmap(createNotificationImage(icon, room)); + notification->setPixmap(createNotificationImage(member, room)); auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room")); connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() { @@ -294,18 +287,10 @@ void NotificationsManager::doPostInviteNotification(QPointer room) } const auto sender = room->member(roomMemberEvent->senderId()); - QImage avatar_image; - if (roomMemberEvent && !room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) { - avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {}); - } else { - qWarning() << "using this room's avatar"; - avatar_image = room->avatar(128); - } - KNotification *notification = new KNotification(u"invite"_s); notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName())); notification->setTitle(room->displayName()); - notification->setPixmap(createNotificationImage(avatar_image, nullptr)); + notification->setPixmap(createNotificationImage(sender, room)); auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat")); connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() { if (!room) { @@ -411,41 +396,125 @@ void NotificationsManager::postPushNotification(const QByteArray &message) } } -QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room) +QPixmap NotificationsManager::createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room) +{ + QImage senderIcon = member.avatar(avatarDimension, avatarDimension, {}); + bool senderIconIsPlaceholder = false; + if (senderIcon.isNull()) { + senderIcon = createPlaceholderImage(member.displayName()); + senderIconIsPlaceholder = true; + } + + QImage icon; + if (room->isDirectChat()) { + icon = senderIcon; + } else { + QImage roomIcon = room->avatar(avatarDimension, avatarDimension); + bool roomIconIsPlaceholder = false; + if (roomIcon.isNull()) { + roomIcon = createPlaceholderImage(room->displayName()); + roomIconIsPlaceholder = true; + } + + icon = createCombinedNotificationImage(senderIcon, senderIconIsPlaceholder, roomIcon, roomIconIsPlaceholder); + } + + return QPixmap::fromImage(icon); +} + +QImage NotificationsManager::createCombinedNotificationImage(const QImage &senderIcon, + const bool senderIconIsPlaceholder, + const QImage &roomIcon, + const bool roomIconIsPlaceholder) { // Handle avatars that are lopsided in one dimension - const int biggestDimension = std::max(icon.width(), icon.height()); - const QRect imageRect{0, 0, biggestDimension, biggestDimension}; + const int biggestDimension = std::max(senderIcon.width(), senderIcon.height()); + const QRectF imageRect = QRect{0, 0, biggestDimension, biggestDimension}.toRectF(); - QImage roundedImage(imageRect.size(), QImage::Format_ARGB32); + QImage roundedImage(imageRect.size().toSize(), QImage::Format_ARGB32); roundedImage.fill(Qt::transparent); QPainter painter(&roundedImage); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.setPen(Qt::NoPen); - // Fill background for transparent avatars - painter.setBrush(Qt::white); - painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height()); + if (senderIconIsPlaceholder) { + painter.drawImage(imageRect, senderIcon); + } else { + // Fill background for transparent non-placeholder avatars + painter.setBrush(Qt::white); + painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height()); - QBrush brush(icon.scaledToHeight(biggestDimension)); - painter.setBrush(brush); - painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height()); - - if (room != nullptr) { - const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height()); - if (!roomAvatar.isNull() && icon != roomAvatar) { - const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2}; - - painter.setBrush(Qt::white); - painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height()); - - painter.setBrush(roomAvatar.scaled(lowerQuarter.size())); - painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height()); - } + painter.setBrush(senderIcon.scaledToHeight(biggestDimension)); + painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height()); } - return QPixmap::fromImage(roundedImage); + const QRectF lowerQuarter{imageRect.center(), imageRect.size() / 2.0}; + + if (roomIconIsPlaceholder) { + // Ditto for room icons, but we also want to "carve out" the transparent area for readability + painter.setCompositionMode(QPainter::CompositionMode_Clear); + painter.setBrush(Qt::transparent); + painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height()); + + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.drawImage(lowerQuarter, roomIcon); + } else { + painter.setBrush(Qt::white); + painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height()); + + painter.setBrush(roomIcon.scaled(lowerQuarter.size().toSize())); + painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height()); + } + + return roundedImage; +} + +QImage NotificationsManager::createPlaceholderImage(const QString &name) +{ + const QColor color = NameUtils().colorsFromString(name); + + QImage image(avatarDimension, avatarDimension, QImage::Format_ARGB32); + image.fill(Qt::transparent); + + QPainter painter(&image); + painter.setRenderHint(QPainter::Antialiasing); + + // Draw background + QColor backgroundColor = color; + backgroundColor.setAlphaF(0.07); // Same as in Kirigami Add-ons. + + painter.setBrush(backgroundColor); + painter.setPen(Qt::transparent); + painter.drawRoundedRect(image.rect(), image.width(), image.height()); + + constexpr float borderWidth = 3.0; // Slightly bigger than in Add-ons so it renders better with QPainter at these dimensions. + + // Draw border + painter.setBrush(Qt::transparent); + painter.setPen(QPen(color, borderWidth)); + painter.drawRoundedRect(image.rect().toRectF().marginsRemoved(QMarginsF(borderWidth, borderWidth, borderWidth, borderWidth)), + image.width(), + image.height()); + + const QString initials = NameUtils().initialsFromString(name); + + QTextOption option; + option.setAlignment(Qt::AlignCenter); + + // Calculation similar to the one found in Kirigami Add-ons. + constexpr int largeSpacing = 8; // Same as what's defined in kirigami. + constexpr int padding = std::max(0, std::min(largeSpacing, avatarDimension - largeSpacing * 2)); + + QFont font; + font.setPixelSize((avatarDimension - padding) / 2); + + painter.setBrush(color); + painter.setPen(color); + painter.setFont(font); + painter.drawText(image.rect(), initials, option); + + return image; } #include "moc_notificationsmanager.cpp" diff --git a/src/app/notificationsmanager.h b/src/app/notificationsmanager.h index ab0a54592..432db1aba 100644 --- a/src/app/notificationsmanager.h +++ b/src/app/notificationsmanager.h @@ -13,6 +13,11 @@ #include #include +namespace Quotient +{ +class RoomMember; +} + class NeoChatConnection; class KNotification; class NeoChatRoom; @@ -67,15 +72,24 @@ private: QStringList m_connActiveJob; void startNotificationJob(QPointer connection); - QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room); + /** + * @return A combined image of the sender and room's avatar. + */ + static QPixmap createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room); + + /** + * @return The sender and room icon combined together into one image. Used internally by createNotificationImage. + */ + static QImage createCombinedNotificationImage(const QImage &senderIcon, bool senderIconIsPlaceholder, const QImage &roomIcon, bool roomIconIsPlaceholder); + + /** + * @return A placeholder avatar image, similar to the one found in Kirigami Add-ons. + */ + static QImage createPlaceholderImage(const QString &name); + bool shouldPostNotification(QPointer connection, const QJsonValue ¬ification); - void postNotification(NeoChatRoom *room, - const QString &sender, - const QString &text, - const QImage &icon, - const QString &replyEventId, - bool canReply, - qint64 timestamp); + void + postNotification(NeoChatRoom *room, const Quotient::RoomMember &member, const QString &text, const QString &replyEventId, bool canReply, qint64 timestamp); void doPostInviteNotification(QPointer room); @@ -84,6 +98,8 @@ private: bool permissionAsked = false; + static constexpr int avatarDimension = 128; + private Q_SLOTS: void processNotificationJob(QPointer connection, Quotient::GetNotificationsJob *job, bool initialization); };