Unverified Commit 5aee0f98 authored by Melvin Keskin's avatar Melvin Keskin Committed by Linus Jahn

Improve notifications

* Add actions for opening chat and marking messages as read
* Do not notify for new messages when sender is muted or chat with sender is already opened
Co-authored-by: Jonah Brüchert's avatarJonah Brüchert <jbb.prv@gmx.de>
Co-authored-by: cacahueto's avatarcaca hueto <cacahueto@olomono.de>
Co-authored-by: Linus Jahn's avatarLinus Jahn <lnj@kaidan.im>
parent e329cd00
Pipeline #25257 passed with stages
in 29 minutes and 16 seconds
......@@ -31,7 +31,6 @@
#include "ClientWorker.h"
// Qt
#include <QDebug>
#include <QGuiApplication>
#include <QSettings>
#include <QStringBuilder>
#include <QString>
......@@ -55,8 +54,8 @@
#include "UploadManager.h"
#include "VCardManager.h"
ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QGuiApplication *app, QObject* parent)
: QObject(parent), m_caches(caches), kaidan(kaidan), enableLogging(enableLogging), app(app)
ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QObject* parent)
: QObject(parent), m_caches(caches), kaidan(kaidan), enableLogging(enableLogging)
{
client = new QXmppClient(this);
logger = new LogHandler(client, this);
......@@ -65,7 +64,7 @@ ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, Q
registrationManager = new RegistrationManager(kaidan, this, client, caches->settings);
rosterManager = new RosterManager(kaidan, client, caches->rosterModel,
caches->avatarStorage, vCardManager, this);
msgHandler = new MessageHandler(kaidan, client, caches->msgModel, this);
msgHandler = new MessageHandler(kaidan, this, client, caches->msgModel);
discoManager = new DiscoveryManager(client, this);
uploadManager = new UploadManager(client, rosterManager, this);
downloadManager = new DownloadManager(kaidan, caches->transferCache,
......@@ -84,8 +83,11 @@ ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, Q
versionManager->setClientVersion(VERSION_STRING);
versionManager->setClientOs(QSysInfo::prettyProductName());
// Client State Indication
connect(app, &QGuiApplication::applicationStateChanged, this, &ClientWorker::setCsiState);
// Inform the client worker when the application window becomes active or inactive.
connect(kaidan, &Kaidan::applicationWindowActiveChanged, this, &ClientWorker::setIsApplicationWindowActive);
// Reduce the network traffic when the application window is inactive.
connect(kaidan, &Kaidan::applicationWindowActiveChanged, client, &QXmppClient::setActive);
// account deletion
connect(kaidan, &Kaidan::deleteAccountFromClient, this, &ClientWorker::deleteAccountFromClient);
......@@ -235,6 +237,11 @@ void ClientWorker::changeDisplayName(const QString &displayName)
}
}
bool ClientWorker::isApplicationWindowActive() const
{
return m_isApplicationWindowActive;
}
void ClientWorker::onConnected()
{
// no mutex needed, because this is called from updateClient()
......@@ -346,12 +353,9 @@ void ClientWorker::onConnectionError(QXmppClient::Error error)
}
}
void ClientWorker::setCsiState(Qt::ApplicationState state)
void ClientWorker::setIsApplicationWindowActive(bool active)
{
if (state == Qt::ApplicationActive)
client->setActive(true);
else
client->setActive(false);
m_isApplicationWindowActive = active;
}
QString ClientWorker::generateJidResourceWithRandomSuffix(const QString jidResourcePrefix , unsigned int length) const
......
......@@ -35,7 +35,6 @@
#include <QObject>
#include <QSettings>
#include <QTimer>
class QGuiApplication;
// QXmpp
#include <QXmppClient.h>
// Kaidan
......@@ -125,10 +124,9 @@ public:
* @param caches All caches running in the main thread for communication with the UI.
* @param kaidan Main back-end class, running in the main thread.
* @param enableLogging If logging of the XMPP stream should be done.
* @param app The QGuiApplication to determine if the window is active.
* @param parent Optional QObject-based parent.
*/
ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QGuiApplication *app, QObject *parent = nullptr);
ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QObject *parent = nullptr);
VCardManager *getVCardManager() const;
......@@ -208,7 +206,23 @@ public slots:
*/
void changeDisplayName(const QString &displayName);
/**
* Returns whether the application window is active.
*
* The application window is active when it is in the foreground and focused.
*/
bool isApplicationWindowActive() const;
signals:
/**
* Requests to show a notification for a chat message via the system's notification channel.
*
* @param senderJid JID of the message's sender
* @param senderName name of the message's sender
* @param message message to show
*/
void showMessageNotificationRequested(const QString &senderJid, const QString &senderName, const QString &message);
// Those signals are emitted by Kaidan.cpp and are used by this class.
void connectRequested();
void disconnectRequested();
......@@ -228,6 +242,13 @@ signals:
void deleteAccountFromDatabase();
private slots:
/**
* Sets the value to know whether the application window is active.
*
* @param active true if the application window is active, false otherwise
*/
void setIsApplicationWindowActive(bool active);
/**
* Called when an authenticated connection to the server is established.
*/
......@@ -243,11 +264,6 @@ private slots:
*/
void onConnectionError(QXmppClient::Error error);
/**
* Uses the QGuiApplication state to reduce network traffic when window is minimized
*/
void setCsiState(Qt::ApplicationState state);
private:
/**
* Generates the resource part of a JID with a suffix consisting of a dot followed by random alphanumeric characters.
......@@ -262,7 +278,6 @@ private:
LogHandler *logger;
Credentials creds;
bool enableLogging;
QGuiApplication *app;
RegistrationManager *registrationManager;
RosterManager *rosterManager;
......@@ -272,6 +287,7 @@ private:
UploadManager *uploadManager;
DownloadManager *downloadManager;
bool m_isApplicationWindowActive;
bool m_isReconnecting = false;
QXmppConfiguration m_configToBeUsedOnNextConnect;
......
......@@ -45,6 +45,7 @@
#include "Database.h"
#include "MessageDb.h"
#include "MessageModel.h"
#include "Notifications.h"
#include "PresenceCache.h"
#include "QmlUtils.h"
#include "RosterDb.h"
......@@ -63,6 +64,8 @@ Kaidan::Kaidan(QGuiApplication *app, bool enableLogging, QObject *parent)
Q_ASSERT(!s_instance);
s_instance = this;
connect(app, &QGuiApplication::applicationStateChanged, this, &Kaidan::handleApplicationStateChanged);
// Database setup
m_database->moveToThread(m_dbThrd);
m_msgDb->moveToThread(m_dbThrd);
......@@ -98,11 +101,14 @@ Kaidan::Kaidan(QGuiApplication *app, bool enableLogging, QObject *parent)
// Start ClientWorker on new thread
//
m_client = new ClientWorker(m_caches, this, enableLogging, app);
m_client = new ClientWorker(m_caches, this, enableLogging);
m_client->setCredentials(creds);
m_client->moveToThread(m_cltThrd);
connect(m_client, &ClientWorker::connectionErrorChanged, this, &Kaidan::setConnectionError);
connect(m_client, &ClientWorker::showMessageNotificationRequested, this, [](const QString &senderJid, const QString &senderName, const QString &message) {
Notifications::sendMessageNotification(senderJid, senderName, message);
});
connect(m_cltThrd, &QThread::started, m_client, &ClientWorker::main);
m_client->setObjectName("XmppClient");
......@@ -143,6 +149,14 @@ void Kaidan::mainDisconnect()
emit m_client->disconnectRequested();
}
void Kaidan::handleApplicationStateChanged(Qt::ApplicationState applicationState)
{
if (applicationState == Qt::ApplicationActive)
emit applicationWindowActiveChanged(true);
else
emit applicationWindowActiveChanged(false);
}
void Kaidan::setConnectionState(QXmppClient::State state)
{
if (this->connectionState != static_cast<ConnectionState>(state)) {
......
......@@ -213,6 +213,15 @@ public:
Q_INVOKABLE bool logInByUri(const QString &uri);
signals:
/**
* Emitted when the application window becomes active or inactive.
*
* The application window is active when it is in the foreground and focused.
*
* @param active true if the application window becomes active, false otherwise
*/
void applicationWindowActiveChanged(bool active);
void avatarStorageChanged();
/**
......@@ -255,6 +264,18 @@ signals:
*/
void loggedInWithNewCredentials();
/**
* Raises the window to the foreground so that it is on top of all other windows.
*/
void raiseWindowRequested();
/**
* Opens the chat page for a given JID.
*
* @param chatJid JID of the chat for which the chat page is opened
*/
void openChatPageRequested(const QString chatJid);
/**
* Show passive notification
*/
......@@ -387,6 +408,13 @@ signals:
void registrationFailed(quint8 error, const QString &errrorMessage);
public slots:
/**
* Handles a changed application state and emits whether the application window is active.
*
* @param applicationState state of the GUI application
*/
void handleApplicationStateChanged(Qt::ApplicationState applicationState);
/**
* Set current connection state
*/
......
......@@ -40,14 +40,13 @@
#include <QXmppRosterManager.h>
#include <QXmppUtils.h>
// Kaidan
#include "ClientWorker.h"
#include "Kaidan.h"
#include "MessageModel.h"
#include "Notifications.h"
#include "MediaUtils.h"
MessageHandler::MessageHandler(Kaidan *kaidan, QXmppClient *client, MessageModel *model,
QObject *parent)
: QObject(parent), kaidan(kaidan), client(client), model(model)
MessageHandler::MessageHandler(Kaidan *kaidan, ClientWorker *clientWorker, QXmppClient *client, MessageModel *model)
: QObject(clientWorker), kaidan(kaidan), m_clientWorker(clientWorker), client(client), model(model)
{
connect(client, &QXmppClient::messageReceived, this, &MessageHandler::handleMessage);
connect(kaidan, &Kaidan::sendMessage, this, &MessageHandler::sendMessage);
......@@ -183,8 +182,12 @@ void MessageHandler::handleMessage(const QXmppMessage &msg)
if (contactName.isEmpty())
contactName = contactJid;
if (!message.sentByMe())
Notifications::sendMessageNotification(contactJid, contactName, msg.body());
// Show a notification for the message in the following cases:
// * The message was not sent by the user from another resource and received via Message Carbons.
// * Notifications from the chat partner are not muted.
// * The corresponding chat is not opened while the application window is active.
if (!message.sentByMe() && !kaidan->notificationsMuted(contactJid) && (model->currentChatJid() != message.from() || !m_clientWorker->isApplicationWindowActive()))
emit m_clientWorker->showMessageNotificationRequested(contactJid, contactName, msg.body());
// TODO: Move back following call to RosterManager::handleMessage when spoiler
// messages are implemented in QXmpp
......
......@@ -39,6 +39,7 @@
// Kaidan
#include "Message.h"
class ClientWorker;
class Kaidan;
class MessageModel;
class QXmppMessage;
......@@ -53,8 +54,7 @@ class MessageHandler : public QObject
Q_OBJECT
public:
MessageHandler(Kaidan *kaidan, QXmppClient *client, MessageModel *model,
QObject *parent = nullptr);
MessageHandler(Kaidan *kaidan, ClientWorker *clientWorker, QXmppClient *client, MessageModel *model);
~MessageHandler();
......@@ -89,6 +89,7 @@ private slots:
private:
Kaidan *kaidan;
ClientWorker *m_clientWorker;
QXmppClient *client;
QXmppMessageReceiptManager receiptManager;
MessageModel *model;
......
......@@ -248,7 +248,8 @@ void MessageModel::insertMessage(int idx, const Message &msg)
void MessageModel::addMessage(Message msg)
{
if (QXmppUtils::jidToBareJid(msg.from()) == m_currentChatJid || QXmppUtils::jidToBareJid(msg.to()) == m_currentChatJid) {
if (QXmppUtils::jidToBareJid(msg.from()) == m_currentChatJid ||
QXmppUtils::jidToBareJid(msg.to()) == m_currentChatJid) {
processMessage(msg);
// index where to add the new message
......
......@@ -28,28 +28,41 @@
* along with Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
// Kaidan
#include "Notifications.h"
#include "Kaidan.h"
// Qt
#include <QVariant>
// KNotifications
#ifdef HAVE_KNOTIFICATIONS
#include <KNotification>
#endif
// Kaidan
#include "Kaidan.h"
#ifdef HAVE_KNOTIFICATIONS
void Notifications::sendMessageNotification(const QString& jid, const QString& fromName, const QString& message)
void Notifications::sendMessageNotification(const QString &senderJid, const QString &senderName, const QString &message)
{
if (!Kaidan::instance()->notificationsMuted(jid)) {
KNotification *notification = new KNotification("new-message");
notification->setText(message);
notification->setTitle(fromName);
KNotification *notification = new KNotification("new-message");
notification->setText(message);
notification->setTitle(senderName);
#ifdef Q_OS_ANDROID
notification->setIconName("kaidan-bw");
notification->setIconName("kaidan-bw");
#endif
notification->sendEvent();
}
notification->setDefaultAction("Open");
notification->setActions(QStringList {
QObject::tr("Mark as read")
});
QObject::connect(notification, &KNotification::defaultActivated, [=] {
emit Kaidan::instance()->openChatPageRequested(senderJid);
emit Kaidan::instance()->raiseWindowRequested();
});
QObject::connect(notification, &KNotification::action1Activated, [=] {
emit Kaidan::instance()->getRosterModel()->updateItemRequested(senderJid, [=](RosterItem &item) {
item.setUnreadMessages(0);
});
});
notification->sendEvent();
}
#else
void Notifications::sendMessageNotification(const QString&, const QString&, const QString&)
......
......@@ -28,10 +28,22 @@
* along with Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QString>
#ifndef NOTIFICATIONS_H
#define NOTIFICATIONS_H
class QString;
class Notifications
{
public:
static void sendMessageNotification(const QString &jid, const QString &fromName, const QString &message);
/**
* Sends a system notification for a chat message.
*
* @param senderJid JID of the message's sender
* @param senderName name of the message's sender
* @param message message to show
*/
static void sendMessageNotification(const QString &senderJid, const QString &senderName, const QString &message);
};
#endif // NOTIFICATIONS_H
......@@ -70,7 +70,6 @@ private:
RosterModel *model;
AvatarFileStorage *avatarStorage;
VCardManager *vCardManager;
QXmppRosterManager *manager;
QString m_currentChatJid;
};
......
......@@ -64,7 +64,7 @@ RosterModel::RosterModel(RosterDb *rosterDb, QObject *parent)
connect(this, &RosterModel::replaceItemsRequested,
this, &RosterModel::replaceItems);
connect(this, &RosterModel::replaceItemsRequested,
rosterDb, &RosterDb::replaceItems);
rosterDb, &RosterDb::replaceItems);
// This is only done in the model, the database is updated automatically by the new
// messages:
......@@ -135,6 +135,23 @@ QVariant RosterModel::data(const QModelIndex &index, int role) const
return {};
}
std::optional<const RosterItem> RosterModel::findItem(const QString &jid) const
{
for (const auto &item : qAsConst(m_items)) {
if (item.jid() == jid)
return item;
}
return std::nullopt;
}
QString RosterModel::itemName(const QString &jid) const
{
if (auto item = findItem(jid))
return item->name();
return {};
}
void RosterModel::handleItemsFetched(const QVector<RosterItem> &items)
{
beginResetModel();
......@@ -161,7 +178,6 @@ void RosterModel::removeItem(const QString &jid)
}
i++;
}
}
void RosterModel::updateItem(const QString &jid,
......
......@@ -61,6 +61,13 @@ public:
Q_REQUIRED_RESULT QHash<int, QByteArray> roleNames() const override;
Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role) const override;
/**
* Retrieves the name of a roster item.
*
* @return Name of the roster item or a null string
*/
Q_INVOKABLE QString itemName(const QString &jid) const;
signals:
void addItemRequested(const RosterItem &item);
void removeItemRequested(const QString &jid);
......@@ -82,6 +89,11 @@ private slots:
void setLastExchanged(const QString &contactJid, const QDateTime &newLastExchanged);
private:
/**
* Searches for the roster item with a given JID.
*/
std::optional<const RosterItem> findItem(const QString &jid) const;
void insertContact(int i, const RosterItem &item);
int updateItemPosition(int currentIndex);
int positionToInsert(const RosterItem &item);
......
......@@ -44,7 +44,12 @@ import "elements"
ChatPageBase {
id: root
property string chatName
property string chatName: {
var currentChatJid = Kaidan.messageModel.currentChatJid
var chatDisplayName = Kaidan.rosterModel.itemName(currentChatJid)
return chatDisplayName ? chatDisplayName : currentChatJid
}
property bool isWritingSpoiler
property string messageToCorrect
......
......@@ -104,6 +104,7 @@ Kirigami.ScrollablePage {
id: filterModel
sourceModel: Kaidan.rosterModel
}
delegate: RosterListItem {
id: rosterItem
width: root.width
......@@ -115,6 +116,7 @@ Kirigami.ScrollablePage {
statusMsg: Kaidan.presenceCache.getStatusText(model.jid)
unreadMessages: model.unreadMessages
avatarImagePath: Kaidan.avatarStorage.getAvatarUrl(model.jid)
backgroundColor: {
if (!Kirigami.Settings.isMobile &&
Kaidan.messageModel.currentChatJid === model.jid) {
......@@ -123,18 +125,8 @@ Kirigami.ScrollablePage {
Kirigami.Theme.backgroundColor
}
}
onClicked: {
searchAction.checked = false
// We need to cache the chatName, because changing the currentChatJid in the
// message model will in some cases also update the roster model. That
// will then remove this item and readd an updated version of it, so
// model.* won't work anymore after this.
var chatName = model.name ? model.name : model.jid
Kaidan.messageModel.currentChatJid = model.jid
pageStack.push(chatPage, {
"chatName": chatName
})
}
onClicked: openChatPage(model.jid)
function newPresenceArrived(jid) {
if (jid === model.jid) {
......@@ -143,20 +135,42 @@ Kirigami.ScrollablePage {
}
}
function xmppUriReceived(uri) {
// 'xmpp:' has length of 5
addContactSheet.jid = uri.substr(5)
addContactSheet.open()
}
Component.onCompleted: {
Kaidan.presenceCache.presenceChanged.connect(newPresenceArrived)
Kaidan.xmppUriReceived.connect(xmppUriReceived)
}
Component.onDestruction: {
Kaidan.presenceCache.presenceChanged.disconnect(newPresenceArrived)
Kaidan.xmppUriReceived.disconnect(xmppUriReceived)
}
}
Connections {
target: Kaidan
onOpenChatPageRequested: openChatPage(chatJid)
onXmppUriReceived: xmppUriReceived(uri)
}
}
/**
* Opens the chat page for the chat JID currently set in the message model.
*
* @param chatJid JID of the chat for which the chat page is opened
*/
function openChatPage(chatJid) {
Kaidan.messageModel.currentChatJid = chatJid
searchAction.checked = false
// Close all pages (especially the chat page) except the roster page.
while (pageStack.depth > 1)
pageStack.pop()
pageStack.push(chatPage)
}
function xmppUriReceived(uri) {
// 'xmpp:' has length of 5
addContactSheet.jid = uri.substr(5)
addContactSheet.open()
}
}
......@@ -94,6 +94,13 @@ Kirigami.ApplicationWindow {
Component {id: accountDeletionFromClientConfirmationPage; AccountDeletionFromClientConfirmationPage {}}
Component {id: accountDeletionFromClientAndServerConfirmationPage; AccountDeletionFromClientAndServerConfirmationPage {}}
function raiseWindow() {
if (!active) {
raise()
requestActivate()
}
}
/**
* Shows a passive notification for a long period.
*/
......@@ -155,6 +162,7 @@ Kirigami.ApplicationWindow {
Connections {
target: Kaidan
onRaiseWindowRequested: raiseWindow()
onPassiveNotificationRequested: passiveNotification(text)
onNewCredentialsNeeded: openStartPage()
onLoggedInWithNewCredentials: openChatView()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment