diff --git a/src/Database.cpp b/src/Database.cpp
index a281635..178d042 100644
--- a/src/Database.cpp
+++ b/src/Database.cpp
@@ -1,383 +1,385 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#include "Database.h"
#include "Globals.h"
#include "Utils.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DATABASE_CONVERT_TO_VERSION(n) \
if (m_version < n) { \
convertDatabaseToV##n(); \
}
// Both need to be updated on version bump:
#define DATABASE_LATEST_VERSION 10
#define DATABASE_CONVERT_TO_LATEST_VERSION() DATABASE_CONVERT_TO_VERSION(10)
#define SQL_BOOL "BOOL"
#define SQL_INTEGER "INTEGER"
#define SQL_INTEGER_NOT_NULL "INTEGER NOT NULL"
#define SQL_TEXT "TEXT"
#define SQL_TEXT_NOT_NULL "TEXT NOT NULL"
#define SQL_BLOB "BLOB"
#define SQL_CREATE_TABLE(tableName, contents) \
"CREATE TABLE IF NOT EXISTS '" QT_STRINGIFY(tableName) "' (" contents ")"
#define SQL_LAST_ATTRIBUTE(name, dataType) \
"'" QT_STRINGIFY(name) "' " dataType
#define SQL_ATTRIBUTE(name, dataType) \
SQL_LAST_ATTRIBUTE(name, dataType) ","
Database::Database(QObject *parent)
: QObject(parent)
{
}
Database::~Database()
{
m_database.close();
}
void Database::openDatabase()
{
m_database = QSqlDatabase::addDatabase("QSQLITE", DB_CONNECTION);
if (!m_database.isValid())
qFatal("Cannot add database: %s", qPrintable(m_database.lastError().text()));
const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (!writeDir.mkpath(".")) {
qFatal("Failed to create writable directory at %s", qPrintable(writeDir.absolutePath()));
}
// Ensure that we have a writable location on all devices.
const QString fileName = writeDir.absoluteFilePath(DB_FILENAME);
// open() will create the SQLite database if it doesn't exist.
m_database.setDatabaseName(fileName);
if (!m_database.open()) {
qFatal("Cannot open database: %s", qPrintable(m_database.lastError().text()));
}
loadDatabaseInfo();
if (needToConvert())
convertDatabase();
}
void Database::transaction()
{
if (!m_transactions) {
// currently no transactions running
if (!m_database.transaction()) {
qWarning() << "Could not begin transaction on database:"
<< m_database.lastError().text();
}
}
// increase counter
m_transactions++;
}
void Database::commit()
{
// reduce counter
m_transactions--;
Q_ASSERT(m_transactions >= 0);
if (!m_transactions) {
// no transaction requested anymore
if (!m_database.commit()) {
qWarning() << "Could not commit transaction on database:"
<< m_database.lastError().text();
}
}
}
void Database::loadDatabaseInfo()
{
QStringList tables = m_database.tables();
if (!tables.contains(DB_TABLE_INFO)) {
if (tables.contains(DB_TABLE_MESSAGES) &&
tables.contains(DB_TABLE_ROSTER))
// old Kaidan v0.1/v0.2 table
m_version = 1;
else
m_version = 0;
// we've got all we want; do not query for a db version
return;
}
QSqlQuery query(m_database);
Utils::execQuery(query, "SELECT version FROM dbinfo");
QSqlRecord record = query.record();
int versionCol = record.indexOf("version");
while (query.next()) {
m_version = query.value(versionCol).toInt();
}
}
void Database::saveDatabaseInfo()
{
QSqlRecord updateRecord;
updateRecord.append(Utils::createSqlField("version", m_version));
QSqlQuery query(m_database);
Utils::execQuery(
query,
m_database.driver()->sqlStatement(
QSqlDriver::UpdateStatement,
DB_TABLE_INFO,
updateRecord,
false
)
);
}
bool Database::needToConvert()
{
return m_version < DATABASE_LATEST_VERSION;
}
void Database::convertDatabase()
{
qDebug() << "[database] Converting database to latest version from version" << m_version;
transaction();
if (m_version == 0)
createNewDatabase();
else
DATABASE_CONVERT_TO_LATEST_VERSION();
saveDatabaseInfo();
commit();
}
void Database::createNewDatabase()
{
createDbInfoTable();
createRosterTable();
createMessagesTable();
}
void Database::createDbInfoTable()
{
QSqlQuery query(m_database);
Utils::execQuery(
query,
SQL_CREATE_TABLE(
dbinfo,
SQL_LAST_ATTRIBUTE(version, SQL_INTEGER_NOT_NULL)
)
);
QSqlRecord insertRecord;
insertRecord.append(Utils::createSqlField("version", DATABASE_LATEST_VERSION));
Utils::execQuery(
query,
m_database.driver()->sqlStatement(
QSqlDriver::InsertStatement,
DB_TABLE_INFO,
insertRecord,
false
)
);
}
void Database::createRosterTable()
{
+ // TODO: remove lastExchanged and lastMessage
+
QSqlQuery query(m_database);
Utils::execQuery(
query,
SQL_CREATE_TABLE(
Roster,
SQL_ATTRIBUTE(jid, SQL_TEXT_NOT_NULL)
SQL_ATTRIBUTE(name, SQL_TEXT)
SQL_ATTRIBUTE(lastExchanged, SQL_TEXT_NOT_NULL)
SQL_ATTRIBUTE(unreadMessages, SQL_INTEGER)
SQL_LAST_ATTRIBUTE(lastMessage, SQL_TEXT)
)
);
}
void Database::createMessagesTable()
{
// TODO: the next time we change the messages table, we need to do:
// * rename author to sender, edited to isEdited
// * delete author_resource, recipient_resource
// * remove 'NOT NULL' from id
QSqlQuery query(m_database);
Utils::execQuery(
query,
SQL_CREATE_TABLE(
Messages,
SQL_ATTRIBUTE(author, SQL_TEXT_NOT_NULL)
SQL_ATTRIBUTE(author_resource, SQL_TEXT)
SQL_ATTRIBUTE(recipient, SQL_TEXT_NOT_NULL)
SQL_ATTRIBUTE(recipient_resource, SQL_TEXT)
SQL_ATTRIBUTE(timestamp, SQL_TEXT)
SQL_ATTRIBUTE(message, SQL_TEXT)
SQL_ATTRIBUTE(id, SQL_TEXT_NOT_NULL)
SQL_ATTRIBUTE(isSent, SQL_BOOL)
SQL_ATTRIBUTE(isDelivered, SQL_BOOL)
SQL_ATTRIBUTE(type, SQL_INTEGER)
SQL_ATTRIBUTE(mediaUrl, SQL_TEXT)
SQL_ATTRIBUTE(mediaSize, SQL_INTEGER)
SQL_ATTRIBUTE(mediaContentType, SQL_TEXT)
SQL_ATTRIBUTE(mediaLastModified, SQL_INTEGER)
SQL_ATTRIBUTE(mediaLocation, SQL_TEXT)
SQL_ATTRIBUTE(mediaThumb, SQL_BLOB)
SQL_ATTRIBUTE(mediaHashes, SQL_TEXT)
SQL_ATTRIBUTE(edited, SQL_BOOL)
SQL_ATTRIBUTE(spoilerHint, SQL_TEXT)
SQL_ATTRIBUTE(isSpoiler, SQL_BOOL)
"FOREIGN KEY('author') REFERENCES Roster ('jid'),"
"FOREIGN KEY('recipient') REFERENCES Roster ('jid')"
)
);
}
void Database::convertDatabaseToV2()
{
// create a new dbinfo table
createDbInfoTable();
m_version = 2;
}
void Database::convertDatabaseToV3()
{
DATABASE_CONVERT_TO_VERSION(2);
QSqlQuery query(m_database);
Utils::execQuery(query, "ALTER TABLE Roster ADD avatarHash TEXT");
m_version = 3;
}
void Database::convertDatabaseToV4()
{
DATABASE_CONVERT_TO_VERSION(3);
QSqlQuery query(m_database);
// SQLite doesn't support the ALTER TABLE drop columns feature, so we have to use a workaround.
// we copy all rows into a back-up table (but without `avatarHash`), and then delete the old table
// and copy everything to the normal table again
Utils::execQuery(query, "CREATE TEMPORARY TABLE roster_backup(jid,name,lastExchanged,"
"unreadMessages,lastMessage,lastOnline,activity,status,mood);");
Utils::execQuery(query, "INSERT INTO roster_backup SELECT jid,name,lastExchanged,unreadMessages,"
"lastMessage,lastOnline,activity,status,mood FROM Roster;");
Utils::execQuery(query, "DROP TABLE Roster;");
Utils::execQuery(query, "CREATE TABLE Roster('jid' TEXT NOT NULL,'name' TEXT NOT NULL,"
"'lastExchanged' TEXT NOT NULL,'unreadMessages' INTEGER,'lastMessage' TEXT,"
"'lastOnline' TEXT,'activity' TEXT,'status' TEXT,'mood' TEXT);");
Utils::execQuery(query, "INSERT INTO Roster SELECT jid,name,lastExchanged,unreadMessages,"
"lastMessage,lastOnline,activity,status,mood FROM Roster_backup;");
Utils::execQuery(query, "DROP TABLE Roster_backup;");
m_version = 4;
}
void Database::convertDatabaseToV5()
{
DATABASE_CONVERT_TO_VERSION(4);
QSqlQuery query(m_database);
Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'type' INTEGER");
Utils::execQuery(query, "UPDATE Messages SET type = 0 WHERE type IS NULL");
Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'mediaUrl' TEXT");
m_version = 5;
}
void Database::convertDatabaseToV6()
{
DATABASE_CONVERT_TO_VERSION(5);
QSqlQuery query(m_database);
for (const QString &column : {"'mediaSize' INTEGER",
"'mediaContentType' TEXT",
"'mediaLastModified' INTEGER",
"'mediaLocation' TEXT"}) {
Utils::execQuery(query, QString("ALTER TABLE 'Messages' ADD ").append(column));
}
m_version = 6;
}
void Database::convertDatabaseToV7()
{
DATABASE_CONVERT_TO_VERSION(6);
QSqlQuery query(m_database);
Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'mediaThumb' BLOB");
Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'mediaHashes' TEXT");
m_version = 7;
}
void Database::convertDatabaseToV8()
{
DATABASE_CONVERT_TO_VERSION(7);
QSqlQuery query(m_database);
Utils::execQuery(query, "CREATE TEMPORARY TABLE roster_backup(jid, name, lastExchanged, "
"unreadMessages, lastMessage);");
Utils::execQuery(query, "INSERT INTO roster_backup SELECT jid, name, lastExchanged, unreadMessages, "
"lastMessage FROM Roster;");
Utils::execQuery(query, "DROP TABLE Roster;");
Utils::execQuery(query, "CREATE TABLE IF NOT EXISTS Roster ('jid' TEXT NOT NULL,'name' TEXT,"
"'lastExchanged' TEXT NOT NULL, 'unreadMessages' INTEGER,"
"'lastMessage' TEXT);");
Utils::execQuery(query, "INSERT INTO Roster SELECT jid, name, lastExchanged, unreadMessages, "
"lastMessage FROM Roster_backup;");
Utils::execQuery(query, "DROP TABLE roster_backup;");
m_version = 8;
}
void Database::convertDatabaseToV9()
{
DATABASE_CONVERT_TO_VERSION(8);
QSqlQuery query(m_database);
Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'edited' BOOL");
m_version = 9;
}
void Database::convertDatabaseToV10()
{
DATABASE_CONVERT_TO_VERSION(9);
QSqlQuery query(m_database);
Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'isSpoiler' BOOL");
Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'spoilerHint' TEXT");
m_version = 10;
}
diff --git a/src/Kaidan.cpp b/src/Kaidan.cpp
index cc4b84c..f6eb26a 100644
--- a/src/Kaidan.cpp
+++ b/src/Kaidan.cpp
@@ -1,293 +1,294 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#include "Kaidan.h"
// Qt
#include
#include
#include
#include
// QXmpp
#include
#include "qxmpp-exts/QXmppColorGenerator.h"
#include "qxmpp-exts/QXmppUri.h"
// Kaidan
#include "AvatarFileStorage.h"
#include "Database.h"
#include "MessageDb.h"
#include "MessageModel.h"
#include "PresenceCache.h"
#include "QmlUtils.h"
#include "RosterDb.h"
#include "RosterModel.h"
Kaidan *Kaidan::s_instance;
Kaidan::Kaidan(QGuiApplication *app, bool enableLogging, QObject *parent)
: QObject(parent),
m_database(new Database()),
m_dbThrd(new QThread()),
m_msgDb(new MessageDb()),
m_rosterDb(new RosterDb(m_database)),
m_cltThrd(new QThread())
{
Q_ASSERT(!s_instance);
s_instance = this;
// Database setup
m_database->moveToThread(m_dbThrd);
m_msgDb->moveToThread(m_dbThrd);
m_rosterDb->moveToThread(m_dbThrd);
connect(m_dbThrd, &QThread::started, m_database, &Database::openDatabase);
m_dbThrd->setObjectName("SqlDatabase");
m_dbThrd->start();
// Caching components
m_caches = new ClientWorker::Caches(this, m_rosterDb, m_msgDb, this);
// Connect the avatar changed signal of the avatarStorage with the NOTIFY signal
// of the Q_PROPERTY for the avatar storage (so all avatars are updated in QML)
connect(m_caches->avatarStorage, &AvatarFileStorage::avatarIdsChanged,
this, &Kaidan::avatarStorageChanged);
//
// Load settings
//
- creds.jid = m_caches->settings->value(KAIDAN_SETTINGS_AUTH_JID).toString();
- creds.jidResourcePrefix = m_caches->settings->value(KAIDAN_SETTINGS_AUTH_JID_RESOURCE_PREFIX).toString();
- creds.password = QString(QByteArray::fromBase64(m_caches->settings->value(
- KAIDAN_SETTINGS_AUTH_PASSWD).toString().toUtf8()));
+ setJid(m_caches->settings->value(KAIDAN_SETTINGS_AUTH_JID).toString());
+ setJidResourcePrefix(m_caches->settings->value(KAIDAN_SETTINGS_AUTH_JID_RESOURCE_PREFIX).toString());
+ setPassword(QByteArray::fromBase64(
+ m_caches->settings->value(KAIDAN_SETTINGS_AUTH_PASSWD).toString().toUtf8()
+ ));
// Use a default prefix for the JID's resource part if no prefix is already set.
if (creds.jidResourcePrefix.isEmpty())
setJidResourcePrefix(KAIDAN_JID_RESOURCE_DEFAULT_PREFIX);
creds.isFirstTry = false;
//
// Start ClientWorker on new thread
//
m_client = new ClientWorker(m_caches, this, enableLogging, app);
m_client->setCredentials(creds);
m_client->moveToThread(m_cltThrd);
connect(m_client, &ClientWorker::connectionErrorChanged, this, &Kaidan::setConnectionError);
connect(m_cltThrd, &QThread::started, m_client, &ClientWorker::main);
m_client->setObjectName("XmppClient");
m_cltThrd->start();
// account deletion
connect(this, &Kaidan::deleteAccountFromClient, m_client, &ClientWorker::deleteAccountFromClient);
connect(this, &Kaidan::deleteAccountFromClientAndServer, m_client, &ClientWorker::deleteAccountFromClientAndServer);
}
Kaidan::~Kaidan()
{
delete m_caches;
delete m_database;
s_instance = nullptr;
}
void Kaidan::start()
{
if (creds.jid.isEmpty() || creds.password.isEmpty())
emit newCredentialsNeeded();
else
mainConnect();
}
void Kaidan::mainConnect()
{
emit m_client->credentialsUpdated(creds);
emit m_client->connectRequested();
}
void Kaidan::mainDisconnect()
{
// disconnect the client if connected or connecting
if (connectionState != ConnectionState::StateDisconnected)
emit m_client->disconnectRequested();
}
void Kaidan::setConnectionState(QXmppClient::State state)
{
if (this->connectionState != static_cast(state)) {
this->connectionState = static_cast(state);
emit connectionStateChanged();
// Open the possibly cached URI when connected.
// This is needed because the XMPP URIs can't be opened when Kaidan is not connected.
if (connectionState == ConnectionState::StateConnected && !openUriCache.isEmpty()) {
// delay is needed because sometimes the RosterPage needs to be loaded first
QTimer::singleShot(300, [=] () {
emit xmppUriReceived(openUriCache);
openUriCache = "";
});
}
}
}
void Kaidan::setConnectionError(ClientWorker::ConnectionError error)
{
connectionError = error;
emit connectionErrorChanged();
}
void Kaidan::deleteCredentials()
{
// Delete the JID.
m_caches->settings->remove(KAIDAN_SETTINGS_AUTH_JID);
setJid(QString());
// Delete the password.
m_caches->settings->remove(KAIDAN_SETTINGS_AUTH_PASSWD);
setPassword(QString());
// Trigger the opening of the login page.
emit newCredentialsNeeded();
}
bool Kaidan::notificationsMuted(const QString &jid)
{
return m_caches->settings->value(QString("muted/") + jid, false).toBool();
}
void Kaidan::setNotificationsMuted(const QString &jid, bool muted)
{
m_caches->settings->setValue(QString("muted/") + jid, muted);
emit notificationsMutedChanged(jid);
}
void Kaidan::setJid(const QString &jid)
{
creds.jid = jid;
// credentials were modified -> first try
creds.isFirstTry = true;
emit jidChanged();
}
void Kaidan::setJidResourcePrefix(const QString &jidResourcePrefix)
{
// JID resource won't influence the authentication, so we don't need
// to set the first try flag and can save it.
creds.jidResourcePrefix = jidResourcePrefix;
m_caches->settings->setValue(KAIDAN_SETTINGS_AUTH_JID_RESOURCE_PREFIX, jidResourcePrefix);
emit jidResourcePrefixChanged();
}
void Kaidan::setPassword(const QString &password)
{
creds.password = password;
// credentials were modified -> first try
creds.isFirstTry = true;
emit passwordChanged();
}
quint8 Kaidan::getConnectionError() const
{
return static_cast(connectionError);
}
void Kaidan::addOpenUri(const QString &uri)
{
if (!QXmppUri::isXmppUri(uri))
return;
if (connectionState == ConnectionState::StateConnected) {
emit xmppUriReceived(uri);
} else {
//: The link is an XMPP-URI (i.e. 'xmpp:kaidan@muc.kaidan.im?join' for joining a chat)
emit passiveNotificationRequested(tr("The link will be opened after you have connected."));
openUriCache = uri;
}
}
void Kaidan::loginByUri(const QString &uri)
{
// input does not start with 'xmpp:'
if (!QXmppUri::isXmppUri(uri)) {
notifyLoginUriNotFound();
return;
}
// parse
QXmppUri parsedUri(uri);
// no JID provided
if (parsedUri.jid().isEmpty()) {
notifyLoginUriNotFound();
return;
}
setJid(parsedUri.jid());
// URI has no login action or no password
if (!parsedUri.hasAction(QXmppUri::Action::Login) || parsedUri.password().isEmpty()) {
// reset password
setPassword(QString());
emit passiveNotificationRequested(tr("No password found. Please enter it."));
return;
}
setPassword(parsedUri.password());
// try to connect
mainConnect();
}
void Kaidan::notifyLoginUriNotFound()
{
qWarning() << "[main]" << "No valid login URI found.";
emit passiveNotificationRequested(tr("No valid login QR code found."));
}
ClientWorker *Kaidan::getClient() const
{
return m_client;
}
RosterDb *Kaidan::rosterDb() const
{
return m_rosterDb;
}
MessageDb *Kaidan::messageDb() const
{
return m_msgDb;
}
Kaidan *Kaidan::instance()
{
return s_instance;
}
diff --git a/src/MessageDb.cpp b/src/MessageDb.cpp
index 3a8c169..1ad3346 100644
--- a/src/MessageDb.cpp
+++ b/src/MessageDb.cpp
@@ -1,316 +1,344 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#include "MessageDb.h"
// Kaidan
#include "Globals.h"
#include "Message.h"
#include "Utils.h"
// Qt
#include
#include
#include
#include
#include
MessageDb *MessageDb::s_instance = nullptr;
MessageDb::MessageDb(QObject *parent)
: QObject(parent)
{
Q_ASSERT(!MessageDb::s_instance);
s_instance = this;
connect(this, &MessageDb::fetchMessagesRequested,
this, &MessageDb::fetchMessages);
}
MessageDb::~MessageDb()
{
s_instance = nullptr;
}
MessageDb *MessageDb::instance()
{
return s_instance;
}
void MessageDb::parseMessagesFromQuery(QSqlQuery &query, QVector &msgs)
{
// get indexes of attributes
QSqlRecord rec = query.record();
int idxFrom = rec.indexOf("author");
int idxTo = rec.indexOf("recipient");
int idxStamp = rec.indexOf("timestamp");
int idxId = rec.indexOf("id");
int idxBody = rec.indexOf("message");
int idxIsSent = rec.indexOf("isSent");
int idxIsDelivered = rec.indexOf("isDelivered");
int idxMediaType = rec.indexOf("type");
int idxOutOfBandUrl = rec.indexOf("mediaUrl");
int idxMediaContentType = rec.indexOf("mediaContentType");
int idxMediaLocation = rec.indexOf("mediaLocation");
int idxMediaSize = rec.indexOf("mediaSize");
int idxMediaLastModified = rec.indexOf("mediaLastModified");
int idxIsEdited = rec.indexOf("edited");
int idxSpoilerHint = rec.indexOf("spoilerHint");
int idxIsSpoiler = rec.indexOf("isSpoiler");
while (query.next()) {
Message msg;
msg.setFrom(query.value(idxFrom).toString());
msg.setTo(query.value(idxTo).toString());
msg.setStamp(QDateTime::fromString(
query.value(idxStamp).toString(),
Qt::ISODate
));
msg.setId(query.value(idxId).toString());
msg.setBody(query.value(idxBody).toString());
msg.setIsSent(query.value(idxIsSent).toBool());
msg.setIsDelivered(query.value(idxIsDelivered).toBool());
msg.setMediaType(static_cast(query.value(idxMediaType).toInt()));
msg.setOutOfBandUrl(query.value(idxOutOfBandUrl).toString());
msg.setMediaContentType(query.value(idxMediaContentType).toString());
msg.setMediaLocation(query.value(idxMediaLocation).toString());
msg.setMediaSize(query.value(idxMediaSize).toLongLong());
msg.setMediaLastModified(QDateTime::fromMSecsSinceEpoch(
query.value(idxMediaLastModified).toLongLong()
));
msg.setIsEdited(query.value(idxIsEdited).toBool());
msg.setSpoilerHint(query.value(idxSpoilerHint).toString());
msg.setIsSpoiler(query.value(idxIsSpoiler).toBool());
msgs << msg;
}
}
QSqlRecord MessageDb::createUpdateRecord(const Message &oldMsg, const Message &newMsg)
{
QSqlRecord rec;
if (oldMsg.from() != newMsg.from())
rec.append(Utils::createSqlField("author", newMsg.from()));
if (oldMsg.to() != newMsg.to())
rec.append(Utils::createSqlField("recipient", newMsg.to()));
if (oldMsg.stamp() != newMsg.stamp())
rec.append(Utils::createSqlField(
"timestamp",
newMsg.stamp().toString(Qt::ISODate)
));
if (oldMsg.id() != newMsg.id()) {
// TODO: remove as soon as 'NOT NULL' was removed from id column
if (newMsg.id().isEmpty())
rec.append(Utils::createSqlField("id", QStringLiteral(" ")));
else
rec.append(Utils::createSqlField("id", newMsg.id()));
}
if (oldMsg.body() != newMsg.body())
rec.append(Utils::createSqlField("message", newMsg.body()));
if (oldMsg.isSent() != newMsg.isSent())
rec.append(Utils::createSqlField("isSent", newMsg.isSent()));
if (oldMsg.isDelivered() != newMsg.isDelivered())
rec.append(Utils::createSqlField("isDelivered", newMsg.isDelivered()));
if (oldMsg.mediaType() != newMsg.mediaType())
rec.append(Utils::createSqlField("type", int(newMsg.mediaType())));
if (oldMsg.outOfBandUrl() != newMsg.outOfBandUrl())
rec.append(Utils::createSqlField("mediaUrl", newMsg.outOfBandUrl()));
if (oldMsg.mediaContentType() != newMsg.mediaContentType())
rec.append(Utils::createSqlField(
"mediaContentType",
newMsg.mediaContentType()
));
if (oldMsg.mediaLocation() != newMsg.mediaLocation())
rec.append(Utils::createSqlField(
"mediaLocation",
newMsg.mediaLocation()
));
if (oldMsg.mediaSize() != newMsg.mediaSize())
rec.append(Utils::createSqlField("mediaSize", newMsg.mediaSize()));
if (oldMsg.mediaLastModified() != newMsg.mediaLastModified())
rec.append(Utils::createSqlField(
"mediaLastModified",
newMsg.mediaLastModified().toMSecsSinceEpoch()
));
if (oldMsg.isEdited() != newMsg.isEdited())
rec.append(Utils::createSqlField("edited", newMsg.isEdited()));
if (oldMsg.spoilerHint() != newMsg.spoilerHint())
rec.append(Utils::createSqlField("spoilerHint", newMsg.spoilerHint()));
if (oldMsg.isSpoiler() != newMsg.isSpoiler())
rec.append(Utils::createSqlField("isSpoiler", newMsg.isSpoiler()));
return rec;
}
void MessageDb::fetchMessages(const QString &user1, const QString &user2, int index)
{
QSqlQuery query(QSqlDatabase::database(DB_CONNECTION));
query.setForwardOnly(true);
QMap bindValues;
bindValues[":user1"] = user1;
bindValues[":user2"] = user2;
bindValues[":index"] = index;
bindValues[":limit"] = DB_MSG_QUERY_LIMIT;
Utils::execQuery(
query,
"SELECT * FROM Messages "
"WHERE (author = :user1 AND recipient = :user2) OR "
"(author = :user2 AND recipient = :user1) "
"ORDER BY timestamp DESC "
"LIMIT :index, :limit",
bindValues
);
QVector messages;
parseMessagesFromQuery(query, messages);
emit messagesFetched(messages);
}
+Message MessageDb::fetchLastMessage(const QString &user1, const QString &user2)
+{
+ QSqlQuery query(QSqlDatabase::database(DB_CONNECTION));
+ query.setForwardOnly(true);
+
+ QMap bindValues = {
+ { QStringLiteral(":user1"), user1 },
+ { QStringLiteral(":user2"), user2 },
+ };
+
+ Utils::execQuery(
+ query,
+ "SELECT * FROM Messages "
+ "WHERE (author = :user1 AND recipient = :user2) OR "
+ "(author = :user2 AND recipient = :user1) "
+ "ORDER BY timestamp DESC "
+ "LIMIT 1",
+ bindValues
+ );
+
+ QVector messages;
+ parseMessagesFromQuery(query, messages);
+
+ if (!messages.isEmpty())
+ return messages.first();
+ return {};
+}
+
void MessageDb::addMessage(const Message &msg)
{
QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION);
QSqlRecord record = db.record(DB_TABLE_MESSAGES);
record.setValue("author", msg.from());
record.setValue("recipient", msg.to());
record.setValue("timestamp", msg.stamp().toString(Qt::ISODate));
record.setValue("message", msg.body());
record.setValue("id", msg.id().isEmpty() ? " " : msg.id());
record.setValue("isSent", msg.isSent());
record.setValue("isDelivered", msg.isDelivered());
record.setValue("type", int(msg.mediaType()));
record.setValue("edited", msg.isEdited());
record.setValue("isSpoiler", msg.isSpoiler());
record.setValue("spoilerHint", msg.spoilerHint());
record.setValue("mediaUrl", msg.outOfBandUrl());
record.setValue("mediaContentType", msg.mediaContentType());
record.setValue("mediaLocation", msg.mediaLocation());
record.setValue("mediaSize", msg.mediaSize());
record.setValue("mediaLastModified", msg.mediaLastModified().toMSecsSinceEpoch());
QSqlQuery query(db);
Utils::execQuery(query, db.driver()->sqlStatement(
QSqlDriver::InsertStatement,
DB_TABLE_MESSAGES,
record,
false
));
}
void MessageDb::removeMessage(const QString &id)
{
QSqlQuery query(QSqlDatabase::database(DB_CONNECTION));
Utils::execQuery(
query,
"DELETE FROM Messages WHERE id = ?",
QVector() << id
);
}
void MessageDb::removeAllMessages()
{
QSqlQuery query(QSqlDatabase::database(DB_CONNECTION));
Utils::execQuery(query, "DELETE FROM Messages");
}
void MessageDb::updateMessage(const QString &id,
const std::function &updateMsg)
{
// load current message item from db
QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION);
QSqlQuery query(db);
query.setForwardOnly(true);
Utils::execQuery(
query,
"SELECT * FROM Messages WHERE id = ? LIMIT 1",
QVector() << id
);
QVector msgs;
parseMessagesFromQuery(query, msgs);
// update loaded item
if (!msgs.isEmpty()) {
Message msg = msgs.first();
updateMsg(msg);
// replace old message with updated one, if message has changed
if (msgs.first() != msg) {
// create an SQL record with only the differences
QSqlRecord rec = createUpdateRecord(msgs.first(), msg);
Utils::execQuery(
query,
db.driver()->sqlStatement(
QSqlDriver::UpdateStatement,
DB_TABLE_MESSAGES,
rec,
false
) +
Utils::simpleWhereStatement(db.driver(), "id", id)
);
}
}
}
void MessageDb::updateMessageRecord(const QString &id,
const QSqlRecord &updateRecord)
{
QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION);
QSqlQuery query(db);
Utils::execQuery(
query,
db.driver()->sqlStatement(
QSqlDriver::UpdateStatement,
DB_TABLE_MESSAGES,
updateRecord,
false
) +
Utils::simpleWhereStatement(db.driver(), "id", id)
);
}
void MessageDb::setMessageAsSent(const QString &msgId)
{
QSqlRecord rec;
rec.append(Utils::createSqlField("isSent", true));
updateMessageRecord(msgId, rec);
}
void MessageDb::setMessageAsDelivered(const QString &msgId)
{
QSqlRecord rec;
rec.append(Utils::createSqlField("isDelivered", true));
updateMessageRecord(msgId, rec);
}
diff --git a/src/MessageDb.h b/src/MessageDb.h
index f14841e..878471f 100644
--- a/src/MessageDb.h
+++ b/src/MessageDb.h
@@ -1,144 +1,149 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#ifndef MESSAGEDB_H
#define MESSAGEDB_H
#include
#include
class Message;
class QSqlQuery;
class QSqlRecord;
/**
* @class The MessageDb is used to query the 'messages' database table. It's used by the
* MessageModel to load messages and by the MessageHandler to insert messages.
*
* All queries must be executed only after the Kaidan SQL connection has been opened in
* the Database class.
*/
class MessageDb : public QObject
{
Q_OBJECT
public:
explicit MessageDb(QObject *parent = nullptr);
~MessageDb();
static MessageDb *instance();
/**
* Parses a list of messages from a SELECT query.
*/
static void parseMessagesFromQuery(QSqlQuery &query, QVector &msgs);
/**
* Creates an @c QSqlRecord for updating an old message to a new message.
*
* @param oldMsg Full message as it is currently saved
* @param newMsg Full message as it should be after the update query ran.
*/
static QSqlRecord createUpdateRecord(const Message &oldMsg,
const Message &newMsg);
signals:
/**
* Can be used to triggerd fetchMessages()
*/
void fetchMessagesRequested(const QString &user1,
const QString &user2,
int index);
/**
* Emitted, when new messages have been fetched
*/
void messagesFetched(const QVector &messages);
public slots:
/**
* @brief Fetches more entries from the database and emits messagesFetched() with
* the results.
*
* @param user1 Messages are from or to this JID.
* @param user2 Messages are from or to this JID.
* @param index Number of entries to be skipped, used for paging.
*/
void fetchMessages(const QString &user1,
const QString &user2,
int index);
+ /**
+ * Fetches the last message and returns it.
+ */
+ Message fetchLastMessage(const QString &user1, const QString &user2);
+
/**
* Adds a message to the database.
*/
void addMessage(const Message &msg);
/**
* Deletes a message from the database.
*/
void removeMessage(const QString &id);
/**
* Removes all messages from the database.
*/
void removeAllMessages();
/**
* Loads a message, runs the update lambda and writes it to the DB again.
*
* @param updateMsg Function that changes the message
*/
void updateMessage(const QString &id,
const std::function &updateMsg);
/**
* Updates message by @c UPDATE record: This means it doesn't load the message
* from the database and writes it again, but executes an UPDATE query.
*
* @param updateRecord
*/
void updateMessageRecord(const QString &id,
const QSqlRecord &updateRecord);
/**
* Marks a message as sent using an UPDATE query.
*/
void setMessageAsSent(const QString &msgId);
/**
* Marks a message as delivered using an UPDATE query.
*/
void setMessageAsDelivered(const QString &msgId);
private:
static MessageDb *s_instance;
};
#endif // MESSAGEDB_H
diff --git a/src/MessageHandler.cpp b/src/MessageHandler.cpp
index bebcd91..812edca 100644
--- a/src/MessageHandler.cpp
+++ b/src/MessageHandler.cpp
@@ -1,296 +1,292 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#include "MessageHandler.h"
// Qt
#include
#include
#include
// QXmpp
#include
#include
#include
#include
#include
// Kaidan
#include "Kaidan.h"
#include "Message.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)
{
connect(client, &QXmppClient::messageReceived, this, &MessageHandler::handleMessage);
connect(kaidan, &Kaidan::sendMessage, this, &MessageHandler::sendMessage);
connect(kaidan, &Kaidan::correctMessage, this, &MessageHandler::correctMessage);
client->addExtension(&receiptManager);
connect(&receiptManager, &QXmppMessageReceiptManager::messageDelivered,
this, [=] (const QString&, const QString &id) {
emit model->setMessageAsDeliveredRequested(id);
});
carbonManager = new QXmppCarbonManager();
client->addExtension(carbonManager);
// messages sent to our account (forwarded from another client)
connect(carbonManager, &QXmppCarbonManager::messageReceived,
client, &QXmppClient::messageReceived);
// messages sent from our account (but another client)
connect(carbonManager, &QXmppCarbonManager::messageSent,
client, &QXmppClient::messageReceived);
// carbons discovery
auto *discoManager = client->findExtension();
if (!discoManager)
return;
connect(discoManager, &QXmppDiscoveryManager::infoReceived,
this, &MessageHandler::handleDiscoInfo);
}
MessageHandler::~MessageHandler()
{
delete carbonManager;
}
void MessageHandler::handleMessage(const QXmppMessage &msg)
{
if (msg.body().isEmpty() || msg.type() == QXmppMessage::Error)
return;
Message message;
message.setFrom(QXmppUtils::jidToBareJid(msg.from()));
message.setTo(QXmppUtils::jidToBareJid(msg.to()));
message.setSentByMe(msg.from() == client->configuration().jidBare());
message.setId(msg.id());
message.setBody(msg.body());
message.setMediaType(MessageType::MessageText); // default to text message without media
#if (QXMPP_VERSION) >= QT_VERSION_CHECK(1, 1, 0)
message.setIsSpoiler(msg.isSpoiler());
message.setSpoilerHint(msg.spoilerHint());
#else
for (const QXmppElement &extension : msg.extensions()) {
if (extension.tagName() == "spoiler" &&
extension.attribute("xmlns") == NS_SPOILERS) {
message.setIsSpoiler(true);
message.setSpoilerHint(extension.value());
break;
}
}
#endif
// check if message contains a link and also check out of band url
QStringList bodyWords = message.body().split(" ");
bodyWords.prepend(msg.outOfBandUrl());
for (const QString &word : qAsConst(bodyWords)) {
if (!MediaUtils::isHttp(word) && !MediaUtils::isGeoLocation(word)) {
continue;
}
// check message type by file name in link
// This is hacky, but needed without SIMS or an additional HTTP request.
// Also, this can be useful when a user manually posts an HTTP url.
const QUrl url(word);
const QMimeType mimeType = MediaUtils::mimeType(url);
const MessageType messageType = MediaUtils::messageType(mimeType);
switch (messageType) {
case MessageType::MessageImage:
case MessageType::MessageAudio:
case MessageType::MessageVideo:
case MessageType::MessageDocument:
case MessageType::MessageFile:
case MessageType::MessageGeoLocation:
message.setMediaType(messageType);
if (messageType == MessageType::MessageGeoLocation) {
message.setMediaLocation(url.toEncoded());
}
message.setMediaContentType(mimeType.name());
message.setOutOfBandUrl(url.toEncoded());
break;
case MessageType::MessageText:
case MessageType::MessageUnknown:
continue;
}
break; // we can only handle one link
}
// get possible delay (timestamp)
message.setStamp((msg.stamp().isNull() || !msg.stamp().isValid())
? QDateTime::currentDateTimeUtc()
: msg.stamp().toUTC());
// save the message to the database
// in case of message correction, replace old message
if (msg.replaceId().isEmpty()) {
emit model->addMessageRequested(message);
} else {
message.setIsEdited(true);
message.setId(QString());
emit model->updateMessageRequested(msg.replaceId(), [=] (Message &m) {
// replace completely
m = message;
});
}
// Send a message notification
// The contact can differ if the message is really from a contact or just
// a forward of another of the user's clients.
QString contactJid = message.sentByMe() ? message.to() : message.from();
// resolve user-defined name of this JID
QString contactName = client->rosterManager().getRosterEntry(contactJid).name();
if (contactName.isEmpty())
contactName = contactJid;
if (!message.sentByMe())
Notifications::sendMessageNotification(contactJid, contactName, msg.body());
// TODO: Move back following call to RosterManager::handleMessage when spoiler
// messages are implemented in QXmpp
const QString lastMessage =
message.isSpoiler() ? message.spoilerHint().isEmpty() ? tr("Spoiler")
: message.spoilerHint()
: msg.body();
- emit kaidan->getRosterModel()->updateItemRequested(
- contactJid,
- [=] (RosterItem &item) {
- item.setLastMessage(lastMessage);
- }
- );
+
+ emit kaidan->getRosterModel()->setLastMessageRequested(contactJid, lastMessage);
}
void MessageHandler::sendMessage(const QString& toJid,
const QString& body,
bool isSpoiler,
const QString& spoilerHint)
{
// TODO: Add offline message cache and send when connnected again
if (client->state() != QXmppClient::ConnectedState) {
emit kaidan->passiveNotificationRequested(
tr("Could not send message, as a result of not being connected.")
);
qWarning() << "[client] [MessageHandler] Could not send message, as a result of "
"not being connected.";
return;
}
Message msg;
msg.setFrom(client->configuration().jidBare());
msg.setTo(toJid);
msg.setBody(body);
msg.setId(QXmppUtils::generateStanzaHash(28));
msg.setReceiptRequested(true);
msg.setSentByMe(true);
msg.setMediaType(MessageType::MessageText); // text message without media
msg.setStamp(QDateTime::currentDateTimeUtc());
if (isSpoiler) {
msg.setIsSpoiler(isSpoiler);
msg.setSpoilerHint(spoilerHint);
// parsing/serialization of spoilers isn't implemented in QXmpp
QXmppElementList extensions = msg.extensions();
QXmppElement spoiler = QXmppElement();
spoiler.setTagName("spoiler");
spoiler.setValue(msg.spoilerHint());
spoiler.setAttribute("xmlns", NS_SPOILERS);
extensions.append(spoiler);
msg.setExtensions(extensions);
} else if (MediaUtils::isGeoLocation(msg.body())) {
const QUrl url(msg.body());
const QMimeType mimeType = MediaUtils::mimeType(url);
const MessageType messageType = MediaUtils::messageType(mimeType);
msg.setMediaType(messageType);
msg.setMediaLocation(msg.body());
msg.setMediaContentType(mimeType.name());
msg.setOutOfBandUrl(msg.body());
}
emit model->addMessageRequested(msg);
if (client->sendPacket(static_cast(msg)))
emit model->setMessageAsSentRequested(msg.id());
else
emit kaidan->passiveNotificationRequested(tr("Message could not be sent."));
// TODO: handle error
}
void MessageHandler::correctMessage(const QString& toJid,
const QString& msgId,
const QString& body)
{
// TODO: load old message from model and put everything into the new message
// instead of only the new body
// TODO: Add offline message cache and send when connnected again
if (client->state() != QXmppClient::ConnectedState) {
emit kaidan->passiveNotificationRequested(
tr("Could not correct message, as a result of not being connected.")
);
qWarning() << "[client] [MessageHandler] Could not correct message, as a result of "
"not being connected.";
return;
}
Message msg;
msg.setFrom(client->configuration().jidBare());
msg.setTo(toJid);
msg.setId(QXmppUtils::generateStanzaHash(28));
msg.setBody(body);
msg.setReceiptRequested(true);
msg.setSentByMe(true);
msg.setMediaType(MessageType::MessageText); // text message without media
msg.setIsEdited(true);
msg.setReplaceId(msgId);
emit model->updateMessageRequested(msgId, [=] (Message &msg) {
msg.setBody(body);
});
if (client->sendPacket(msg))
emit model->setMessageAsSentRequested(msg.id());
else
emit kaidan->passiveNotificationRequested(
tr("Message correction was not successful."));
}
void MessageHandler::handleDiscoInfo(const QXmppDiscoveryIq &info)
{
if (info.from() != client->configuration().domain())
return;
// enable carbons, if feature found
if (info.features().contains(NS_CARBONS))
carbonManager->setCarbonsEnabled(true);
}
diff --git a/src/RosterDb.cpp b/src/RosterDb.cpp
index e29957c..fab9a8e 100644
--- a/src/RosterDb.cpp
+++ b/src/RosterDb.cpp
@@ -1,271 +1,269 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#include "RosterDb.h"
// Kaidan
#include "Database.h"
#include "Globals.h"
#include "Utils.h"
+#include "RosterItem.h"
+#include "Message.h"
+#include "MessageDb.h"
// Qt
#include
#include
#include
#include
#include
RosterDb *RosterDb::s_instance = nullptr;
RosterDb::RosterDb(Database *db, QObject *parent)
: QObject(parent),
m_db(db)
{
Q_ASSERT(!RosterDb::s_instance);
s_instance = this;
connect(this, &RosterDb::fetchItemsRequested, this, &RosterDb::fetchItems);
}
RosterDb::~RosterDb()
{
s_instance = nullptr;
}
RosterDb *RosterDb::instance()
{
return s_instance;
}
void RosterDb::parseItemsFromQuery(QSqlQuery &query, QVector &items)
{
QSqlRecord rec = query.record();
int idxJid = rec.indexOf("jid");
int idxName = rec.indexOf("name");
- int idxLastExchanged = rec.indexOf("lastExchanged");
int idxUnreadMessages = rec.indexOf("unreadMessages");
- int idxLastMessage = rec.indexOf("lastMessage");
while (query.next()) {
RosterItem item;
item.setJid(query.value(idxJid).toString());
item.setName(query.value(idxName).toString());
- item.setLastExchanged(QDateTime::fromString(
- query.value(idxLastExchanged).toString(),
- Qt::ISODateWithMs
- ));
item.setUnreadMessages(query.value(idxUnreadMessages).toInt());
- item.setLastMessage(query.value(idxLastMessage).toString());
items << item;
}
}
QSqlRecord RosterDb::createUpdateRecord(const RosterItem &oldItem, const RosterItem &newItem)
{
QSqlRecord rec;
if (oldItem.jid() != newItem.jid())
rec.append(Utils::createSqlField("jid", newItem.jid()));
if (oldItem.name() != newItem.name())
rec.append(Utils::createSqlField("name", oldItem.name()));
- if (oldItem.lastMessage() != newItem.lastMessage())
- rec.append(Utils::createSqlField("lastMessage", newItem.lastMessage()));
- if (oldItem.lastExchanged() != newItem.lastExchanged())
- rec.append(Utils::createSqlField(
- "lastExchanged",
- newItem.lastExchanged().toString(Qt::ISODateWithMs)
- ));
if (oldItem.unreadMessages() != newItem.unreadMessages())
rec.append(Utils::createSqlField(
"unreadMessages",
newItem.unreadMessages()
));
return rec;
}
void RosterDb::addItem(const RosterItem &item)
{
addItems(QVector() << item);
}
void RosterDb::addItems(const QVector &items)
{
QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION);
m_db->transaction();
QSqlQuery query(db);
Utils::prepareQuery(query, db.driver()->sqlStatement(
QSqlDriver::InsertStatement,
DB_TABLE_ROSTER,
db.record(DB_TABLE_ROSTER),
true
));
for (const auto &item : items) {
query.addBindValue(item.jid());
query.addBindValue(item.name());
- query.addBindValue(item.lastExchanged().toString(Qt::ISODateWithMs));
+ query.addBindValue(QStringLiteral("")); // lastExchanged (NOT NULL)
query.addBindValue(item.unreadMessages());
- query.addBindValue(item.lastMessage());
+ query.addBindValue(QString()); // lastMessage
Utils::execQuery(query);
}
m_db->commit();
}
void RosterDb::removeItem(const QString &jid)
{
QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION);
QSqlQuery query(db);
Utils::execQuery(
query,
"DELETE FROM Roster WHERE jid = ?",
QVector() << jid
);
}
void RosterDb::updateItem(const QString &jid,
const std::function &updateItem)
{
// load current roster item from db
QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION);
QSqlQuery query(db);
query.setForwardOnly(true);
Utils::execQuery(
query,
"SELECT * FROM Roster WHERE jid = ? LIMIT 1",
QVector() << jid
);
QVector items;
parseItemsFromQuery(query, items);
// update loaded item
if (!items.isEmpty()) {
RosterItem item = items.first();
updateItem(item);
// replace old item with updated one, if item has changed
if (items.first() != item) {
// create an SQL record with only the differences
QSqlRecord rec = createUpdateRecord(items.first(), item);
+ if (rec.isEmpty())
+ return;
+
Utils::execQuery(
query,
db.driver()->sqlStatement(
QSqlDriver::UpdateStatement,
DB_TABLE_ROSTER,
rec,
false
) +
Utils::simpleWhereStatement(db.driver(), "jid", jid)
);
}
}
}
void RosterDb::replaceItems(const QHash &items)
{
// load current items
QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION);
QSqlQuery query(db);
query.setForwardOnly(true);
Utils::execQuery(query, "SELECT * FROM Roster");
QVector currentItems;
parseItemsFromQuery(query, currentItems);
m_db->transaction();
QSet newJids = items.keys().toSet();
for (const auto &oldItem : qAsConst(currentItems)) {
// We will remove the already existing JIDs, so we get a set of JIDs that
// are completely new.
//
// By calling remove(), we also find out whether the JID is already
// existing or not.
if (newJids.remove(oldItem.jid())) {
// item is also included in newJids -> update
// name is (currently) the only attribute that is defined by the
// XMPP roster and so could cause a change
if (oldItem.name() != items[oldItem.jid()].name())
setItemName(oldItem.jid(), items[oldItem.jid()].name());
} else {
// item is not included in newJids -> delete
removeItem(oldItem.jid());
}
}
// now add the completely new JIDs
for (const QString &jid : newJids)
addItem(items[jid]);
m_db->commit();
}
void RosterDb::setItemName(const QString &jid, const QString &name)
{
QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION);
QSqlQuery query(db);
QSqlRecord rec;
rec.append(Utils::createSqlField("name", name));
Utils::execQuery(
query,
db.driver()->sqlStatement(
QSqlDriver::UpdateStatement,
DB_TABLE_ROSTER,
rec,
false
) +
Utils::simpleWhereStatement(db.driver(), "jid", jid)
);
}
void RosterDb::clearAll()
{
QSqlQuery query(QSqlDatabase::database(DB_CONNECTION));
Utils::execQuery(query, "DELETE FROM Roster");
}
-void RosterDb::fetchItems()
+void RosterDb::fetchItems(const QString &accountId)
{
QSqlQuery query(QSqlDatabase::database(DB_CONNECTION));
query.setForwardOnly(true);
Utils::execQuery(query, "SELECT * FROM Roster");
QVector items;
parseItemsFromQuery(query, items);
+ for (auto &item : items) {
+ Message lastMessage = MessageDb::instance()->fetchLastMessage(accountId, item.jid());
+ item.setLastExchanged(lastMessage.stamp());
+ item.setLastMessage(lastMessage.body());
+ }
+
emit itemsFetched(items);
}
diff --git a/src/RosterDb.h b/src/RosterDb.h
index 85c0aad..2609366 100644
--- a/src/RosterDb.h
+++ b/src/RosterDb.h
@@ -1,88 +1,88 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#ifndef ROSTERDB_H
#define ROSTERDB_H
// C++
#include
// Qt
#include
class QSqlQuery;
class QSqlRecord;
// Kaidan
-#include "RosterItem.h"
+class RosterItem;
class Database;
class RosterDb : public QObject
{
Q_OBJECT
public:
RosterDb(Database *db, QObject *parent = nullptr);
~RosterDb();
static RosterDb *instance();
static void parseItemsFromQuery(QSqlQuery &query, QVector &items);
/**
* Creates an @c QSqlRecord for updating an old item to a new item.
*
* @param oldMsg Full item as it is currently saved
* @param newMsg Full item as it should be after the update query ran.
*/
static QSqlRecord createUpdateRecord(const RosterItem &oldItem,
const RosterItem &newItem);
signals:
- void fetchItemsRequested();
+ void fetchItemsRequested(const QString &accountId);
void itemsFetched(const QVector &items);
public slots:
void addItem(const RosterItem &item);
void addItems(const QVector &items);
void removeItem(const QString &jid);
void updateItem(const QString &jid,
const std::function &updateItem);
void replaceItems(const QHash &items);
void setItemName(const QString &jid, const QString &name);
void clearAll();
private slots:
- void fetchItems();
+ void fetchItems(const QString &accountId);
private:
Database *m_db;
static RosterDb *s_instance;
};
#endif // ROSTERDB_H
diff --git a/src/RosterManager.cpp b/src/RosterManager.cpp
index 83ee07e..0373a22 100644
--- a/src/RosterManager.cpp
+++ b/src/RosterManager.cpp
@@ -1,218 +1,209 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#include "RosterManager.h"
// Kaidan
#include "ClientWorker.h"
#include "Globals.h"
#include "Kaidan.h"
#include "VCardManager.h"
// QXmpp
#include
#include
#include
RosterManager::RosterManager(Kaidan *kaidan,
QXmppClient *client,
RosterModel *model,
AvatarFileStorage *avatarStorage,
VCardManager *vCardManager,
QObject *parent)
: QObject(parent),
kaidan(kaidan),
client(client),
model(model),
avatarStorage(avatarStorage),
vCardManager(vCardManager),
manager(client->rosterManager())
{
connect(&manager, &QXmppRosterManager::rosterReceived,
this, &RosterManager::populateRoster);
connect(&manager, &QXmppRosterManager::itemAdded,
this, [this, vCardManager, model] (const QString &jid) {
emit model->addItemRequested(RosterItem(manager.getRosterEntry(jid)));
vCardManager->fetchVCard(jid);
});
connect(&manager, &QXmppRosterManager::itemChanged,
this, [this, model] (const QString &jid) {
emit model->updateItemRequested(m_chatPartner, [=] (RosterItem &item) {
item.setName(manager.getRosterEntry(jid).name());
});
});
connect(&manager, &QXmppRosterManager::itemRemoved, model, &RosterModel::removeItemRequested);
connect(&manager, &QXmppRosterManager::subscriptionReceived,
this, [kaidan] (const QString &jid) {
// emit signal to ask user
emit kaidan->subscriptionRequestReceived(jid, QString());
});
connect(kaidan, &Kaidan::subscriptionRequestAnswered,
this, [=] (QString jid, bool accepted) {
if (accepted) {
manager.acceptSubscription(jid);
// do not send a subscription request if both users have already subscribed
// each others presence
if (manager.getRosterEntry(jid).subscriptionType() != QXmppRosterIq::Item::Both)
manager.subscribe(jid);
} else {
manager.refuseSubscription(jid);
}
});
// update local copy of chat partner
connect(kaidan->getMessageModel(), &MessageModel::chatPartnerChanged,
this, [=] (const QString &jid) {
m_chatPartner = jid;
}
);
// user actions
connect(kaidan, &Kaidan::addContact, this, &RosterManager::addContact);
connect(kaidan, &Kaidan::removeContact, this, &RosterManager::removeContact);
connect(kaidan, &Kaidan::renameContact, this, &RosterManager::renameContact);
connect(kaidan, &Kaidan::sendMessage, this, &RosterManager::handleSendMessage);
connect(client, &QXmppClient::messageReceived, this, &RosterManager::handleMessage);
}
void RosterManager::populateRoster()
{
qDebug() << "[client] [RosterManager] Populating roster";
// create a new list of contacts
QHash items;
const QStringList bareJids = manager.getRosterBareJids();
const auto currentTime = QDateTime::currentDateTimeUtc();
for (const auto &jid : bareJids) {
items[jid] = RosterItem(manager.getRosterEntry(jid), currentTime);
if (avatarStorage->getHashOfJid(jid).isEmpty())
vCardManager->fetchVCard(jid);
}
// replace current contacts with new ones from server
emit model->replaceItemsRequested(items);
}
void RosterManager::addContact(const QString &jid, const QString &name, const QString &msg)
{
if (client->state() == QXmppClient::ConnectedState) {
manager.addItem(jid, name);
manager.subscribe(jid, msg);
} else {
emit kaidan->passiveNotificationRequested(
tr("Could not add contact, as a result of not being connected.")
);
qWarning() << "[client] [RosterManager] Could not add contact, as a result of "
"not being connected.";
}
}
void RosterManager::removeContact(const QString &jid)
{
if (client->state() == QXmppClient::ConnectedState) {
manager.unsubscribe(jid);
manager.removeItem(jid);
} else {
emit kaidan->passiveNotificationRequested(
tr("Could not remove contact, as a result of not being connected.")
);
qWarning() << "[client] [RosterManager] Could not remove contact, as a result of "
"not being connected.";
}
}
void RosterManager::renameContact(const QString &jid, const QString &newContactName)
{
if (client->state() == QXmppClient::ConnectedState) {
manager.renameItem(jid, newContactName);
} else {
emit kaidan->passiveNotificationRequested(
tr("Could not rename contact, as a result of not being connected.")
);
qWarning() << "[client] [RosterManager] Could not rename contact, as a result of "
"not being connected.";
}
}
void RosterManager::handleSendMessage(const QString &jid, const QString &message,
bool isSpoiler, const QString &spoilerHint)
{
if (client->state() == QXmppClient::ConnectedState) {
// update roster item
const QString lastMessage =
isSpoiler ? spoilerHint.isEmpty() ? tr("Spoiler")
: spoilerHint
: message;
// sorting order in contact list
- const QDateTime dateTime = QDateTime::currentDateTimeUtc();
-
- emit model->updateItemRequested(jid,
- [=] (RosterItem &item) {
- item.setLastMessage(lastMessage);
- item.setLastExchanged(dateTime);
- });
+ emit model->setLastExchangedRequested(jid, QDateTime::currentDateTimeUtc());
+ emit model->setLastMessageRequested(jid, lastMessage);
}
}
void RosterManager::handleMessage(const QXmppMessage &msg)
{
- if (msg.body().isEmpty() || msg.type() == QXmppMessage::Error)
+ if (msg.body().isEmpty() || msg.type() == QXmppMessage::Error)
return;
// msg.from() can be our JID, if it's a carbon/forward from another client
QString fromJid = QXmppUtils::jidToBareJid(msg.from());
bool sentByMe = fromJid == client->configuration().jidBare();
QString contactJid = sentByMe ? QXmppUtils::jidToBareJid(msg.to())
: fromJid;
// update last exchanged datetime (sorting order in contact list)
- const QDateTime dateTime = QDateTime::currentDateTimeUtc();
+ emit model->setLastExchangedRequested(contactJid, QDateTime::currentDateTimeUtc());
// update unread message counter, if chat is not active
if (sentByMe) {
// if we sent a message (with another device), reset counter
- emit model->updateItemRequested(contactJid,
- [dateTime] (RosterItem &item) {
- item.setLastExchanged(dateTime);
+ emit model->updateItemRequested(contactJid, [](RosterItem &item) {
item.setUnreadMessages(0);
});
} else if (m_chatPartner != contactJid) {
- emit model->updateItemRequested(contactJid,
- [dateTime] (RosterItem &item) {
- item.setLastExchanged(dateTime);
+ emit model->updateItemRequested(contactJid, [](RosterItem &item) {
item.setUnreadMessages(item.unreadMessages() + 1);
});
}
}
diff --git a/src/RosterModel.cpp b/src/RosterModel.cpp
index 99caf50..24b90ba 100644
--- a/src/RosterModel.cpp
+++ b/src/RosterModel.cpp
@@ -1,244 +1,278 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#include "RosterModel.h"
// Kaidan
#include "RosterDb.h"
#include "MessageModel.h"
+#include "Kaidan.h"
// C++
#include
// Qt
#include
#include
#include
#include
RosterModel::RosterModel(RosterDb *rosterDb, QObject *parent)
: QAbstractListModel(parent),
rosterDb(rosterDb)
{
connect(rosterDb, &RosterDb::itemsFetched,
this, &RosterModel::handleItemsFetched);
connect(this, &RosterModel::addItemRequested, this, &RosterModel::addItem);
connect(this, &RosterModel::addItemRequested, rosterDb, &RosterDb::addItem);
connect(this, &RosterModel::removeItemRequested,
this, &RosterModel::removeItem);
connect(this, &RosterModel::removeItemRequested,
rosterDb, &RosterDb::removeItem);
connect(this, &RosterModel::updateItemRequested,
this, &RosterModel::updateItem);
connect(this, &RosterModel::updateItemRequested,
rosterDb, &RosterDb::updateItem);
connect(this, &RosterModel::replaceItemsRequested,
this, &RosterModel::replaceItems);
connect(this, &RosterModel::replaceItemsRequested,
rosterDb, &RosterDb::replaceItems);
- emit rosterDb->fetchItemsRequested();
+ // This is only done in the model, the database is updated automatically by the new
+ // messages:
+ connect(this, &RosterModel::setLastMessageRequested,
+ this, &RosterModel::setLastMessage);
+ connect(this, &RosterModel::setLastExchangedRequested,
+ this, &RosterModel::setLastExchanged);
+
+ connect(Kaidan::instance(), &Kaidan::jidChanged, this, [=]() {
+ beginResetModel();
+ m_items.clear();
+ endResetModel();
+
+ emit rosterDb->fetchItemsRequested(Kaidan::instance()->getJid());
+ });
}
void RosterModel::setMessageModel(MessageModel *model)
{
connect(model, &MessageModel::chatPartnerChanged,
this, [=] (const QString &chatPartner) {
// reset unread message counter
emit updateItemRequested(chatPartner,
[] (RosterItem &item) {
item.setUnreadMessages(0);
});
});
}
bool RosterModel::isEmpty() const
{
return m_items.isEmpty();
}
int RosterModel::rowCount(const QModelIndex&) const
{
return m_items.length();
}
QHash RosterModel::roleNames() const
{
QHash roles;
roles[JidRole] = "jid";
roles[NameRole] = "name";
roles[LastExchangedRole] = "lastExchanged";
roles[UnreadMessagesRole] = "unreadMessages";
roles[LastMessageRole] = "lastMessage";
return roles;
}
QVariant RosterModel::data(const QModelIndex &index, int role) const
{
if (!hasIndex(index.row(), index.column(), index.parent())) {
qWarning() << "Could not get data from roster model." << index << role;
return {};
}
switch (role) {
case JidRole:
return m_items.at(index.row()).jid();
case NameRole:
return m_items.at(index.row()).name();
case LastExchangedRole:
return m_items.at(index.row()).lastExchanged();
case UnreadMessagesRole:
return m_items.at(index.row()).unreadMessages();
case LastMessageRole:
return m_items.at(index.row()).lastMessage();
}
return {};
}
void RosterModel::handleItemsFetched(const QVector &items)
{
beginResetModel();
m_items = items;
- std::sort(
- m_items.begin(),
- m_items.end(),
- [] (const RosterItem &a, const RosterItem &b) {
- return a < b;
- }
- );
+ std::sort(m_items.begin(), m_items.end());
endResetModel();
}
void RosterModel::addItem(const RosterItem &item)
{
- // prepend the item, if no timestamp is set
- if (item.lastExchanged().isNull()) {
- insertContact(0, item);
- return;
- }
-
- // index where to add the new contact
- int i = 0;
- for (const auto &itrItem : qAsConst(m_items)) {
- if (item < itrItem) {
- insertContact(i, item);
- return;
- }
- i++;
- }
-
- // append the item to the end of the list
- insertContact(i, item);
+ insertContact(positionToInsert(item), item);
}
void RosterModel::removeItem(const QString &jid)
{
QMutableVectorIterator itr(m_items);
int i = 0;
while (itr.hasNext()) {
if (itr.next().jid() == jid) {
beginRemoveRows(QModelIndex(), i, i);
itr.remove();
endRemoveRows();
return;
}
i++;
}
}
void RosterModel::updateItem(const QString &jid,
const std::function &updateItem)
{
for (int i = 0; i < m_items.length(); i++) {
if (m_items.at(i).jid() == jid) {
// update item
RosterItem item = m_items.at(i);
updateItem(item);
// check if item was actually modified
if (m_items.at(i) == item)
return;
+ // item was changed: refresh all roles
+ emit dataChanged(index(i), index(i), {});
+
// check, if the position of the new item may be different
- if (item.lastExchanged() == m_items.at(i).lastExchanged()) {
- beginRemoveRows(QModelIndex(), i, i);
- m_items.removeAt(i);
- endRemoveRows();
-
- // add the item at the same position
- insertContact(i, item);
- } else {
- beginRemoveRows(QModelIndex(), i, i);
- m_items.removeAt(i);
- endRemoveRows();
-
- // put to new position
- addItem(item);
- }
- break;
+ updateItemPosition(i);
+ return;
}
}
}
void RosterModel::replaceItems(const QHash &items)
{
QVector newItems;
- for (auto item : items) {
+ for (auto item : qAsConst(items)) {
// find old item
auto oldItem = std::find_if(
m_items.begin(),
m_items.end(),
[&] (const RosterItem &oldItem) {
return oldItem.jid() == item.jid();
}
);
// use the old item's values, if found
if (oldItem != m_items.end()) {
item.setLastMessage(oldItem->lastMessage());
item.setLastExchanged(oldItem->lastExchanged());
item.setUnreadMessages(oldItem->unreadMessages());
}
newItems << item;
}
// replace all items
handleItemsFetched(newItems);
}
+void RosterModel::setLastMessage(const QString &contactJid, const QString &newLastMessage)
+{
+ for (int i = 0; i < m_items.length(); i++) {
+ if (m_items.at(i).jid() == contactJid) {
+ m_items[i].setLastMessage(newLastMessage);
+ emit dataChanged(index(i), index(i), QVector() << int(LastMessageRole));
+ return;
+ }
+ }
+}
+
+void RosterModel::setLastExchanged(const QString &contactJid, const QDateTime &newLastExchanged)
+{
+ for (int i = 0; i < m_items.length(); i++) {
+ if (m_items.at(i).jid() == contactJid) {
+ // update item
+ m_items[i].setLastExchanged(newLastExchanged);
+ emit dataChanged(index(i), index(i), QVector() << int(LastExchangedRole));
+
+ // Move row to correct position
+ updateItemPosition(i);
+ return;
+ }
+ }
+}
+
void RosterModel::insertContact(int i, const RosterItem &item)
{
beginInsertRows(QModelIndex(), i, i);
m_items.insert(i, item);
endInsertRows();
}
+
+int RosterModel::updateItemPosition(int currentPosition)
+{
+ const int newPosition = positionToInsert(m_items.at(currentPosition));
+ if (currentPosition == newPosition)
+ return currentPosition;
+
+ beginMoveRows(QModelIndex(), currentPosition, currentPosition, QModelIndex(), newPosition);
+ m_items.move(currentPosition, newPosition);
+ endMoveRows();
+
+ return newPosition;
+}
+
+int RosterModel::positionToInsert(const RosterItem &item)
+{
+ // prepend the item, if no timestamp is set
+ if (item.lastExchanged().isNull())
+ return 0;
+
+ for (int i = 0; i < m_items.size(); i++) {
+ if (item <= m_items.at(i))
+ return i;
+ }
+
+ // append
+ return m_items.size();
+}
diff --git a/src/RosterModel.h b/src/RosterModel.h
index f1b93a3..502fb3e 100644
--- a/src/RosterModel.h
+++ b/src/RosterModel.h
@@ -1,86 +1,93 @@
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see .
*/
#ifndef ROSTERMODEL_H
#define ROSTERMODEL_H
#include
#include
#include "RosterItem.h"
class Kaidan;
class RosterDb;
class MessageModel;
class RosterModel : public QAbstractListModel
{
Q_OBJECT
+
public:
enum RosterItemRoles {
JidRole,
NameRole,
LastExchangedRole,
UnreadMessagesRole,
LastMessageRole,
};
RosterModel(RosterDb *rosterDb, QObject *parent = nullptr);
void setMessageModel(MessageModel *model);
Q_REQUIRED_RESULT bool isEmpty() const;
Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_REQUIRED_RESULT QHash roleNames() const override;
Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role) const override;
signals:
void addItemRequested(const RosterItem &item);
void removeItemRequested(const QString &jid);
void updateItemRequested(const QString &jid,
const std::function &updateItem);
void replaceItemsRequested(const QHash &items);
+ void setLastMessageRequested(const QString &contactJid, const QString &newLastMessage);
+ void setLastExchangedRequested(const QString &contactJid, const QDateTime &newLastExchanged);
private slots:
void handleItemsFetched(const QVector &items);
void addItem(const RosterItem &item);
void removeItem(const QString &jid);
void updateItem(const QString &jid,
const std::function &updateItem);
void replaceItems(const QHash &items);
+ void setLastMessage(const QString &contactJid, const QString &newLastMessage);
+ void setLastExchanged(const QString &contactJid, const QDateTime &newLastExchanged);
private:
void insertContact(int i, const RosterItem &item);
+ int updateItemPosition(int currentIndex);
+ int positionToInsert(const RosterItem &item);
RosterDb *rosterDb;
QVector m_items;
};
#endif // ROSTERMODEL_H