Render placeholder avatars in notifications, like we do in app
Right now if someone or a room doesn't have an avatar, there's a good chance it will end up falling back to the NeoChat app icon (not very descriptive.) It also tends to break the flow of conversations, that's the whole reason to have an avatar in the notification in the first place. So I made it render a similar-looking avatar like it does in-app, by re-implementing it in QPainter. I also took the time to refactor and clean up the avatar generation for notifications so the logic should be easier to follow. To reduce the maintenance required, we re-use some functionality from Kirigami Add-ons. The rest of the drawing logic is custom but sensible, but some creative liberty was used to ensure it looks decent.
This commit is contained in:
@@ -220,6 +220,7 @@ target_link_libraries(neochat PUBLIC
|
|||||||
KF6::ItemModels
|
KF6::ItemModels
|
||||||
KF6::I18nQml
|
KF6::I18nQml
|
||||||
KirigamiApp
|
KirigamiApp
|
||||||
|
KirigamiAddonsComponents
|
||||||
QuotientQt6
|
QuotientQt6
|
||||||
Login
|
Login
|
||||||
Rooms
|
Rooms
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <KNotification>
|
#include <KNotification>
|
||||||
#include <KNotificationPermission>
|
#include <KNotificationPermission>
|
||||||
#include <KNotificationReplyAction>
|
#include <KNotificationReplyAction>
|
||||||
|
#include <KirigamiAddons/Components/NameUtils>
|
||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <Quotient/accountregistry.h>
|
#include <Quotient/accountregistry.h>
|
||||||
@@ -144,16 +145,9 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
|
|||||||
body = notification["event"_L1]["content"_L1]["body"_L1].toString();
|
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<NeoChatRoom *>(room),
|
postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||||
sender.displayName(),
|
room->member(sender.id()),
|
||||||
body,
|
body,
|
||||||
avatar_image,
|
|
||||||
notification["event"_L1].toObject()["event_id"_L1].toString(),
|
notification["event"_L1].toObject()["event_id"_L1].toString(),
|
||||||
true,
|
true,
|
||||||
pair.first);
|
pair.first);
|
||||||
@@ -195,9 +189,8 @@ bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> co
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NotificationsManager::postNotification(NeoChatRoom *room,
|
void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||||
const QString &sender,
|
const RoomMember &member,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
const QImage &icon,
|
|
||||||
const QString &replyEventId,
|
const QString &replyEventId,
|
||||||
bool canReply,
|
bool canReply,
|
||||||
qint64 timestamp)
|
qint64 timestamp)
|
||||||
@@ -222,11 +215,11 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
|||||||
if (room->isDirectChat()) {
|
if (room->isDirectChat()) {
|
||||||
entry = text.toHtmlEscaped();
|
entry = text.toHtmlEscaped();
|
||||||
} else {
|
} else {
|
||||||
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
|
entry = i18n("%1: %2", member.displayName(), text.toHtmlEscaped());
|
||||||
}
|
}
|
||||||
|
|
||||||
notification->setText(entry);
|
notification->setText(entry);
|
||||||
notification->setPixmap(createNotificationImage(icon, room));
|
notification->setPixmap(createNotificationImage(member, room));
|
||||||
|
|
||||||
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
|
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
|
||||||
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
||||||
@@ -294,18 +287,10 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
|
|||||||
}
|
}
|
||||||
const auto sender = room->member(roomMemberEvent->senderId());
|
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);
|
KNotification *notification = new KNotification(u"invite"_s);
|
||||||
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
|
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
|
||||||
notification->setTitle(room->displayName());
|
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"));
|
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
|
||||||
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
||||||
if (!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
|
// Handle avatars that are lopsided in one dimension
|
||||||
const int biggestDimension = std::max(icon.width(), icon.height());
|
const int biggestDimension = std::max(senderIcon.width(), senderIcon.height());
|
||||||
const QRect imageRect{0, 0, biggestDimension, biggestDimension};
|
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);
|
roundedImage.fill(Qt::transparent);
|
||||||
|
|
||||||
QPainter painter(&roundedImage);
|
QPainter painter(&roundedImage);
|
||||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||||
painter.setPen(Qt::NoPen);
|
painter.setPen(Qt::NoPen);
|
||||||
|
|
||||||
// Fill background for transparent avatars
|
if (senderIconIsPlaceholder) {
|
||||||
painter.setBrush(Qt::white);
|
painter.drawImage(imageRect, senderIcon);
|
||||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
} 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(senderIcon.scaledToHeight(biggestDimension));
|
||||||
painter.setBrush(brush);
|
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
#include "moc_notificationsmanager.cpp"
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
#include <Quotient/csapi/notifications.h>
|
#include <Quotient/csapi/notifications.h>
|
||||||
#include <Quotient/jobs/basejob.h>
|
#include <Quotient/jobs/basejob.h>
|
||||||
|
|
||||||
|
namespace Quotient
|
||||||
|
{
|
||||||
|
class RoomMember;
|
||||||
|
}
|
||||||
|
|
||||||
class NeoChatConnection;
|
class NeoChatConnection;
|
||||||
class KNotification;
|
class KNotification;
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
@@ -67,15 +72,24 @@ private:
|
|||||||
QStringList m_connActiveJob;
|
QStringList m_connActiveJob;
|
||||||
void startNotificationJob(QPointer<NeoChatConnection> connection);
|
void startNotificationJob(QPointer<NeoChatConnection> 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<NeoChatConnection> connection, const QJsonValue ¬ification);
|
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue ¬ification);
|
||||||
void postNotification(NeoChatRoom *room,
|
void
|
||||||
const QString &sender,
|
postNotification(NeoChatRoom *room, const Quotient::RoomMember &member, const QString &text, const QString &replyEventId, bool canReply, qint64 timestamp);
|
||||||
const QString &text,
|
|
||||||
const QImage &icon,
|
|
||||||
const QString &replyEventId,
|
|
||||||
bool canReply,
|
|
||||||
qint64 timestamp);
|
|
||||||
|
|
||||||
void doPostInviteNotification(QPointer<NeoChatRoom> room);
|
void doPostInviteNotification(QPointer<NeoChatRoom> room);
|
||||||
|
|
||||||
@@ -84,6 +98,8 @@ private:
|
|||||||
|
|
||||||
bool permissionAsked = false;
|
bool permissionAsked = false;
|
||||||
|
|
||||||
|
static constexpr int avatarDimension = 128;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
|
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user