diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c65eb23..0ea7822 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,43 +1,49 @@ # set Kaidans sources (used in main cmake file) set(KAIDAN_SOURCES src/main.cpp src/Kaidan.cpp src/ClientWorker.cpp src/AvatarFileStorage.cpp src/Database.cpp + src/RosterItem.cpp src/RosterModel.cpp + src/RosterDb.cpp src/RosterManager.cpp src/RegistrationManager.cpp - src/MessageHandler.cpp + src/Message.cpp src/MessageModel.cpp + src/MessageDb.cpp + src/MessageHandler.cpp src/Notifications.cpp src/PresenceCache.cpp src/DiscoveryManager.cpp src/VCardManager.cpp src/LogHandler.cpp src/StatusBar.cpp src/UploadManager.cpp src/EmojiModel.cpp src/TransferCache.cpp src/DownloadManager.cpp src/QmlUtils.cpp + src/Utils.cpp - # needed to trigger moc generation + # needed to trigger moc generation / to be displayed in IDEs src/Enums.h + src/Globals.h # kaidan QXmpp extensions (need to be merged into QXmpp upstream) src/qxmpp-exts/QXmppHttpUploadIq.cpp src/qxmpp-exts/QXmppUploadRequestManager.cpp src/qxmpp-exts/QXmppUploadManager.cpp src/qxmpp-exts/QXmppColorGenerator.cpp - # hsluv-c required for color generation + # hsluv-c required for color generation src/hsluv-c/hsluv.c ) if(NOT ANDROID AND NOT IOS) set(KAIDAN_SOURCES ${KAIDAN_SOURCES} src/singleapp/singleapplication.cpp src/singleapp/singleapplication_p.cpp ) endif() diff --git a/src/ClientWorker.cpp b/src/ClientWorker.cpp index 8229f90..5ae5678 100644 --- a/src/ClientWorker.cpp +++ b/src/ClientWorker.cpp @@ -1,210 +1,198 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 "ClientWorker.h" // Qt #include #include #include #include // QXmpp #include #include #include #include // Kaidan #include "Kaidan.h" #include "LogHandler.h" #include "RegistrationManager.h" #include "RosterManager.h" #include "MessageHandler.h" #include "DiscoveryManager.h" #include "VCardManager.h" #include "UploadManager.h" #include "DownloadManager.h" ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QGuiApplication *app, QObject* parent) : QObject(parent), caches(caches), kaidan(kaidan), enableLogging(enableLogging), app(app) { client = new QXmppClient(this); logger = new LogHandler(client, this); logger->enableLogging(enableLogging); vCardManager = new VCardManager(client, caches->avatarStorage, this); registrationManager = new RegistrationManager(kaidan, caches->settings); rosterManager = new RosterManager(kaidan, client, caches->rosterModel, caches->avatarStorage, vCardManager, this); msgHandler = new MessageHandler(kaidan, client, caches->msgModel, this); discoManager = new DiscoveryManager(client, this); uploadManager = new UploadManager(kaidan, client, caches->msgModel, rosterManager, caches->transferCache, this); downloadManager = new DownloadManager(kaidan, caches->transferCache, caches->msgModel, this); client->addExtension(registrationManager); connect(client, &QXmppClient::presenceReceived, caches->presCache, &PresenceCache::updatePresenceRequested); connect(this, &ClientWorker::credentialsUpdated, this, &ClientWorker::setCredentials); // publish kaidan version client->versionManager().setClientName(APPLICATION_DISPLAY_NAME); client->versionManager().setClientVersion(VERSION_STRING); client->versionManager().setClientOs(QSysInfo::prettyProductName()); -#if QXMPP_VERSION >= 0x000904 +#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) // Client State Indication connect(app, &QGuiApplication::applicationStateChanged, this, &ClientWorker::setCsiState); #endif } -ClientWorker::~ClientWorker() -{ - delete client; - delete logger; - delete rosterManager; - delete msgHandler; - delete discoManager; - delete vCardManager; - delete uploadManager; - delete downloadManager; -} - void ClientWorker::main() { // initialize random generator qsrand(time(nullptr)); connect(client, &QXmppClient::stateChanged, kaidan, &Kaidan::setConnectionState); connect(client, &QXmppClient::connected, this, &ClientWorker::onConnect); connect(client, &QXmppClient::error, this, &ClientWorker::onConnectionError); connect(this, &ClientWorker::connectRequested, this, &ClientWorker::xmppConnect); connect(this, &ClientWorker::disconnectRequested, client, &QXmppClient::disconnectFromServer); } void ClientWorker::xmppConnect() { QXmppConfiguration config; config.setJid(creds.jid); config.setResource(creds.jidResource.append(".").append(generateRandomString())); config.setPassword(creds.password); config.setAutoAcceptSubscriptions(false); config.setStreamSecurityMode(QXmppConfiguration::TLSRequired); config.setAutoReconnectionEnabled(true); // will automatically reconnect // on first try we must be sure that we connect successfully // otherwise this could end in a reconnection loop if (creds.isFirstTry) config.setAutoReconnectionEnabled(false); client->connectToServer(config, QXmppPresence(QXmppPresence::Available)); } void ClientWorker::onConnect() { // no mutex needed, because this is called from updateClient() qDebug() << "[client] Connected successfully to server"; // Emit signal, that logging in with these credentials has worked for the first time if (creds.isFirstTry) emit kaidan->logInWorked(); // accept credentials and save them creds.isFirstTry = false; caches->settings->setValue(KAIDAN_SETTINGS_AUTH_JID, creds.jid); caches->settings->setValue(KAIDAN_SETTINGS_AUTH_PASSWD, QString::fromUtf8(creds.password.toUtf8().toBase64())); // after first log in we always want to automatically reconnect client->configuration().setAutoReconnectionEnabled(true); } void ClientWorker::onConnectionError(QXmppClient::Error error) { // no mutex needed, because this is called from updateClient() qDebug() << "[client] Disconnected:" << error; // Check if first time connecting with these credentials if (creds.isFirstTry || error == QXmppClient::XmppStreamError) { // always request new credentials, when failed to connect on first time emit kaidan->newCredentialsNeeded(); } if (error == QXmppClient::NoError) { emit disconnReasonChanged(DisconnReason::ConnUserDisconnected); } else if (error == QXmppClient::KeepAliveError) { emit disconnReasonChanged(DisconnReason::ConnKeepAliveError); } else if (error == QXmppClient::XmppStreamError) { QXmppStanza::Error::Condition xError = client->xmppStreamError(); qDebug() << xError; if (xError == QXmppStanza::Error::NotAuthorized) { emit disconnReasonChanged(DisconnReason::ConnAuthenticationFailed); } else { emit disconnReasonChanged(DisconnReason::ConnNotConnected); } } else if (error == QXmppClient::SocketError) { QAbstractSocket::SocketError sError = client->socketError(); if (sError == QAbstractSocket::ConnectionRefusedError || sError == QAbstractSocket::RemoteHostClosedError) { emit disconnReasonChanged(DisconnReason::ConnConnectionRefused); } else if (sError == QAbstractSocket::HostNotFoundError) { emit disconnReasonChanged(DisconnReason::ConnDnsError); } else if (sError == QAbstractSocket::SocketAccessError) { emit disconnReasonChanged(DisconnReason::ConnNoNetworkPermission); } else if (sError == QAbstractSocket::SocketTimeoutError) { emit disconnReasonChanged(DisconnReason::ConnKeepAliveError); } else if (sError == QAbstractSocket::SslHandshakeFailedError || sError == QAbstractSocket::SslInternalError) { emit disconnReasonChanged(DisconnReason::ConnTlsFailed); } else { emit disconnReasonChanged(DisconnReason::ConnNotConnected); } } } QString ClientWorker::generateRandomString(unsigned int length) const { QString randomString; for (unsigned int i = 0; i < length; ++i) randomString.append(KAIDAN_RESOURCE_RANDOM_CHARS.at( qrand() % KAIDAN_RESOURCE_RANDOM_CHARS.length())); return randomString; } -#if QXMPP_VERSION >= 0x000904 // after QXmpp v0.9.4 +#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) void ClientWorker::setCsiState(Qt::ApplicationState state) { if (state == Qt::ApplicationActive) client->setActive(true); else client->setActive(false); } #endif diff --git a/src/ClientWorker.h b/src/ClientWorker.h index ba11a5f..c1ef071 100644 --- a/src/ClientWorker.h +++ b/src/ClientWorker.h @@ -1,213 +1,194 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 CLIENTWORKER_H #define CLIENTWORKER_H // Qt #include #include -#include #include class QGuiApplication; // QXmpp #include // Kaidan #include "Globals.h" #include "Enums.h" #include "Database.h" #include "MessageModel.h" #include "RosterModel.h" #include "AvatarFileStorage.h" #include "PresenceCache.h" #include "TransferCache.h" class LogHandler; class Kaidan; class ClientWorker; class RegistrationManager; class RosterManager; class MessageHandler; class DiscoveryManager; class VCardManager; class UploadManager; class DownloadManager; using namespace Enums; -class ClientThread : public QThread -{ - Q_OBJECT - friend ClientWorker; - -public: - ClientThread() - { - setObjectName("QXmppClient"); - } - -protected: - void run() override - { - exec(); - } -}; - /** * The ClientWorker is used as a QObject-based worker on the ClientThread. */ class ClientWorker : public QObject { Q_OBJECT public: struct Caches { - Caches(Database *database, QObject *parent = nullptr) - : msgModel(new MessageModel(database->getDatabase(), parent)), - rosterModel(new RosterModel(database->getDatabase(), parent)), + Caches(Kaidan *kaidan, RosterDb *rosterDb, MessageDb *msgDb, + QObject *parent = nullptr) + : msgModel(new MessageModel(kaidan, msgDb, parent)), + rosterModel(new RosterModel(rosterDb, parent)), avatarStorage(new AvatarFileStorage(parent)), presCache(new PresenceCache(parent)), transferCache(new TransferCache(parent)), settings(new QSettings(APPLICATION_NAME, APPLICATION_NAME)) { + rosterModel->setMessageModel(msgModel); } ~Caches() { delete msgModel; delete rosterModel; delete avatarStorage; delete presCache; delete transferCache; delete settings; } MessageModel *msgModel; RosterModel *rosterModel; AvatarFileStorage *avatarStorage; PresenceCache *presCache; TransferCache* transferCache; QSettings *settings; }; struct Credentials { QString jid; QString jidResource; QString password; // if never connected successfully before with these credentials bool isFirstTry; }; /** * @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(); - public slots: /** * Main function of the client thread */ void main(); /** * Sets the new credentials for next connect. * * @param creds The new credentials for the next connect */ void setCredentials(Credentials creds) { this->creds = creds; } /** * Connects the client with the server. */ void xmppConnect(); signals: // emitted by 'Kaidan' to us: void connectRequested(); void disconnectRequested(); void credentialsUpdated(Credentials creds); // emitted by us: // connection state is directly connected (client -> kaidan) without this step void disconnReasonChanged(DisconnectionReason reason); private slots: /** * Notifys via signal that the client has connected. */ void onConnect(); /** * Shows error reason */ void onConnectionError(QXmppClient::Error error); -#if QXMPP_VERSION >= 0x000904 // after QXmpp v0.9.4 +#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) /** * Uses the QGuiApplication state to reduce network traffic when window is minimized */ void setCsiState(Qt::ApplicationState state); #endif private: /** * Generates a random alphanumeric string * * @param length The length of the generated string */ QString generateRandomString(unsigned int length = 4) const; Caches *caches; Kaidan *kaidan; QXmppClient *client; LogHandler *logger; Credentials creds; bool enableLogging; QGuiApplication *app; RegistrationManager *registrationManager; RosterManager *rosterManager; MessageHandler *msgHandler; DiscoveryManager *discoManager; VCardManager *vCardManager; UploadManager *uploadManager; DownloadManager *downloadManager; }; #endif // CLIENTWORKER_H diff --git a/src/Database.cpp b/src/Database.cpp index 69b4a80..36042f8 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -1,362 +1,356 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 +#include +#include +#include static const int DATABASE_LATEST_VERSION = 10; -static const char *DATABASE_TABLE_INFO = "dbinfo"; -static const char *DATABASE_TABLE_MESSAGES = "Messages"; -static const char *DATABASE_TABLE_ROSTER = "Roster"; -Database::Database(QObject *parent) : QObject(parent) +Database::Database(QObject *parent) + : QObject(parent) { - version = -1; - - database = QSqlDatabase::addDatabase("QSQLITE", "kaidan_default_db"); - if (!database.isValid()) { - qFatal("Cannot add database: %s", qPrintable(database.lastError().text())); - } } Database::~Database() { - database.close(); -} - -QSqlDatabase* Database::getDatabase() -{ - return &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("messages.sqlite3"); + const QString fileName = writeDir.absoluteFilePath(DB_FILENAME); // open() will create the SQLite database if it doesn't exist. - database.setDatabaseName(fileName); - if (!database.open()) { - qFatal("Cannot open database: %s", qPrintable(database.lastError().text())); - QFile::remove(fileName); + 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 = database.tables(); - if (!tables.contains(DATABASE_TABLE_INFO)) { - if (tables.contains(DATABASE_TABLE_MESSAGES) && - tables.contains(DATABASE_TABLE_ROSTER)) { + 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 - version = 1; - } else { - version = 0; - } + m_version = 1; + else + m_version = 0; // we've got all we want; do not query for a db version return; } - QSqlQuery query(database); - query.prepare("SELECT version FROM dbinfo"); - if (!query.exec()) { - qWarning("Cannot query database info: %s", qPrintable(database.lastError().text())); - } + QSqlQuery query(m_database); + Utils::execQuery(query, "SELECT version FROM dbinfo"); QSqlRecord record = query.record(); int versionCol = record.indexOf("version"); while (query.next()) { - version = query.value(versionCol).toInt(); + m_version = query.value(versionCol).toInt(); } } bool Database::needToConvert() { - if (version < DATABASE_LATEST_VERSION) { - return true; - } - return false; + return m_version < DATABASE_LATEST_VERSION; } void Database::convertDatabase() { - qDebug() << "[database] Converting database to latest version from version" << version; - while (version < DATABASE_LATEST_VERSION) { - switch (version) { + qDebug() << "[database] Converting database to latest version from version" << m_version; + transaction(); + while (m_version < DATABASE_LATEST_VERSION) { + switch (m_version) { case 0: - createNewDatabase(); version = DATABASE_LATEST_VERSION; break; + createNewDatabase(); m_version = DATABASE_LATEST_VERSION; break; case 1: - convertDatabaseToV2(); version = 2; break; + convertDatabaseToV2(); m_version = 2; break; case 2: - convertDatabaseToV3(); version = 3; break; + convertDatabaseToV3(); m_version = 3; break; case 3: - convertDatabaseToV4(); version = 4; break; + convertDatabaseToV4(); m_version = 4; break; case 4: - convertDatabaseToV5(); version = 5; break; + convertDatabaseToV5(); m_version = 5; break; case 5: - convertDatabaseToV6(); version = 6; break; + convertDatabaseToV6(); m_version = 6; break; case 6: - convertDatabaseToV7(); version = 7; break; + convertDatabaseToV7(); m_version = 7; break; case 7: - convertDatabaseToV8(); version = 8; break; + convertDatabaseToV8(); m_version = 8; break; case 8: - convertDatabaseToV9(); version = 9; break; + convertDatabaseToV9(); m_version = 9; break; case 9: - convertDatabaseToV10(); version = 10; break; + convertDatabaseToV10(); m_version = 10; break; default: break; } } - QSqlQuery query(database); - query.prepare(QString("UPDATE dbinfo SET version = %1").arg(DATABASE_LATEST_VERSION)); - if (!query.exec()) { - qDebug("Failed to query database: %s", qPrintable(query.lastError().text())); - } - - database.commit(); - version = DATABASE_LATEST_VERSION; + QSqlRecord updateRecord; + updateRecord.append(Utils::createSqlField("version", DATABASE_LATEST_VERSION)); + + QSqlQuery query(m_database); + Utils::execQuery( + query, + m_database.driver()->sqlStatement( + QSqlDriver::UpdateStatement, + DB_TABLE_INFO, + updateRecord, + false + ) + ); + + commit(); + m_version = DATABASE_LATEST_VERSION; } void Database::createNewDatabase() { - QSqlQuery query(database); + QSqlQuery query(m_database); // // DB info // createDbInfoTable(); // // Roster // if (!query.exec("CREATE TABLE IF NOT EXISTS 'Roster' (" "'jid' TEXT NOT NULL," "'name' TEXT," "'lastExchanged' TEXT NOT NULL," "'unreadMessages' INTEGER," "'lastMessage' TEXT" ")")) { qFatal("Error creating roster table: Failed to query database: %s", qPrintable(query.lastError().text())); } // // Messages // if (!query.exec("CREATE TABLE IF NOT EXISTS 'Messages' (" "'author' TEXT NOT NULL," "'author_resource' TEXT," "'recipient' TEXT NOT NULL," "'recipient_resource' TEXT," "'timestamp' TEXT NOT NULL," "'message' TEXT NOT NULL," "'id' TEXT NOT NULL," "'isSent' BOOL," // is sent to server "'isDelivered' BOOL," // message has arrived at other client "'type' INTEGER," // type of message (text/image/video/...) "'mediaUrl' TEXT," "'mediaSize' INTEGER," "'mediaContentType' TEXT," "'mediaLastModified' INTEGER," "'mediaLocation' TEXT," "'mediaThumb' BLOB," "'mediaHashes' TEXT," "'edited' BOOL," // whether the message has been edited "'spoilerHint' TEXT," //spoiler hint if isSpoiler "'isSpoiler' BOOL," // message is spoiler "FOREIGN KEY('author') REFERENCES Roster ('jid')," "FOREIGN KEY('recipient') REFERENCES Roster ('jid')" ")" )) { qFatal("Error creating messages table: Failed to query database: %s", qPrintable(query.lastError().text())); } } void Database::createDbInfoTable() { - QSqlQuery query(database); - query.prepare("CREATE TABLE IF NOT EXISTS 'dbinfo' (version INTEGER NOT NULL)"); - execQuery(query); - - query.prepare(QString("INSERT INTO 'dbinfo' (version) VALUES (%1)") - .arg(DATABASE_LATEST_VERSION)); - execQuery(query); + QSqlQuery query(m_database); + Utils::execQuery( + query, + "CREATE TABLE IF NOT EXISTS 'dbinfo' (version 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::convertDatabaseToV2() { // create a new dbinfo table createDbInfoTable(); } void Database::convertDatabaseToV3() { - QSqlQuery query(database); - query.prepare("ALTER TABLE Roster ADD avatarHash TEXT"); - execQuery(query); + QSqlQuery query(m_database); + Utils::execQuery(query, "ALTER TABLE Roster ADD avatarHash TEXT"); } void Database::convertDatabaseToV4() { - QSqlQuery query(database); + 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 - query.prepare("CREATE TEMPORARY TABLE roster_backup(jid,name,lastExchanged," - "unreadMessages,lastMessage,lastOnline,activity,status,mood);"); - execQuery(query); - - query.prepare("INSERT INTO roster_backup SELECT jid,name,lastExchanged,unreadMessages," - "lastMessage,lastOnline,activity,status,mood FROM Roster;"); - execQuery(query); - - query.prepare("DROP TABLE Roster;"); - execQuery(query); - - query.prepare("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);"); - execQuery(query); - - query.prepare("INSERT INTO Roster SELECT jid,name,lastExchanged,unreadMessages," - "lastMessage,lastOnline,activity,status,mood FROM Roster_backup;"); - execQuery(query); - - query.prepare("DROP TABLE Roster_backup;"); - execQuery(query); + 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;"); } void Database::convertDatabaseToV5() { - QSqlQuery query(database); - query.prepare("ALTER TABLE 'Messages' ADD 'type' INTEGER"); - execQuery(query); - - query.prepare("UPDATE Messages SET type = 0 WHERE type IS NULL"); - execQuery(query); - - query.prepare("ALTER TABLE 'Messages' ADD 'mediaUrl' TEXT"); - execQuery(query); + 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"); } void Database::convertDatabaseToV6() { - QSqlQuery query(database); + QSqlQuery query(m_database); for (QString column : {"'mediaSize' INTEGER", "'mediaContentType' TEXT", "'mediaLastModified' INTEGER", "'mediaLocation' TEXT"}) { - query.prepare(QString("ALTER TABLE 'Messages' ADD ").append(column)); - execQuery(query); + Utils::execQuery(query, QString("ALTER TABLE 'Messages' ADD ").append(column)); } } void Database::convertDatabaseToV7() { - QSqlQuery query(database); - query.prepare(QString("ALTER TABLE 'Messages' ADD 'mediaThumb' BLOB")); - execQuery(query); - query.prepare(QString("ALTER TABLE 'Messages' ADD 'mediaHashes' TEXT")); - execQuery(query); + QSqlQuery query(m_database); + Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'mediaThumb' BLOB"); + Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'mediaHashes' TEXT"); } void Database::convertDatabaseToV8() { - QSqlQuery query(database); - - query.prepare("CREATE TEMPORARY TABLE roster_backup(jid, name, lastExchanged, " - "unreadMessages, lastMessage);"); - execQuery(query); - - query.prepare("INSERT INTO roster_backup SELECT jid, name, lastExchanged, unreadMessages, " - "lastMessage FROM Roster;"); - execQuery(query); - - query.prepare("DROP TABLE Roster;"); - execQuery(query); - - query.prepare("CREATE TABLE IF NOT EXISTS Roster ('jid' TEXT NOT NULL,'name' TEXT," - "'lastExchanged' TEXT NOT NULL, 'unreadMessages' INTEGER," - "'lastMessage' TEXT);"); - execQuery(query); - - query.prepare("INSERT INTO Roster SELECT jid, name, lastExchanged, unreadMessages, " - "lastMessage FROM Roster_backup;"); - execQuery(query); - - query.prepare("DROP TABLE roster_backup;"); - execQuery(query); + 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;"); } void Database::convertDatabaseToV9() { - QSqlQuery query(database); - - query.prepare("ALTER TABLE 'Messages' ADD 'edited' BOOL"); - execQuery(query); + QSqlQuery query(m_database); + Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'edited' BOOL"); } void Database::convertDatabaseToV10() { - QSqlQuery query(database); - - query.prepare("ALTER TABLE 'Messages' ADD 'isSpoiler' BOOL"); - execQuery(query); - query.prepare("ALTER TABLE 'Messages' ADD 'spoilerHint' TEXT"); - execQuery(query); -} - -void Database::execQuery(QSqlQuery &query) -{ - if (!query.exec()) { - qDebug() << query.executedQuery(); - qFatal("Failed to query database: %s", qPrintable(query.lastError().text())); - } + QSqlQuery query(m_database); + Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'isSpoiler' BOOL"); + Utils::execQuery(query, "ALTER TABLE 'Messages' ADD 'spoilerHint' TEXT"); } diff --git a/src/Database.h b/src/Database.h index c5613c7..b5ef99d 100644 --- a/src/Database.h +++ b/src/Database.h @@ -1,71 +1,120 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 DATABASE_H #define DATABASE_H #include #include class QSqlQuery; +/** + * The Database class manages the SQL database. It opens the database and converts old + * formats. + */ class Database : public QObject { Q_OBJECT public: Database(QObject *parent = nullptr); ~Database(); - QSqlDatabase* getDatabase(); - bool needToConvert(); - void convertDatabase(); + /** + * Opens the database for reading and writing and guarantees the database to be + * up-to-date. + */ void openDatabase(); + /** + * Begins a transaction if none has been started. + */ + void transaction(); + + /** + * Commits the transaction if every transaction has been finished. + */ + void commit(); + private: + /** + * @return true if the database has to be converted using @c convertDatabase() + * because the database is not up-to-date. + */ + bool needToConvert(); + + /** + * Converts the database to latest model. + */ + void convertDatabase(); + + /** + * Loads the database information and detects the database version. + */ void loadDatabaseInfo(); + + /** + * Creates the database information table which contains the database version. + */ void createDbInfoTable(); + + /** + * Creates a new database without content. + */ void createNewDatabase(); + + /* + * Upgrades the database to the next version. + */ void convertDatabaseToV2(); void convertDatabaseToV3(); void convertDatabaseToV4(); void convertDatabaseToV5(); void convertDatabaseToV6(); void convertDatabaseToV7(); void convertDatabaseToV8(); void convertDatabaseToV9(); void convertDatabaseToV10(); - void execQuery(QSqlQuery &query); - QSqlDatabase database; - int version; + QSqlDatabase m_database; + + /** + * -1 : Database not loaded. + * 0 : Database not existent. + * 1 : Old database before Kaidan v0.3. + * > 1 : Database version. + */ + int m_version = -1; + + int m_transactions = 0; }; #endif // DATABASE_H diff --git a/src/DownloadManager.cpp b/src/DownloadManager.cpp index cd782b3..38514e8 100644 --- a/src/DownloadManager.cpp +++ b/src/DownloadManager.cpp @@ -1,172 +1,182 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 . */ // Kaidan #include "DownloadManager.h" #include "Kaidan.h" #include "TransferCache.h" #include "MessageModel.h" #include "Globals.h" +// C++ +#include // Qt #include "QDir" #include "QStandardPaths" #include "QNetworkRequest" #include "QNetworkReply" #include "QNetworkAccessManager" DownloadManager::DownloadManager(Kaidan *kaidan, TransferCache *transferCache, MessageModel *model, QObject *parent) : QObject(parent), thread(new DownloadThread()), netMngr(new QNetworkAccessManager), kaidan(kaidan), transferCache(transferCache), model(model) { connect(this, &DownloadManager::startDownloadRequested, this, &DownloadManager::startDownload); connect(this, &DownloadManager::abortDownloadRequested, this, &DownloadManager::abortDownload); connect(kaidan, &Kaidan::downloadMedia, this, &DownloadManager::startDownload); netMngr->moveToThread(thread); thread->start(); } DownloadManager::~DownloadManager() { delete netMngr; delete thread; } -void DownloadManager::startDownload(const QString msgId, const QString url) +void DownloadManager::startDownload(const QString &msgId, const QString &url) { // don't download the same file twice and in parallel if (downloads.keys().contains(msgId)) { qWarning() << "Tried to download a file that is currently being " "downloaded."; return; } // we want to save files to 'Downloads/Kaidan/' QString dirPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + QDir::separator() + APPLICATION_DISPLAY_NAME + QDir::separator(); DownloadJob *dl = new DownloadJob(msgId, QUrl(url), dirPath, netMngr, transferCache, kaidan); dl->moveToThread(thread); downloads[msgId] = dl; - connect(dl, &DownloadJob::finished, this, [this, dl, msgId]() { - MessageModel::Message msgUpdate; - msgUpdate.mediaLocation = dl->downloadLocation(); - emit model->updateMessageRequested(msgId, msgUpdate); + connect(dl, &DownloadJob::finished, this, [=]() { + const QString mediaLocation = dl->downloadLocation(); + emit model->updateMessageRequested(msgId, [=] (Message &msg) { + msg.setMediaLocation(mediaLocation); + }); abortDownload(msgId); }); - connect(dl, &DownloadJob::failed, this, [this, msgId]() { + connect(dl, &DownloadJob::failed, this, [=]() { abortDownload(msgId); }); emit dl->startDownloadRequested(); } -void DownloadManager::abortDownload(const QString msgId) +void DownloadManager::abortDownload(const QString &msgId) { DownloadJob *job = downloads.value(msgId); - if (job != nullptr) - delete job; + delete job; downloads.remove(msgId); emit transferCache->removeJobRequested(msgId); } -DownloadJob::DownloadJob(QString msgId, QUrl source, QString filePath, +DownloadJob::DownloadJob(QString msgId, + QUrl source, + QString filePath, QNetworkAccessManager *netMngr, - TransferCache *transferCache, Kaidan *kaidan) - : QObject(nullptr), msgId(msgId), source(source), filePath(filePath), - netMngr(netMngr), transferCache(transferCache), kaidan(kaidan), file() + TransferCache *transferCache, + Kaidan *kaidan) + : QObject(nullptr), + msgId(std::move(msgId)), + source(std::move(source)), + filePath(std::move(filePath)), + netMngr(netMngr), + transferCache(transferCache), + kaidan(kaidan) { connect(this, &DownloadJob::startDownloadRequested, this, &DownloadJob::startDownload); } void DownloadJob::startDownload() { QDir dlDir(filePath); if (!dlDir.exists()) dlDir.mkpath("."); // don't override other files file.setFileName(filePath + source.fileName()); int counter = 1; while (file.exists()) { file.setFileName(filePath + source.fileName() + "-" + QString::number(counter++)); } if (!file.open(QIODevice::WriteOnly)) { qWarning() << "Could not open file for writing:" << file.errorString(); emit kaidan->passiveNotificationRequested( tr("Could not save file: %1").arg(file.errorString())); emit failed(); return; } QNetworkRequest request(source); QNetworkReply *reply = netMngr->get(request); emit transferCache->addJobRequested(msgId, 0); connect(reply, &QNetworkReply::downloadProgress, this, [this] (qint64 bytesReceived, qint64 bytesTotal) { emit transferCache->setJobProgressRequested(msgId, bytesReceived, bytesTotal); }); - connect(reply, &QNetworkReply::finished, this, [this] () { + connect(reply, &QNetworkReply::finished, this, [=] () { emit transferCache->removeJobRequested(msgId); emit finished(); }); connect(reply, QOverload::of(&QNetworkReply::error), - [this, reply] () { + this, [=] () { emit transferCache->removeJobRequested(msgId); qWarning() << "Couldn't download file:" << reply->errorString(); emit kaidan->passiveNotificationRequested( tr("Download failed: %1").arg(reply->errorString())); emit finished(); }); - connect(reply, &QNetworkReply::readyRead, this, [this, reply](){ + connect(reply, &QNetworkReply::readyRead, this, [=](){ file.write(reply->readAll()); }); } QString DownloadJob::downloadLocation() const { return file.fileName(); } diff --git a/src/DownloadManager.h b/src/DownloadManager.h index 14c995c..a9616fc 100644 --- a/src/DownloadManager.h +++ b/src/DownloadManager.h @@ -1,115 +1,118 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 DOWNLOADMANAGER_H #define DOWNLOADMANAGER_H #include #include #include #include #include class Kaidan; class TransferCache; class MessageModel; class QNetworkAccessManager; class DownloadJob : public QObject { Q_OBJECT public: - DownloadJob(QString msgId, QUrl source, QString filePath, - QNetworkAccessManager *netMngr, TransferCache *transferCache, + DownloadJob(QString msgId, + QUrl source, + QString filePath, + QNetworkAccessManager *netMngr, + TransferCache *transferCache, Kaidan *kaidan); QString downloadLocation() const; signals: void startDownloadRequested(); void finished(); void failed(); private slots: void startDownload(); private: QString msgId; QUrl source; QString filePath; QNetworkAccessManager *netMngr; TransferCache *transferCache; Kaidan *kaidan; QFile file; }; class DownloadThread : public QThread { Q_OBJECT public: DownloadThread() { setObjectName("DownloadManager"); } protected: void run() override { exec(); } }; class DownloadManager : public QObject { Q_OBJECT public: DownloadManager(Kaidan *kaidan, TransferCache *transferCache, MessageModel *model, QObject *parent = nullptr); ~DownloadManager(); signals: void startDownloadRequested(const QString msgId, const QString url); void abortDownloadRequested(const QString msgId); public slots: - void startDownload(const QString msgId, const QString url); - void abortDownload(const QString msgId); + void startDownload(const QString &msgId, const QString &url); + void abortDownload(const QString &msgId); private: DownloadThread *thread; QNetworkAccessManager *netMngr; Kaidan *kaidan; TransferCache *transferCache; MessageModel *model; QMap downloads; }; #endif // DOWNLOADMANAGER_H diff --git a/src/Globals.h b/src/Globals.h index b59ec06..e3a186b 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -1,55 +1,60 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 GLOBALS_H #define GLOBALS_H +#include + // Application information #define APPLICATION_DESCRIPTION "A simple, user-friendly Jabber/XMPP client" // Kaidan settings #define KAIDAN_SETTINGS_AUTH_JID "auth/jid" #define KAIDAN_SETTINGS_AUTH_RESOURCE "auth/resource" #define KAIDAN_SETTINGS_AUTH_PASSWD "auth/password" const QString KAIDAN_RESOURCE_RANDOM_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop" "qrstuvwxyz0123456789"; // XML namespaces #define NS_SPOILERS "urn:xmpp:spoiler:0" #define NS_CARBONS "urn:xmpp:carbons:2" #define NS_REGISTER "jabber:iq:register" -/** - * Map of JIDs to contact names - */ -typedef QHash ContactMap; +// SQL +#define DB_CONNECTION "kaidan-messages" +#define DB_FILENAME "messages.sqlite3" +#define DB_MSG_QUERY_LIMIT 20 +#define DB_TABLE_INFO "dbinfo" +#define DB_TABLE_ROSTER "Roster" +#define DB_TABLE_MESSAGES "Messages" #endif // GLOBALS_H diff --git a/src/Kaidan.cpp b/src/Kaidan.cpp index 5c56abf..5cd2f18 100644 --- a/src/Kaidan.cpp +++ b/src/Kaidan.cpp @@ -1,221 +1,222 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 // Kaidan #include "AvatarFileStorage.h" #include "Database.h" -#include "RosterModel.h" +#include "MessageDb.h" #include "MessageModel.h" #include "PresenceCache.h" #include "QmlUtils.h" +#include "RosterDb.h" +#include "RosterModel.h" Kaidan *Kaidan::s_instance = nullptr; Kaidan::Kaidan(QGuiApplication *app, bool enableLogging, QObject *parent) - : QObject(parent), m_utils(new QmlUtils(this)), database(new Database()) + : QObject(parent), + m_utils(new QmlUtils(this)), + m_database(new Database()), + m_dbThrd(new QThread()), + m_msgDb(new MessageDb()), + m_rosterDb(new RosterDb(m_database)), + m_cltThrd(new QThread()) { Q_ASSERT(!Kaidan::s_instance); Kaidan::s_instance = this; // Database setup - database->openDatabase(); - if (database->needToConvert()) - database->convertDatabase(); + 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 - caches = new ClientWorker::Caches(database, this); + 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(caches->avatarStorage, &AvatarFileStorage::avatarIdsChanged, + connect(m_caches->avatarStorage, &AvatarFileStorage::avatarIdsChanged, this, &Kaidan::avatarStorageChanged); // // Load settings // - creds.jid = caches->settings->value(KAIDAN_SETTINGS_AUTH_JID).toString(); - creds.jidResource = caches->settings->value(KAIDAN_SETTINGS_AUTH_RESOURCE) + creds.jid = m_caches->settings->value(KAIDAN_SETTINGS_AUTH_JID).toString(); + creds.jidResource = m_caches->settings->value(KAIDAN_SETTINGS_AUTH_RESOURCE) .toString(); - creds.password = QString(QByteArray::fromBase64(caches->settings->value( + creds.password = QString(QByteArray::fromBase64(m_caches->settings->value( KAIDAN_SETTINGS_AUTH_PASSWD).toString().toUtf8())); // use Kaidan as resource, if no set if (creds.jidResource.isEmpty()) setJidResource(APPLICATION_DISPLAY_NAME); creds.isFirstTry = false; // // Start ClientWorker on new thread // - cltThrd = new ClientThread(); - client = new ClientWorker(caches, this, enableLogging, app); - client->setCredentials(creds); - connect(client, &ClientWorker::disconnReasonChanged, this, &Kaidan::setDisconnReason); + m_client = new ClientWorker(m_caches, this, enableLogging, app); + m_client->setCredentials(creds); + m_client->moveToThread(m_cltThrd); + + connect(m_client, &ClientWorker::disconnReasonChanged, this, &Kaidan::setDisconnReason); + connect(m_cltThrd, &QThread::started, m_client, &ClientWorker::main); - client->moveToThread(cltThrd); - connect(cltThrd, &QThread::started, client, &ClientWorker::main); - cltThrd->start(); + m_client->setObjectName("XmppClient"); + m_cltThrd->start(); } Kaidan::~Kaidan() { - delete caches; - delete database; + delete m_caches; + delete m_database; Kaidan::s_instance = nullptr; } void Kaidan::start() { if (creds.jid.isEmpty() || creds.password.isEmpty()) emit newCredentialsNeeded(); else mainConnect(); } void Kaidan::mainConnect() { if (connectionState != ConnectionState::StateDisconnected) { qWarning() << "[main] Tried to connect, even if still connected!" << "Requesting disconnect."; - emit client->disconnectRequested(); + emit m_client->disconnectRequested(); } - emit client->credentialsUpdated(creds); - emit client->connectRequested(); - - // update own JID to display correct messages - caches->msgModel->setOwnJid(creds.jid); + emit m_client->credentialsUpdated(creds); + emit m_client->connectRequested(); } void Kaidan::mainDisconnect(bool openLogInPage) { // disconnect the client if connected or connecting if (connectionState != ConnectionState::StateDisconnected) - emit client->disconnectRequested(); + emit m_client->disconnectRequested(); if (openLogInPage) { // clear password - caches->settings->remove(KAIDAN_SETTINGS_AUTH_PASSWD); + m_caches->settings->remove(KAIDAN_SETTINGS_AUTH_PASSWD); setPassword(QString()); // trigger log in page emit newCredentialsNeeded(); } } void Kaidan::setConnectionState(QXmppClient::State state) { - this->connectionState = (ConnectionState) 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::setDisconnReason(DisconnectionReason reason) { disconnReason = reason; emit disconnReasonChanged(); } void Kaidan::setJid(const QString &jid) { creds.jid = jid; // credentials were modified -> first try creds.isFirstTry = true; } void Kaidan::setJidResource(const QString &jidResource) { // JID resource won't influence the authentication, so we don't need // to set the first try flag and can save it. creds.jidResource = jidResource; - caches->settings->setValue(KAIDAN_SETTINGS_AUTH_RESOURCE, jidResource); + m_caches->settings->setValue(KAIDAN_SETTINGS_AUTH_RESOURCE, jidResource); } void Kaidan::setPassword(const QString &password) { creds.password = password; // credentials were modified -> first try creds.isFirstTry = true; } -void Kaidan::setChatPartner(const QString &chatPartner) -{ - // check if different - if (this->chatPartner == chatPartner) - return; - - this->chatPartner = chatPartner; - emit chatPartnerChanged(chatPartner); - caches->msgModel->applyRecipientFilter(chatPartner); -} - quint8 Kaidan::getDisconnReason() const { return static_cast(disconnReason); } void Kaidan::addOpenUri(const QByteArray &uri) { qDebug() << "[main]" << uri; if (!uri.startsWith("xmpp:") || !uri.contains("@")) return; if (connectionState == ConnectionState::StateConnected) { emit xmppUriReceived(QString::fromUtf8(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 = QString::fromUtf8(uri); } } Kaidan *Kaidan::instance() { return s_instance; } diff --git a/src/Kaidan.h b/src/Kaidan.h index f7bf5b5..8c39ccf 100644 --- a/src/Kaidan.h +++ b/src/Kaidan.h @@ -1,425 +1,406 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 KAIDAN_H #define KAIDAN_H // Qt #include #include #include // Kaidan #include "ClientWorker.h" #include "Globals.h" #include "Enums.h" class QGuiApplication; class Database; class QXmppClient; class QmlUtils; using namespace Enums; /** * @class Kaidan Kaidan's Back-End Class * * @brief This class will initiate the complete back-end, including the @see Database * connection, viewing models (@see MessageModel, @see RosterModel), etc. * - * This class will run in the main thread, only the XMPP connection runs in another - * thread (@see ClientThread). + * This class will run in the main thread, the XMPP connection and the database managers + * run in other threads. */ class Kaidan : public QObject { Q_OBJECT Q_PROPERTY(QmlUtils* utils READ getUtils CONSTANT) Q_PROPERTY(RosterModel* rosterModel READ getRosterModel CONSTANT) Q_PROPERTY(MessageModel* messageModel READ getMessageModel CONSTANT) Q_PROPERTY(AvatarFileStorage* avatarStorage READ getAvatarStorage NOTIFY avatarStorageChanged) Q_PROPERTY(PresenceCache* presenceCache READ getPresenceCache CONSTANT) Q_PROPERTY(TransferCache* transferCache READ getTransferCache CONSTANT) Q_PROPERTY(QSettings* settings READ getSettings CONSTANT) Q_PROPERTY(quint8 connectionState READ getConnectionState NOTIFY connectionStateChanged) Q_PROPERTY(quint8 disconnReason READ getDisconnReason NOTIFY disconnReasonChanged) Q_PROPERTY(QString jid READ getJid WRITE setJid NOTIFY jidChanged) Q_PROPERTY(QString jidResource READ getJidResource WRITE setJidResource NOTIFY jidResourceChanged) Q_PROPERTY(QString password READ getPassword WRITE setPassword NOTIFY passwordChanged) - Q_PROPERTY(QString chatPartner READ getChatPartner WRITE setChatPartner NOTIFY chatPartnerChanged) Q_PROPERTY(bool uploadServiceFound READ getUploadServiceFound NOTIFY uploadServiceFoundChanged) public: Kaidan(QGuiApplication *app, bool enableLogging = true, QObject *parent = nullptr); ~Kaidan(); /** * Start connection (called from QML when ready) */ Q_INVOKABLE void start(); /** * Connect to the XMPP server * * If you haven't set a username and password, they are used from the * last successful login (the settings file). */ Q_INVOKABLE void mainConnect(); /** * Disconnect from XMPP server * * This will disconnect the client from the server. When disconnected, * the connectionStateChanged signal will be emitted. * * @param openLogInPage If true, the newCredentialsNeeded signal will be * emitted. */ Q_INVOKABLE void mainDisconnect(bool openLogInPage = false); /** * Returns the current ConnectionState */ Q_INVOKABLE quint8 getConnectionState() const { return (quint8) connectionState; } /** * Returns the last disconnection reason */ Q_INVOKABLE quint8 getDisconnReason() const; /** * Set own JID used for connection * * To really change the JID of the current connection, you'll need to * reconnect. */ void setJid(const QString &jid); /** * Get the current JID */ QString getJid() const { return creds.jid; } /** * Set a optional custom JID resource (device name) */ void setJidResource(const QString &jidResource); /** * Get the JID resoruce */ QString getJidResource() const { return creds.jidResource; } /** * Set the password for next connection */ void setPassword(const QString &password); /** * Get the currently used password */ QString getPassword() const { return creds.password; } - /** - * Set the currently opened chat - * - * This will set a filter on the database to only view the related messages. - */ - void setChatPartner(const QString &jid); - - /** - * Get the currrently opened chat - */ - QString getChatPartner() const - { - return chatPartner; - } - RosterModel* getRosterModel() const { - return caches->rosterModel; + return m_caches->rosterModel; } MessageModel* getMessageModel() const { - return caches->msgModel; + return m_caches->msgModel; } AvatarFileStorage* getAvatarStorage() const { - return caches->avatarStorage; + return m_caches->avatarStorage; } PresenceCache* getPresenceCache() const { - return caches->presCache; + return m_caches->presCache; } TransferCache* getTransferCache() const { - return caches->transferCache; + return m_caches->transferCache; } QSettings* getSettings() const { - return caches->settings; + return m_caches->settings; } QmlUtils* getUtils() const { return m_utils; } /** * Adds XMPP URI to open as soon as possible */ void addOpenUri(const QByteArray &uri); /** * Returns whether an HTTP File Upload service has been found */ bool getUploadServiceFound() const { return uploadServiceFound; } static Kaidan *instance(); signals: void avatarStorageChanged(); /** * Emitted, when the client's connection state has changed (e.g. when * successfully connected or when disconnected) */ void connectionStateChanged(); /** * Emitted, when the client failed to connect and gives the reason in * a DisconnectionReason enumatrion. */ void disconnReasonChanged(); /** * Emitted when the JID was changed */ void jidChanged(); /** * Emitted when the JID resouce (device name) has changed */ void jidResourceChanged(); /** * Emitted when the used password for logging in has changed */ void passwordChanged(); - /** - * Emitted when the currently opnened chat has changed - */ - void chatPartnerChanged(QString chatPartner); - /** * Emitted when there are no (correct) credentials and new are needed * * The client will be in disconnected state, when this is emitted. */ void newCredentialsNeeded(); /** * Emitted when log in worked with new credentials * * The client will be in connected state, when this is emitted. */ void logInWorked(); /** * Show passive notification */ void passiveNotificationRequested(QString text); /** * Emitted, whan a subscription request was received */ void subscriptionRequestReceived(QString from, QString msg); /** * Incoming subscription request was accepted or declined by the user */ void subscriptionRequestAnswered(QString jid, bool accepted); /** * Request vCard of any JID * * Is required when the avatar (or other information) of a JID are * requested and the JID is not in the roster. */ void vCardRequested(QString jid); /** * XMPP URI received * * Is called when Kaidan was used to open an XMPP URI (i.e. 'xmpp:kaidan@muc.kaidan.im?join') */ void xmppUriReceived(QString uri); /** * The upload progress of a file upload has changed */ void uploadProgressMade(QString msgId, unsigned long sent, unsigned long total); /** * An HTTP File Upload service was discovered */ void uploadServiceFoundChanged(); /** * Send a text message to any JID * * Currently only contacts are displayed on the RosterPage (there is no * way to view a list of all chats -> for contacts and non-contacts), so * you should only send messages to JIDs from your roster, otherwise you * won't be able to see the message history. */ void sendMessage(QString jid, QString message, bool isSpoiler, QString spoilerHint); /** * Correct the last message * * To get/check the last message id, use `kaidan.messageModel.lastMessageId(jid)` */ void correctMessage(QString toJid, QString msgId, QString message); /** * Upload and send file */ void sendFile(QString jid, QString filePath, QString message); /** * Add a contact to your roster * * @param nick A simple nick name for the new contact, which should be * used to display in the roster. */ void addContact(QString jid, QString nick, QString msg); /** * Remove a contact from your roster * * Only the JID is needed. */ void removeContact(QString jid); /** * Downloads an attached media file of a message * * @param msgId The message * @param url the media url from the message */ void downloadMedia(QString msgId, QString url); /** * Changes the user's password on the server * * @param newPassword The new password */ void changePassword(const QString &newPassword); /** * Emitted, when changing the password has succeeded. */ void passwordChangeSucceeded(); /** * Emitted, when changing the password has failed. */ void passwordChangeFailed(); public slots: /** * Set current connection state */ void setConnectionState(QXmppClient::State state); /** * Sets the disconnection error/reason */ void setDisconnReason(DisconnectionReason reason); /** * Receives messages from another instance of the application */ void receiveMessage(quint32, QByteArray msg) { // currently we only send XMPP URIs addOpenUri(msg); } /** * Enables HTTP File Upload to be used (will be called from UploadManager) */ void setUploadServiceFound(bool enabled) { uploadServiceFound = enabled; emit uploadServiceFoundChanged(); } private: void connectDatabases(); QmlUtils *m_utils; - Database *database; - ClientWorker::Caches *caches; - ClientThread *cltThrd; - ClientWorker *client; + Database *m_database; + QThread *m_dbThrd; + MessageDb *m_msgDb; + RosterDb *m_rosterDb; + QThread *m_cltThrd; + ClientWorker::Caches *m_caches; + ClientWorker *m_client; ClientWorker::Credentials creds; - QString chatPartner; QString openUriCache; bool uploadServiceFound = false; ConnectionState connectionState = ConnectionState::StateDisconnected; DisconnReason disconnReason = DisconnReason::ConnNoError; static Kaidan *s_instance; }; #endif diff --git a/src/Message.cpp b/src/Message.cpp new file mode 100644 index 0000000..5d4aa5b --- /dev/null +++ b/src/Message.cpp @@ -0,0 +1,184 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 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 "Message.h" +#include + +MessageType Message::mediaTypeFromMimeType(const QMimeType &type) +{ + if (type.inherits("image/jpeg") || type.inherits("image/png") || + type.inherits("image/gif")) + return MessageType::MessageImage; + if (type.inherits("audio/flac") || type.inherits("audio/mp4") || + type.inherits("audio/ogg") || type.inherits("audio/wav") || + type.inherits("audio/mpeg") || type.inherits("audio/webm")) + return MessageType::MessageAudio; + if (type.inherits("video/mpeg") || type.inherits("video/x-msvideo") || + type.inherits("video/quicktime") || type.inherits("video/mp4") || + type.inherits("video/x-matroska")) + return MessageType::MessageVideo; + if (type.inherits("text/plain")) + return MessageType::MessageDocument; + return MessageType::MessageFile; +} + +bool Message::operator==(const Message &m) const +{ + return m.id() == id() && + m.body() == body() && + m.from() == from() && + m.to() == to() && + m.type() == type() && + m.stamp() == stamp() && + m.outOfBandUrl() == outOfBandUrl() && + m.isSent() == isSent() && + m.isDelivered() == isDelivered() && + m.mediaType() == mediaType() && + m.mediaContentType() == mediaContentType() && + m.mediaLocation() == mediaLocation() && + m.isEdited() == isEdited() && + m.spoilerHint() == spoilerHint() && + m.isSpoiler() == isSpoiler(); +} + +bool Message::operator!=(const Message &m) const +{ + return !operator==(m); +} + +MessageType Message::mediaType() const +{ + return m_mediaType; +} + +void Message::setMediaType(MessageType mediaType) +{ + m_mediaType = mediaType; +} + +bool Message::sentByMe() const +{ + return m_sentByMe; +} + +void Message::setSentByMe(bool sentByMe) +{ + m_sentByMe = sentByMe; +} + +bool Message::isEdited() const +{ + return m_isEdited; +} + +void Message::setIsEdited(bool isEdited) +{ + m_isEdited = isEdited; +} + +bool Message::isSent() const +{ + return m_isSent; +} + +void Message::setIsSent(bool isSent) +{ + m_isSent = isSent; +} + +bool Message::isDelivered() const +{ + return m_isDelivered; +} + +void Message::setIsDelivered(bool isDelivered) +{ + m_isDelivered = isDelivered; +} + +QString Message::mediaLocation() const +{ + return m_mediaLocation; +} + +void Message::setMediaLocation(const QString &mediaLocation) +{ + m_mediaLocation = mediaLocation; +} + +QString Message::mediaContentType() const +{ + return m_mediaContentType; +} + +void Message::setMediaContentType(const QString &mediaContentType) +{ + m_mediaContentType = mediaContentType; +} + +QDateTime Message::mediaLastModified() const +{ + return m_mediaLastModified; +} + +void Message::setMediaLastModified(const QDateTime &mediaLastModified) +{ + m_mediaLastModified = mediaLastModified; +} + +qint64 Message::mediaSize() const +{ + return m_mediaSize; +} + +void Message::setMediaSize(const qint64 &mediaSize) +{ + m_mediaSize = mediaSize; +} + +bool Message::isSpoiler() const +{ + return m_isSpoiler; +} + +void Message::setIsSpoiler(bool isSpoiler) +{ + m_isSpoiler = isSpoiler; +} + +QString Message::spoilerHint() const +{ + return m_spoilerHint; +} + +void Message::setSpoilerHint(const QString &spoilerHint) +{ + m_spoilerHint = spoilerHint; +} diff --git a/src/Message.h b/src/Message.h new file mode 100644 index 0000000..babb865 --- /dev/null +++ b/src/Message.h @@ -0,0 +1,147 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 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 MESSAGE_H +#define MESSAGE_H + +#include +#include "Enums.h" +class QMimeType; + +using namespace Enums; + +/** + * @brief This class is used to load messages from the database and use them in + * the @c MessageModel. The class inherits from @c QXmppMessage and most + * properties are shared. + */ +class Message : public QXmppMessage +{ +public: + static MessageType mediaTypeFromMimeType(const QMimeType&); + + /** + * Compares another @c Message with this. Only attributes that are saved in the + * database are checked. + */ + bool operator==(const Message &m) const; + bool operator!=(const Message &m) const; + + MessageType mediaType() const; + void setMediaType(MessageType mediaType); + + bool sentByMe() const; + void setSentByMe(bool sentByMe); + + bool isEdited() const; + void setIsEdited(bool isEdited); + + bool isSent() const; + void setIsSent(bool isSent); + + bool isDelivered() const; + void setIsDelivered(bool isDelivered); + + QString mediaLocation() const; + void setMediaLocation(const QString &mediaLocation); + + QString mediaContentType() const; + void setMediaContentType(const QString &mediaContentType); + + QDateTime mediaLastModified() const; + void setMediaLastModified(const QDateTime &mediaLastModified); + + qint64 mediaSize() const; + void setMediaSize(const qint64 &mediaSize); + + bool isSpoiler() const; + void setIsSpoiler(bool isSpoiler); + + QString spoilerHint() const; + void setSpoilerHint(const QString &spoilerHint); + +private: + /** + * Media type of the message, e.g. a text or image. + */ + MessageType m_mediaType = MessageType::MessageText; + + /** + * True if the message was sent by the user. + */ + bool m_sentByMe = true; + + /** + * True if the orginal message was edited. + */ + bool m_isEdited = false; + + /** + * True if the message was sent. + */ + bool m_isSent = false; + + /** + * True if a sent message was delivered to the contact. + */ + bool m_isDelivered = false; + + /** + * Location of the media on the local storage. + */ + QString m_mediaLocation; + + /** + * Media content type, e.g. "image/jpeg". + */ + QString m_mediaContentType; + + /** + * Size of the file in bytes. + */ + qint64 m_mediaSize; + + /** + * Timestamp of the last modification date of the file locally on disk. + */ + QDateTime m_mediaLastModified; + + /** + * True if the message is a spoiler message. + */ + bool m_isSpoiler = false; + + /** + * Hint of the spoiler message. + */ + QString m_spoilerHint; +}; + +#endif // MESSAGE_H diff --git a/src/MessageDb.cpp b/src/MessageDb.cpp new file mode 100644 index 0000000..57f655c --- /dev/null +++ b/src/MessageDb.cpp @@ -0,0 +1,289 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 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(QObject *parent) + : QObject(parent) +{ + connect(this, &MessageDb::fetchMessagesRequested, + this, &MessageDb::fetchMessages); +} + +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()) + 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); +} + +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()); + record.setValue("isSent", msg.isSent()); + record.setValue("isDelivered", msg.isDelivered()); + record.setValue("type", int(msg.type())); + record.setValue("edited", msg.isEdited()); + record.setValue("isSpoiler", msg.isSpoiler()); + record.setValue("spoilerHint", msg.spoilerHint()); + 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::updateMessage(const QString &id, + const std::function &updateMsg) +{ + // load current roster 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 new file mode 100644 index 0000000..8504d35 --- /dev/null +++ b/src/MessageDb.h @@ -0,0 +1,133 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 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); + + /** + * 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); + + /** + * Adds a message to the database. + */ + void addMessage(const Message &msg); + + /** + * Deletes a message from the database. + */ + void removeMessage(const QString &id); + + /** + * 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); +}; + +#endif // MESSAGEDB_H diff --git a/src/MessageHandler.cpp b/src/MessageHandler.cpp index 9df64ef..75adbcc 100644 --- a/src/MessageHandler.cpp +++ b/src/MessageHandler.cpp @@ -1,273 +1,291 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 #include // QXmpp #include -#include -#include #include +#include +#include #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) #include #endif // Kaidan #include "Kaidan.h" +#include "Message.h" #include "MessageModel.h" #include "Notifications.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, - [=] (const QString&, const QString &id) { + this, [=] (const QString&, const QString &id) { emit model->setMessageAsDeliveredRequested(id); }); #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) 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); #endif } MessageHandler::~MessageHandler() { #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) delete carbonManager; #endif } void MessageHandler::handleMessage(const QXmppMessage &msg) { if (msg.body().isEmpty()) return; - MessageModel::Message entry; - entry.author = QXmppUtils::jidToBareJid(msg.from()); - entry.recipient = QXmppUtils::jidToBareJid(msg.to()); - entry.id = msg.id(); - entry.sentByMe = (entry.author == client->configuration().jidBare()); - entry.message = msg.body(); + 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 for (const QXmppElement &extension : msg.extensions()) { if (extension.tagName() == "spoiler" && extension.attribute("xmlns") == NS_SPOILERS) { - entry.isSpoiler = true; - entry.spoilerHint = extension.value(); + message.setIsSpoiler(true); + message.setSpoilerHint(extension.value()); break; } } - entry.type = MessageType::MessageText; // default to text message without media // check if message contains a link and also check out of band url - QList bodyWords = msg.body().split(" "); + QStringList bodyWords = message.body().split(" "); #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) bodyWords.prepend(msg.outOfBandUrl()); #endif for (const QString &word : bodyWords) { if (!word.startsWith("https://") && !word.startsWith("http://")) 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. QUrl url(word); - QList mediaTypes = QMimeDatabase().mimeTypesForFileName(url.fileName()); + const QList mediaTypes = + QMimeDatabase().mimeTypesForFileName(url.fileName()); for (const QMimeType &type : mediaTypes) { - MessageType mType = MessageModel::messageTypeFromMimeType(type); + MessageType mType = Message::mediaTypeFromMimeType(type); if (mType == MessageType::MessageImage || mType == MessageType::MessageAudio || mType == MessageType::MessageVideo || mType == MessageType::MessageDocument || mType == MessageType::MessageFile) { - entry.type = mType; - entry.mediaContentType = type.name(); - entry.mediaUrl = url.toEncoded(); + message.setMediaType(mType); + message.setMediaContentType(type.name()); + message.setOutOfBandUrl(url.toEncoded()); break; } } break; // we can only handle one link } // get possible delay (timestamp) - entry.timestamp = (msg.stamp().isNull() || !msg.stamp().isValid()) - ? QDateTime::currentDateTimeUtc().toString(Qt::ISODate) - : msg.stamp().toUTC().toString(Qt::ISODate); + message.setStamp((msg.stamp().isNull() || !msg.stamp().isValid()) + ? QDateTime::currentDateTimeUtc() + : msg.stamp().toUTC()); // save the message to the database #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) // in case of message correction, replace old message if (msg.replaceId().isEmpty()) { - emit model->addMessageRequested(entry); + emit model->addMessageRequested(message); } else { - entry.edited = true; - entry.id = ""; - emit model->updateMessageRequested(msg.replaceId(), entry); + message.setIsEdited(true); + message.setId(QString()); + emit model->updateMessageRequested(msg.replaceId(), [=] (Message &m) { + // replace completely + m = message; + }); } #else // no message correction with old QXmpp - emit model->addMessageRequested(entry); + emit model->addMessageRequested(message); #endif // 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 = entry.sentByMe ? entry.recipient : entry.author; + 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 (!entry.sentByMe) + if (!message.sentByMe()) Notifications::sendMessageNotification(contactName.toStdString(), msg.body().toStdString()); // TODO: Move back following call to RosterManager::handleMessage when spoiler // messages are implemented in QXmpp - emit kaidan->getRosterModel()->setLastMessageRequested(contactJid, - entry.isSpoiler ? entry.spoilerHint.isEmpty() ? tr("Spoiler") : entry.spoilerHint - : msg.body() + const QString lastMessage = + message.isSpoiler() ? message.spoilerHint().isEmpty() ? tr("Spoiler") + : message.spoilerHint() + : msg.body(); + emit kaidan->getRosterModel()->updateItemRequested( + contactJid, + [=] (RosterItem &item) { + item.setLastMessage(lastMessage); + } ); } -void MessageHandler::sendMessage(QString toJid, QString body, bool isSpoiler, QString spoilerHint) +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; } - MessageModel::Message msg; - msg.isSpoiler = isSpoiler; - msg.spoilerHint = spoilerHint; - msg.author = client->configuration().jidBare(); - msg.recipient = toJid; - msg.id = QXmppUtils::generateStanzaHash(48); - msg.sentByMe = true; - msg.message = body; - msg.type = MessageType::MessageText; // text message without media - msg.timestamp = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - emit model->addMessageRequested(msg); - - QXmppMessage m(msg.author, msg.recipient, body); - m.setId(msg.id); - m.setReceiptRequested(true); + 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) { - QXmppElementList extensions = m.extensions(); + 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.setValue(msg.spoilerHint()); spoiler.setAttribute("xmlns", NS_SPOILERS); extensions.append(spoiler); - m.setExtensions(extensions); + msg.setExtensions(extensions); } - if (client->sendPacket(m)) - emit model->setMessageAsSentRequested(msg.id); + 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(QString toJid, QString msgId, QString body) +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; } - MessageModel::Message msg; - msg.author = client->configuration().jidBare(); - msg.recipient = toJid; - msg.sentByMe = true; - msg.message = body; - msg.type = MessageType::MessageText; // text message without media - msg.edited = true; - - emit model->updateMessageRequested(msgId, msg); - - QXmppMessage m(msg.author, msg.recipient, body); - m.setReceiptRequested(true); + 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); #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) - m.setReplaceId(msgId); + msg.setReplaceId(msgId); #endif - if (client->sendPacket(m)) - emit model->setMessageAsSentRequested(msg.id); - // TODO: handle error + 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 QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) if (info.from() != client->configuration().domain()) return; // enable carbons, if feature found if (info.features().contains(NS_CARBONS)) carbonManager->setCarbonsEnabled(true); #endif } diff --git a/src/MessageHandler.h b/src/MessageHandler.h index f50e424..5e30d6c 100644 --- a/src/MessageHandler.h +++ b/src/MessageHandler.h @@ -1,98 +1,94 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 MESSAGEHANDLER_H #define MESSAGEHANDLER_H // Qt #include // QXmpp #include -#include #include -// Kaidan -#include "Enums.h" class Kaidan; class MessageModel; class QMimeType; +class QXmppMessage; class QXmppDiscoveryIq; #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) class QXmppCarbonManager; #endif -using namespace Enums; - /** - * @class MessageHandler Handler for incoming and outgoing messages + * @class MessageHandler Handler for incoming and outgoing messages. */ class MessageHandler : public QObject { Q_OBJECT public: MessageHandler(Kaidan *kaidan, QXmppClient *client, MessageModel *model, QObject *parent = nullptr); ~MessageHandler(); public slots: /** - * Handles incoming messages from the server + * Handles incoming messages from the server. */ void handleMessage(const QXmppMessage &msg); /** - * Sends a new message to the server and inserts it into the database + * Sends a new message to the server and inserts it into the database. */ - void sendMessage(QString toJid, QString body, bool isSpoiler, QString spoilerHint); + void sendMessage(const QString& toJid, const QString& body, bool isSpoiler, const QString& spoilerHint); /** - * Sends the corrected version of a message + * Sends the corrected version of a message. */ - void correctMessage(QString toJid, QString msgId, QString newBody); + void correctMessage(const QString& toJid, const QString& msgId, const QString& newBody); /** - * Handles service discovery info and enables carbons if feature was found + * Handles service discovery info and enables carbons if feature was found. */ void handleDiscoInfo(const QXmppDiscoveryIq &); private: Kaidan *kaidan; QXmppClient *client; QXmppMessageReceiptManager receiptManager; MessageModel *model; #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) QXmppCarbonManager *carbonManager; #endif QString chatPartner; }; #endif // MESSAGEHANDLER_H diff --git a/src/MessageModel.cpp b/src/MessageModel.cpp index 1f00e16..d3437e4 100644 --- a/src/MessageModel.cpp +++ b/src/MessageModel.cpp @@ -1,229 +1,285 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 . */ -// Kaidan #include "MessageModel.h" -// C++ -#include +// Kaidan +#include "Kaidan.h" +#include "MessageDb.h" // Qt 5 #include -#include -#include -#include -#include -#include +// QXmpp +#include -MessageModel::MessageModel(QSqlDatabase *database, QObject *parent): - QSqlTableModel(parent, *database), database(database) +MessageModel::MessageModel(Kaidan *kaidan, MessageDb *msgDb, QObject *parent) + : QAbstractListModel(parent), + kaidan(kaidan), + msgDb(msgDb) { - setTable("Messages"); - // sort in descending order of the timestamp column - setSort(4, Qt::DescendingOrder); + connect(msgDb, &MessageDb::messagesFetched, + this, &MessageModel::handleMessagesFetched); - setEditStrategy(QSqlTableModel::OnManualSubmit); + connect(this, &MessageModel::addMessageRequested, + this, &MessageModel::addMessage); + connect(this, &MessageModel::addMessageRequested, + msgDb, &MessageDb::addMessage); + + connect(this, &MessageModel::updateMessageRequested, + this, &MessageModel::updateMessage); + connect(this, &MessageModel::updateMessageRequested, + msgDb, &MessageDb::updateMessage); - connect(this, &MessageModel::chatPartnerChanged, - this, &MessageModel::applyRecipientFilter); - connect(this, &MessageModel::addMessageRequested, this, &MessageModel::addMessage); connect(this, &MessageModel::setMessageAsSentRequested, this, &MessageModel::setMessageAsSent); + connect(this, &MessageModel::setMessageAsSentRequested, + msgDb, &MessageDb::setMessageAsSent); + connect(this, &MessageModel::setMessageAsDeliveredRequested, this, &MessageModel::setMessageAsDelivered); - connect(this, &MessageModel::updateMessageRequested, - this, &MessageModel::updateMessage); + connect(this, &MessageModel::setMessageAsDeliveredRequested, + msgDb, &MessageDb::setMessageAsDelivered); } -void MessageModel::applyRecipientFilter(QString recipient) -{ - const QString filterString = QString::fromLatin1( - "(recipient = '%1' AND author = '%2') OR (recipient = '%2' AND author = '%1')") - .arg(recipient, ownJid); +MessageModel::~MessageModel() = default; - setFilter(filterString); - select(); +bool MessageModel::isEmpty() const +{ + return m_messages.isEmpty(); } -QVariant MessageModel::data(const QModelIndex &index, int role) const +int MessageModel::rowCount(const QModelIndex&) const { - if (role < Qt::UserRole) - return QSqlTableModel::data(index, role); - - const QSqlRecord sqlRecord = record(index.row()); - return sqlRecord.value(role - Qt::UserRole); + return m_messages.length(); } QHash MessageModel::roleNames() const { QHash roles; - // record() returns an empty QSqlRecord - for (int i = 0; i < this->record().count(); i++) { - roles.insert(Qt::UserRole + i, record().fieldName(i).toUtf8()); - } + roles[Timestamp] = "timestamp"; + roles[Id] = "id"; + roles[Body] = "body"; + roles[SentByMe] = "sentByMe"; + roles[MediaType] = "mediaType"; + roles[IsEdited] = "isEdited"; + roles[IsSent] = "isSent"; + roles[IsDelivered] = "isDelivered"; + roles[MediaUrl] = "mediaUrl"; + roles[MediaSize] = "mediaSize"; + roles[MediaContentType] = "mediaContentType"; + roles[MediaLastModified] = "mediaLastModifed"; + roles[MediaLocation] = "mediaLocation"; + roles[MediaThumb] = "mediaThumb"; + roles[IsSpoiler] = "isSpoiler"; + roles[SpoilerHint] = "spoilerHint"; return roles; } -MessageType MessageModel::messageTypeFromMimeType(const QMimeType &type) +QVariant MessageModel::data(const QModelIndex &index, int role) const { - if (type.inherits("image/jpeg") || type.inherits("image/png") || - type.inherits("image/gif")) - return MessageType::MessageImage; - else if (type.inherits("audio/flac") || type.inherits("audio/mp4") || - type.inherits("audio/ogg") || type.inherits("audio/wav") || - type.inherits("audio/mpeg") || type.inherits("audio/webm")) - return MessageType::MessageAudio; - else if (type.inherits("video/mpeg") || type.inherits("video/x-msvideo") || - type.inherits("video/quicktime") || type.inherits("video/mp4") || - type.inherits("video/x-matroska")) - return MessageType::MessageVideo; - else if (type.inherits("text/plain")) - return MessageType::MessageDocument; - return MessageType::MessageFile; + if (!hasIndex(index.row(), index.column(), index.parent())) { + qWarning() << "Could not get data from message model." << index << role; + return {}; + } + Message msg = m_messages.at(index.row()); + + switch (role) { + case Timestamp: + return msg.stamp(); + case Id: + return msg.id(); + case Body: + return msg.body(); + case SentByMe: + return msg.sentByMe(); + case MediaType: + return int(msg.mediaType()); + case IsEdited: + return msg.isEdited(); + case IsSent: + return msg.isSent(); + case IsDelivered: + return msg.isDelivered(); + case MediaUrl: + return msg.outOfBandUrl(); + case MediaLocation: + return msg.mediaLocation(); + case MediaContentType: + return msg.mediaContentType(); + case MediaSize: + return msg.mediaLastModified(); + case MediaLastModified: + return msg.mediaLastModified(); + case IsSpoiler: + return msg.isSpoiler(); + case SpoilerHint: + return msg.spoilerHint(); + + // TODO: add (only useful as soon as we have got SIMS) + case MediaThumb: + return {}; + } + return {}; } -QString MessageModel::lastMessageId(QString jid) const +void MessageModel::fetchMore(const QModelIndex &) { - return lastMsgIdCache.value(jid, ""); + emit msgDb->fetchMessagesRequested(kaidan->getJid(), chatPartner(), + m_messages.size()); } -void MessageModel::setMessageAsSent(const QString msgId) +bool MessageModel::canFetchMore(const QModelIndex &) const { - for (int i = 0; i < rowCount(); ++i) { - QSqlRecord rec = record(i); - if (rec.value("id").toString() == msgId) { - rec.setValue("isSent", true); - setRecord(i, rec); - break; - } - } - submitAll(); + return !m_fetchedAll; } -void MessageModel::setMessageAsDelivered(const QString msgId) +QString MessageModel::chatPartner() { - for (int i = 0; i < rowCount(); ++i) { - QSqlRecord rec = record(i); - if (rec.value("id").toString() == msgId) { - rec.setValue("isDelivered", true); - setRecord(i, rec); - break; - } - } - submitAll(); + return m_chatPartner; } -void MessageModel::updateMessage(const QString id, Message msg) +void MessageModel::setChatPartner(const QString &chatPartner) { - QSqlRecord rec; - int recId; - bool found = false; - for (int i = 0; i < rowCount(); ++i) { - rec = record(i); - if (rec.value("id").toString() == id) { - recId = i; - found = true; - break; - } + if (chatPartner == m_chatPartner) + return; + + m_chatPartner = chatPartner; + m_fetchedAll = false; + + emit chatPartnerChanged(chatPartner); + clearAll(); +} + +bool MessageModel::canCorrectMessage(const QString &msgId) const +{ + // Only allow correction of the latest message sent by us + for (const auto &msg : m_messages) { + if (msg.from() == kaidan->getJid()) + return msg.id() == msgId; } + return false; +} - if (!found) +void MessageModel::handleMessagesFetched(const QVector &msgs) +{ + if (msgs.isEmpty()) return; - rec.setValue("id", msg.id.isEmpty() ? id : msg.id); - rec.setValue("edited", msg.edited); - rec.setValue("isSent", msg.isSent); - rec.setValue("isDelivered", msg.isDelivered); - if (!msg.timestamp.isEmpty()) - rec.setValue("timestamp", msg.timestamp); - if (!msg.message.isEmpty()) - rec.setValue("message", msg.message); - if (!msg.mediaUrl.isEmpty()) - rec.setValue("mediaUrl", msg.mediaUrl); - if (msg.mediaSize) - rec.setValue("mediaSize", msg.mediaSize); - if (!msg.mediaContentType.isEmpty()) - rec.setValue("mediaContentType", msg.mediaContentType); - if (msg.mediaLastModified) - rec.setValue("mediaLastModified", msg.mediaLastModified); - if (!msg.mediaLocation.isEmpty()) - rec.setValue("mediaLocation", msg.mediaLocation); - if (!msg.mediaThumb.isEmpty()) - rec.setValue("mediaThumb", msg.mediaThumb); - if (!msg.mediaHashes.isEmpty()) - rec.setValue("mediaHashes", msg.mediaHashes); - - setRecord(recId, rec); - submitAll(); - - // update last message id - if (!msg.id.isEmpty() && msg.author == ownJid) { - lastMsgIdCache[msg.recipient] = msg.id; + beginInsertRows(QModelIndex(), rowCount(), rowCount() + msgs.length() - 1); + for (auto msg : msgs) { + msg.setSentByMe(kaidan->getJid() == msg.from()); + m_messages << msg; } + endInsertRows(); + + if (msgs.length() < DB_MSG_QUERY_LIMIT) + m_fetchedAll = true; } -void MessageModel::addMessage(Message msg) -{ - // - // add the new message - // - - QSqlRecord record = this->record(); - record.setValue("author", msg.author); - record.setValue("recipient", msg.recipient); - record.setValue("timestamp", msg.timestamp); - record.setValue("message", msg.message); - record.setValue("id", msg.id); - record.setValue("isSent", msg.isSent); - record.setValue("isDelivered", msg.isDelivered); - record.setValue("type", (quint8) msg.type); - record.setValue("edited", msg.edited); - record.setValue("mediaUrl", msg.mediaUrl); - record.setValue("isSpoiler", msg.isSpoiler); - record.setValue("spoilerHint", msg.spoilerHint); - if (msg.mediaSize) - record.setValue("mediaSize", msg.mediaSize); - record.setValue("mediaContentType", msg.mediaContentType); - if (msg.mediaLastModified) - record.setValue("mediaLastModified", msg.mediaLastModified); - record.setValue("mediaLocation", msg.mediaLocation); - record.setValue("mediaThumb", msg.mediaThumb); - record.setValue("mediaHashes", msg.mediaHashes); - - if (!insertRecord(0, record)) { - qWarning() << "Failed to add message to DB:" << lastError().text(); - return; +void MessageModel::clearAll() +{ + if (!m_messages.isEmpty()) { + beginRemoveRows(QModelIndex(), 0, rowCount() - 1); + m_messages.clear(); + endRemoveRows(); } +} + +void MessageModel::insertMessage(int idx, const Message &msg) +{ + beginInsertRows(QModelIndex(), idx, idx); + m_messages.insert(idx, msg); + endInsertRows(); +} - submitAll(); +void MessageModel::addMessage(const Message &msg) +{ + if (QXmppUtils::jidToBareJid(msg.from()) == m_chatPartner + || QXmppUtils::jidToBareJid(msg.to()) == m_chatPartner) { + // index where to add the new message + int i = 0; + for (const auto &message : m_messages) { + if (msg.stamp() > message.stamp()) { + insertMessage(i, msg); + return; + } + i++; + } - // update last message id, in case we're author - if (!msg.id.isEmpty() && msg.author == ownJid) { - lastMsgIdCache[msg.recipient] = msg.id; + // add message to the end of the list + insertMessage(i, msg); } } + +void MessageModel::updateMessage(const QString &id, + const std::function &updateMsg) +{ + for (int i = 0; i < m_messages.length(); i++) { + if (m_messages.at(i).id() == id) { + // update message + Message msg = m_messages.at(i); + updateMsg(msg); + + // check if item was actually modified + if (m_messages.at(i) == msg) + return; + + // check, if the position of the new message may be different + if (msg.stamp() == m_messages.at(i).stamp()) { + beginRemoveRows(QModelIndex(), i, i); + m_messages.removeAt(i); + endRemoveRows(); + + // add the message at the same position + insertMessage(i, msg); + } else { + beginRemoveRows(QModelIndex(), i, i); + m_messages.removeAt(i); + endRemoveRows(); + + // put to new position + addMessage(msg); + } + break; + } + } +} + +void MessageModel::setMessageAsSent(const QString &msgId) +{ + updateMessage(msgId, [] (Message &msg) { + msg.setIsSent(true); + }); +} + +void MessageModel::setMessageAsDelivered(const QString &msgId) +{ + updateMessage(msgId, [] (Message &msg) { + msg.setIsDelivered(true); + }); +} diff --git a/src/MessageModel.h b/src/MessageModel.h index 74ef63c..a20c9a4 100644 --- a/src/MessageModel.h +++ b/src/MessageModel.h @@ -1,123 +1,114 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 MESSAGEMODEL_H #define MESSAGEMODEL_H -#include -#include "Enums.h" - -using namespace Enums; +#include +#include "Message.h" class QMimeType; +class MessageDb; +class Kaidan; -class MessageModel : public QSqlTableModel +class MessageModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(QString chatPartner READ chatPartner WRITE setChatPartner + NOTIFY chatPartnerChanged) public: - MessageModel(QSqlDatabase *database, QObject *parent = nullptr); - - QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; - QHash roleNames() const Q_DECL_OVERRIDE; - - /** - * Applies a filter to the database to only show messages of a certain chat - */ - void applyRecipientFilter(QString recipient); - - struct Message { - QString author; - QString authorResource; - QString recipient; - QString recipientResource; - QString timestamp; - QString message; - QString id; - bool sentByMe; - MessageType type; - bool edited = false; - bool isSent = false; - bool isDelivered = false; - bool isSpoiler = false; - QString spoilerHint; - QString mediaUrl; - quint64 mediaSize; - QString mediaContentType; - quint64 mediaLastModified; - QString mediaLocation; - QByteArray mediaThumb; - QString mediaHashes; + enum MessageRoles { + Timestamp = Qt::UserRole + 1, + Id, + Body, + SentByMe, + MediaType, + IsEdited, + IsSent, + IsDelivered, + MediaUrl, + MediaSize, + MediaContentType, + MediaLastModified, + MediaLocation, + MediaThumb, + IsSpoiler, + SpoilerHint }; + Q_ENUM(MessageRoles) + + MessageModel(Kaidan *kaidan, MessageDb *msgDb, QObject *parent = nullptr); + ~MessageModel(); + + 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; - static MessageType messageTypeFromMimeType(const QMimeType &); + Q_INVOKABLE void fetchMore(const QModelIndex &parent) override; + Q_INVOKABLE bool canFetchMore(const QModelIndex &parent) const override; - /** - * Returns the last message id of a contact - * - * The result can be empty, if the last message was sent in a previous session. This - * is, because we currently can't be sure if there were other messages since then. - */ - Q_INVOKABLE QString lastMessageId(QString jid) const; + QString chatPartner(); + void setChatPartner(const QString &chatPartner); + + Q_INVOKABLE bool canCorrectMessage(const QString &msgId) const; signals: - /** - * Emitted when the user opens another chat to apply a filter to the db - */ - void chatPartnerChanged(QString &jid); - void addMessageRequested(Message msg); - void setMessageAsSentRequested(const QString msgId); - void setMessageAsDeliveredRequested(const QString msgId); - void updateMessageRequested(const QString id, Message msg); - -public slots: - /** - * Set own JID for displaying correct messages - */ - void setOwnJid(const QString &jid) - { - ownJid = jid; - } + void chatPartnerChanged(const QString &chatPartner); + + void addMessageRequested(const Message &msg); + void updateMessageRequested(const QString &id, + const std::function &updateMsg); + void setMessageAsSentRequested(const QString &msgId); + void setMessageAsDeliveredRequested(const QString &msgId); private slots: - void addMessage(Message msg); - void setMessageAsSent(const QString msgId); - void setMessageAsDelivered(const QString msgId); - void updateMessage(const QString id, Message msg); + void handleMessagesFetched(const QVector &m_messages); + + void addMessage(const Message &msg); + void updateMessage(const QString &id, + const std::function &updateMsg); + void setMessageAsSent(const QString &msgId); + void setMessageAsDelivered(const QString &msgId); private: - QSqlDatabase *database; + void clearAll(); + void insertMessage(int i, const Message &msg); - QString ownJid; + Kaidan *kaidan; + MessageDb *msgDb; - QHash lastMsgIdCache; + QVector m_messages; + QString m_chatPartner; + bool m_fetchedAll = false; }; #endif // MESSAGEMODEL_H diff --git a/src/QmlUtils.cpp b/src/QmlUtils.cpp index fb9fe79..0a9784a 100644 --- a/src/QmlUtils.cpp +++ b/src/QmlUtils.cpp @@ -1,140 +1,142 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 "QmlUtils.h" -#include +// Qt #include +#include #include #include #include #include #include #include -#include #include +#include +// QXmpp #include "qxmpp-exts/QXmppColorGenerator.h" QmlUtils::QmlUtils(QObject *parent) : QObject(parent) { } QString QmlUtils::getResourcePath(const QString &name) const { // We generally prefer to first search for files in application resources if (QFile::exists(":/" + name)) return QString("qrc:/" + name); // list of file paths where to search for the resource file QStringList pathList; // add relative path from binary (only works if installed) pathList << QCoreApplication::applicationDirPath() + QString("/../share/") + QString(APPLICATION_NAME); // get the standard app data locations for current platform pathList << QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); #ifdef UBUNTU_TOUCH pathList << QString("./share/") + QString(APPLICATION_NAME); #endif #ifndef NDEBUG #ifdef DEBUG_SOURCE_PATH // add source directory (only for debug builds) pathList << QString(DEBUG_SOURCE_PATH) + QString("/data"); #endif #endif // search for file in directories for (int i = 0; i < pathList.size(); i++) { // open directory QDir directory(pathList.at(i)); // look up the file if (directory.exists(name)) { // found the file, return the path return QUrl::fromLocalFile(directory.absoluteFilePath(name)).toString(); } } // no file found qWarning() << "[main] Could NOT find media file:" << name; - return ""; + return QString(); } bool QmlUtils::isImageFile(const QUrl &fileUrl) const { QMimeType type = QMimeDatabase().mimeTypeForUrl(fileUrl); return type.inherits("image/jpeg") || type.inherits("image/png"); } void QmlUtils::copyToClipboard(const QString &text) const { QGuiApplication::clipboard()->setText(text); } QString QmlUtils::fileNameFromUrl(const QUrl &url) const { return QUrl(url).fileName(); } QString QmlUtils::fileSizeFromUrl(const QUrl &url) const { #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) // Qt 5.10 or later return QLocale::system().formattedDataSize(QFileInfo(QUrl(url).toLocalFile()) .size()); #else // before Qt 5.10 there was no formattedDataSize() method: // sizes will always be in MiB double size = QFileInfo(QUrl(url).toLocalFile()).size(); return QString::number(qRound(size / 1024.0 / 10.24) / 100.0).append(" MiB"); #endif } QString QmlUtils::formatMessage(const QString &message) const { // escape all special XML chars (like '<' and '>') // and spilt into words for processing return processMsgFormatting(message.toHtmlEscaped().split(" ")); } QColor QmlUtils::getUserColor(const QString &nickName) const { QXmppColorGenerator::RGBColor color = QXmppColorGenerator::generateColor(nickName); - return QColor(color.red, color.green, color.blue); + return {color.red, color.green, color.blue}; } QString QmlUtils::processMsgFormatting(const QStringList &list, bool isFirst) const { if (list.isEmpty()) - return ""; + return QString(); // link highlighting if (list.first().startsWith("https://") || list.first().startsWith("http://")) - return (isFirst ? "" : " ") + QString("%1").arg(list.first()) + return (isFirst ? QString() : " ") + QString("%1").arg(list.first()) + processMsgFormatting(list.mid(1), false); - return (isFirst ? "" : " ") + list.first() + processMsgFormatting(list.mid(1), false); + return (isFirst ? QString() : " ") + list.first() + processMsgFormatting(list.mid(1), false); } diff --git a/src/RosterDb.cpp b/src/RosterDb.cpp new file mode 100644 index 0000000..31a5c3b --- /dev/null +++ b/src/RosterDb.cpp @@ -0,0 +1,256 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 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" +// Qt +#include +#include +#include +#include +#include + +RosterDb::RosterDb(Database *db, QObject *parent) + : QObject(parent), + m_db(db) +{ + connect(this, &RosterDb::fetchItemsRequested, this, &RosterDb::fetchItems); +} + +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", + oldItem.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(item.unreadMessages()); + query.addBindValue(item.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); + + 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() +{ + QSqlQuery query(QSqlDatabase::database(DB_CONNECTION)); + query.setForwardOnly(true); + Utils::execQuery(query, "SELECT * FROM Roster ORDER BY lastExchanged"); + + QVector items; + parseItemsFromQuery(query, items); + + emit itemsFetched(items); +} diff --git a/src/RosterManager.h b/src/RosterDb.h similarity index 57% copy from src/RosterManager.h copy to src/RosterDb.h index f6a7644..37cd72c 100644 --- a/src/RosterManager.h +++ b/src/RosterDb.h @@ -1,77 +1,82 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 ROSTERMANAGER_H -#define ROSTERMANAGER_H +#ifndef ROSTERDB_H +#define ROSTERDB_H +// C++ +#include // Qt #include -// QXmpp -#include -#include +class QSqlQuery; +class QSqlRecord; // Kaidan -#include "RosterModel.h" -#include "VCardManager.h" +#include "RosterItem.h" +class Database; -class Kaidan; -class QXmppClient; -class VCardManager; - -class RosterManager : public QObject +class RosterDb : public QObject { Q_OBJECT - public: - RosterManager(Kaidan *kaidan, QXmppClient *client, RosterModel *rosterModel, - AvatarFileStorage *avatarStorage, VCardManager *vCardManager, - QObject *parent = nullptr); + RosterDb(Database *db, QObject *parent = nullptr); + + 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 itemsFetched(const QVector &items); public slots: - void addContact(const QString jid, const QString name, const QString msg); - void removeContact(const QString jid); - void handleSendMessage(const QString &jid, const QString &message, - bool isSpoiler = false, const QString isSpoilerMessage = ""); + 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 populateRoster(); - void handleMessage(const QXmppMessage &msg); + void fetchItems(); private: - Kaidan *kaidan; - QXmppClient *client; - RosterModel *model; - AvatarFileStorage *avatarStorage; - VCardManager *vCardManager; - - QXmppRosterManager &manager; - QString chatPartner; + Database *m_db; }; -#endif // ROSTERMANAGER_H +#endif // ROSTERDB_H diff --git a/src/RosterManager.h b/src/RosterItem.cpp similarity index 54% copy from src/RosterManager.h copy to src/RosterItem.cpp index f6a7644..b143e15 100644 --- a/src/RosterManager.h +++ b/src/RosterItem.cpp @@ -1,77 +1,100 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 ROSTERMANAGER_H -#define ROSTERMANAGER_H +#include "RosterItem.h" -// Qt -#include -// QXmpp -#include -#include -// Kaidan -#include "RosterModel.h" -#include "VCardManager.h" +RosterItem::RosterItem(const QXmppRosterIq::Item &item) + : m_jid(item.bareJid()), m_name(item.name()) +{ +} + +QString RosterItem::jid() const +{ + return m_jid; +} + +void RosterItem::setJid(const QString &jid) +{ + m_jid = jid; +} + +QString RosterItem::name() const +{ + return m_name; +} + +void RosterItem::setName(const QString &name) +{ + m_name = name; +} -class Kaidan; -class QXmppClient; -class VCardManager; +int RosterItem::unreadMessages() const +{ + return m_unreadMessages; +} -class RosterManager : public QObject +void RosterItem::setUnreadMessages(int unreadMessages) { - Q_OBJECT + m_unreadMessages = unreadMessages; +} -public: - RosterManager(Kaidan *kaidan, QXmppClient *client, RosterModel *rosterModel, - AvatarFileStorage *avatarStorage, VCardManager *vCardManager, - QObject *parent = nullptr); +QDateTime RosterItem::lastExchanged() const +{ + return m_lastExchanged; +} -public slots: - void addContact(const QString jid, const QString name, const QString msg); - void removeContact(const QString jid); - void handleSendMessage(const QString &jid, const QString &message, - bool isSpoiler = false, const QString isSpoilerMessage = ""); +void RosterItem::setLastExchanged(const QDateTime &lastExchanged) +{ + m_lastExchanged = lastExchanged; +} -private slots: - void populateRoster(); - void handleMessage(const QXmppMessage &msg); +QString RosterItem::lastMessage() const +{ + return m_lastMessage; +} -private: - Kaidan *kaidan; - QXmppClient *client; - RosterModel *model; - AvatarFileStorage *avatarStorage; - VCardManager *vCardManager; +void RosterItem::setLastMessage(const QString &lastMessage) +{ + m_lastMessage = lastMessage; +} - QXmppRosterManager &manager; - QString chatPartner; -}; +bool RosterItem::operator==(const RosterItem &other) const +{ + return m_jid == other.jid() && + m_name == other.name() && + m_lastMessage == other.lastMessage() && + m_lastExchanged == other.lastExchanged() && + m_unreadMessages == other.unreadMessages(); +} -#endif // ROSTERMANAGER_H +bool RosterItem::operator!=(const RosterItem &other) const +{ + return !operator==(other); +} diff --git a/src/RosterManager.h b/src/RosterItem.h similarity index 56% copy from src/RosterManager.h copy to src/RosterItem.h index f6a7644..b901a5f 100644 --- a/src/RosterManager.h +++ b/src/RosterItem.h @@ -1,77 +1,92 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 ROSTERMANAGER_H -#define ROSTERMANAGER_H +#ifndef ROSTERITEM_H +#define ROSTERITEM_H -// Qt -#include -// QXmpp -#include -#include -// Kaidan -#include "RosterModel.h" -#include "VCardManager.h" +#include +#include "QXmppRosterIq.h" -class Kaidan; -class QXmppClient; -class VCardManager; - -class RosterManager : public QObject +/** + * Item containing one contact / conversation. + */ +class RosterItem { - Q_OBJECT - public: - RosterManager(Kaidan *kaidan, QXmppClient *client, RosterModel *rosterModel, - AvatarFileStorage *avatarStorage, VCardManager *vCardManager, - QObject *parent = nullptr); + RosterItem() = default; + RosterItem(const QXmppRosterIq::Item &item); + + QString jid() const; + void setJid(const QString &jid); + + QString name() const; + void setName(const QString &name); -public slots: - void addContact(const QString jid, const QString name, const QString msg); - void removeContact(const QString jid); - void handleSendMessage(const QString &jid, const QString &message, - bool isSpoiler = false, const QString isSpoilerMessage = ""); + int unreadMessages() const; + void setUnreadMessages(int unreadMessages); -private slots: - void populateRoster(); - void handleMessage(const QXmppMessage &msg); + QDateTime lastExchanged() const; + void setLastExchanged(const QDateTime &lastExchanged); + + QString lastMessage() const; + void setLastMessage(const QString &lastMessage); + + bool operator==(const RosterItem &other) const; + bool operator!=(const RosterItem &other) const; private: - Kaidan *kaidan; - QXmppClient *client; - RosterModel *model; - AvatarFileStorage *avatarStorage; - VCardManager *vCardManager; + /** + * JID of the contact. + */ + QString m_jid; + + /** + * Name of the contact. + */ + QString m_name; + + /** + * Number of messages unread by the user. + */ + int m_unreadMessages = 0; + + /** + * Last activity of the conversation, e.g. an incoming message. + * This is used to sort the contacts on the roster page. + */ + QDateTime m_lastExchanged = QDateTime::currentDateTimeUtc(); - QXmppRosterManager &manager; - QString chatPartner; + /** + * Last message of the conversation. + */ + QString m_lastMessage; }; -#endif // ROSTERMANAGER_H +#endif // ROSTERITEM_H diff --git a/src/RosterManager.cpp b/src/RosterManager.cpp index ccb98d3..a54fa3c 100644 --- a/src/RosterManager.cpp +++ b/src/RosterManager.cpp @@ -1,175 +1,192 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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" -#include "Kaidan.h" +// Kaidan +#include "ClientWorker.h" #include "Globals.h" +#include "Kaidan.h" #include "VCardManager.h" -#include "ClientWorker.h" // QXmpp #include -#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()) +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, [=] (QString jid) { - QXmppRosterIq::Item item = manager.getRosterEntry(jid); - emit model->insertContactRequested(jid, item.name()); + connect(&manager, &QXmppRosterManager::itemAdded, + [this, vCardManager, model] (const QString &jid) { + emit model->addItemRequested(RosterItem(manager.getRosterEntry(jid))); vCardManager->fetchVCard(jid); }); - connect(&manager, &QXmppRosterManager::itemChanged, [=] (QString jid) { - QXmppRosterIq::Item item = manager.getRosterEntry(jid); - emit model->setContactNameRequested(jid, item.name()); + connect(&manager, &QXmppRosterManager::itemChanged, + this, [this, model] (QString jid) { + emit model->updateItemRequested(m_chatPartner, + [this, &jid] (RosterItem &item) { + item.setName(manager.getRosterEntry(jid).name()); + }); }); - connect(&manager, &QXmppRosterManager::itemRemoved, [=] (QString jid) { - emit model->removeContactRequested(jid); - }); + connect(&manager, &QXmppRosterManager::itemRemoved, model, &RosterModel::removeItemRequested); - connect(&manager, &QXmppRosterManager::subscriptionReceived, [=] (QString jid) { + connect(&manager, &QXmppRosterManager::subscriptionReceived, + this, [kaidan] (const QString &jid) { // emit signal to ask user emit kaidan->subscriptionRequestReceived(jid, QString()); }); - connect(kaidan, &Kaidan::subscriptionRequestAnswered, [=] (QString jid, bool accepted) { + connect(kaidan, &Kaidan::subscriptionRequestAnswered, + this, [=] (QString jid, bool accepted) { if (accepted) manager.acceptSubscription(jid); else manager.refuseSubscription(jid); }); // user actions connect(kaidan, &Kaidan::addContact, this, &RosterManager::addContact); connect(kaidan, &Kaidan::removeContact, this, &RosterManager::removeContact); connect(kaidan, &Kaidan::sendMessage, this, &RosterManager::handleSendMessage); - connect(kaidan, &Kaidan::chatPartnerChanged, [=] (QString chatPartner) { - this->chatPartner = chatPartner; - - // reset unread message counter - emit model->setUnreadMessageCountRequested(chatPartner, 0); - }); - connect(client, &QXmppClient::messageReceived, this, &RosterManager::handleMessage); } void RosterManager::populateRoster() { qDebug() << "[client] [RosterManager] Populating roster"; // create a new list of contacts - ContactMap contactList; - for (auto const& jid : manager.getRosterBareJids()) { - QString name = manager.getRosterEntry(jid).name(); - contactList[jid] = name; + QHash items; + for (const auto &jid : manager.getRosterBareJids()) { + items[jid] = RosterItem(manager.getRosterEntry(jid)); if (avatarStorage->getHashOfJid(jid).isEmpty()) vCardManager->fetchVCard(jid); } - // replace current contacts with new ones from server - emit model->replaceContactsRequested(contactList); + if (!items.isEmpty()) { + // replace current contacts with new ones from server + emit model->replaceItemsRequested(items); + } } -void RosterManager::addContact(const QString jid, const QString name, const QString msg) +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) +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::handleSendMessage(const QString &jid, const QString &message, - bool isSpoiler, const QString spoilerHint) + bool isSpoiler, const QString &spoilerHint) { if (client->state() == QXmppClient::ConnectedState) { - // update last message of the contact - emit model->setLastMessageRequested(jid, - isSpoiler ? spoilerHint.isEmpty() ? tr("Spoiler") : spoilerHint - : message - ); - - // update last exchanged datetime (sorting order in contact list) - QString dateTime = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate); - emit model->setLastExchangedRequested(jid, dateTime); + // 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); + }); } } void RosterManager::handleMessage(const QXmppMessage &msg) { if (msg.body().isEmpty()) return; - // TODO: Check if it's a carbon message (will need QXmpp v0.10) // 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) - QString dateTime = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - emit model->setLastExchangedRequested(contactJid, dateTime); + const QDateTime dateTime = QDateTime::currentDateTimeUtc(); - // when we sent a message we can ignore all unread message notifications - if (sentByMe) - emit model->setUnreadMessageCountRequested(contactJid, 0); // update unread message counter, if chat is not active - else if (chatPartner != contactJid) - emit model->newUnreadMessageRequested(contactJid); + if (sentByMe) { + // if we sent a message (with another device), reset counter + emit model->updateItemRequested(contactJid, + [dateTime] (RosterItem &item) { + item.setLastExchanged(dateTime); + item.setUnreadMessages(0); + }); + } else if (m_chatPartner != contactJid) { + emit model->updateItemRequested(contactJid, + [dateTime] (RosterItem &item) { + item.setLastExchanged(dateTime); + item.setUnreadMessages(item.unreadMessages() + 1); + }); + } } diff --git a/src/RosterManager.h b/src/RosterManager.h index f6a7644..2e9849d 100644 --- a/src/RosterManager.h +++ b/src/RosterManager.h @@ -1,77 +1,76 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 ROSTERMANAGER_H #define ROSTERMANAGER_H // Qt #include -// QXmpp -#include -#include // Kaidan -#include "RosterModel.h" -#include "VCardManager.h" - +class AvatarFileStorage; class Kaidan; -class QXmppClient; +class RosterModel; class VCardManager; +// QXmpp +class QXmppClient; +class QXmppMessage; +class QXmppRosterManager; class RosterManager : public QObject { Q_OBJECT public: RosterManager(Kaidan *kaidan, QXmppClient *client, RosterModel *rosterModel, AvatarFileStorage *avatarStorage, VCardManager *vCardManager, QObject *parent = nullptr); public slots: - void addContact(const QString jid, const QString name, const QString msg); - void removeContact(const QString jid); + void addContact(const QString &jid, const QString &name, const QString &msg); + void removeContact(const QString &jid); void handleSendMessage(const QString &jid, const QString &message, - bool isSpoiler = false, const QString isSpoilerMessage = ""); + bool isSpoiler = false, const QString &spoilerHint = QString()); private slots: void populateRoster(); void handleMessage(const QXmppMessage &msg); private: Kaidan *kaidan; QXmppClient *client; RosterModel *model; AvatarFileStorage *avatarStorage; VCardManager *vCardManager; QXmppRosterManager &manager; - QString chatPartner; + QString m_chatPartner; }; #endif // ROSTERMANAGER_H diff --git a/src/RosterModel.cpp b/src/RosterModel.cpp index 413defd..bb9f88c 100644 --- a/src/RosterModel.cpp +++ b/src/RosterModel.cpp @@ -1,236 +1,237 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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" - -#include -#include +// Kaidan +#include "RosterDb.h" +#include "MessageModel.h" +// C++ +#include +// Qt +#include #include #include #include -#include -RosterModel::RosterModel(QSqlDatabase *database, QObject *parent): - QSqlTableModel(parent, *database) +RosterModel::RosterModel(RosterDb *rosterDb, QObject *parent) + : QAbstractListModel(parent), + rosterDb(rosterDb) { - setTable("Roster"); - setEditStrategy(QSqlTableModel::OnManualSubmit); - - // sort from last time exchanged - setSort(2, Qt::DescendingOrder); - - select(); - - connect(this, &RosterModel::insertContactRequested, this, &RosterModel::insertContact); - connect(this, &RosterModel::removeContactRequested, this, &RosterModel::removeContact); - connect(this, &RosterModel::setContactNameRequested, this, &RosterModel::setContactName); - connect(this, &RosterModel::setLastExchangedRequested, this, &RosterModel::setLastExchanged); - connect(this, &RosterModel::setUnreadMessageCountRequested, this, &RosterModel::setUnreadMessageCount); - connect(this, &RosterModel::setLastMessageRequested, this, &RosterModel::setLastMessage); - connect(this, &RosterModel::newUnreadMessageRequested, this, &RosterModel::newUnreadMessage); - connect(this, &RosterModel::replaceContactsRequested, this, &RosterModel::replaceContacts); + 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(); } -QHash RosterModel::roleNames() const +void RosterModel::setMessageModel(MessageModel *model) { - QHash roles; - // record() returns an empty QSqlRecord - for (int i = 0; i < this->record().count(); i ++) { - roles.insert(Qt::UserRole + i + 1, record().fieldName(i).toUtf8()); - } - return roles; + connect(model, &MessageModel::chatPartnerChanged, + this, [=] (const QString &chatPartner) { + // reset unread message counter + emit updateItemRequested(chatPartner, + [] (RosterItem &item) { + item.setUnreadMessages(0); + }); + }); } -QVariant RosterModel::data(const QModelIndex &index, int role) const +bool RosterModel::isEmpty() const { - QVariant value; - - if (index.isValid()) { - if (role < Qt::UserRole) { - value = QSqlQueryModel::data(index, role); - } else { - int columnIdx = role - Qt::UserRole - 1; - QModelIndex modelIndex = this->index(index.row(), columnIdx); - value = QSqlQueryModel::data(modelIndex, Qt::DisplayRole); - } - } - return value; + return m_items.isEmpty(); } -void RosterModel::insertContact(QString jid, QString name) +int RosterModel::rowCount(const QModelIndex&) const { - // create a new record - QSqlRecord newRecord = record(); - - // set the given data - newRecord.setValue("jid", jid); - newRecord.setValue("name", name); - newRecord.setValue("lastExchanged", QDateTime::currentDateTime().toString(Qt::ISODate)); - newRecord.setValue("unreadMessages", 0); - - // inster the record into the DB (or print error) - if (!insertRecord(rowCount(), newRecord)) { - qWarning() << "Failed to save Contact into DB:" - << lastError().text(); - } - submitAll(); + return m_items.length(); } -void RosterModel::removeContact(QString jid) +QHash RosterModel::roleNames() const { - for (int i = 0; i < rowCount(); ++i) { - if (record(i).value("jid").toString() == jid) { - removeRow(i); - break; - } - } - submitAll(); + QHash roles; + roles[JidRole] = "jid"; + roles[NameRole] = "name"; + roles[LastExchangedRole] = "lastExchanged"; + roles[UnreadMessagesRole] = "unreadMessages"; + roles[LastMessageRole] = "lastMessage"; + return roles; } -void RosterModel::setContactName(QString jid, QString name) +QVariant RosterModel::data(const QModelIndex &index, int role) const { - for (int i = 0; i < rowCount(); ++i) { - QSqlRecord rec = record(i); - if (rec.value("jid").toString() == jid) { - rec.setValue("name", name); - setRecord(i, rec); - break; - } + if (!hasIndex(index.row(), index.column(), index.parent())) { + qWarning() << "Could not get data from roster model." << index << role; + return {}; } - submitAll(); + + 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::setLastExchanged(const QString jid, QString date) +void RosterModel::handleItemsFetched(const QVector &items) { - for (int i = 0; i < rowCount(); ++i) { - QSqlRecord rec = record(i); - if (rec.value("jid").toString() == jid) { - rec.setValue("lastExchanged", date); - setRecord(i, rec); - break; - } - } - submitAll(); + beginResetModel(); + m_items = items; + endResetModel(); } -void RosterModel::setUnreadMessageCount(const QString jid, const int count) +void RosterModel::addItem(const RosterItem &item) { - for (int i = 0; i < rowCount(); ++i) { - QSqlRecord rec = record(i); - if (rec.value("jid").toString() == jid) { - rec.setValue("unreadMessages", count); - setRecord(i, rec); - break; + // 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 : m_items) { + if (item.lastExchanged().toMSecsSinceEpoch() >= itrItem.lastExchanged().toMSecsSinceEpoch()) { + insertContact(i, item); + return; } + i++; } - submitAll(); + + // append the item to the end of the list + insertContact(i, item); } -void RosterModel::newUnreadMessage(const QString jid) +void RosterModel::removeItem(const QString &jid) { - for (int i = 0; i < rowCount(); ++i) { - QSqlRecord rec = record(i); - if (rec.value("jid").toString() == jid) { - // increase unreadMessages by one - rec.setValue("unreadMessages", rec.value("unreadMessages").toInt() + 1); - setRecord(i, rec); - break; + QMutableVectorIterator itr(m_items); + int i = 0; + while (itr.hasNext()) { + if (itr.next().jid() == jid) { + beginRemoveRows(QModelIndex(), i, i); + itr.remove(); + endRemoveRows(); + return; } + i++; } - submitAll(); + } -void RosterModel::setLastMessage(const QString jid, QString message) +void RosterModel::updateItem(const QString &jid, + const std::function &updateItem) { - for (int i = 0; i < rowCount(); ++i) { - QSqlRecord rec = record(i); - if (rec.value("jid").toString() == jid) { - rec.setValue("lastMessage", message); - setRecord(i, rec); + 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; + + // 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; } } - submitAll(); } -void RosterModel::replaceContacts(const ContactMap &contactList) +void RosterModel::replaceItems(const QHash &items) { - // This will first remove a list of JIDs from the DB that were deleted on - // the server, then it'll update all the nick names. This is made so - // complicated, because otherwise information about lastExchanged, lastMessage, - // etc. were lost. - - // list of the JIDs from the DB - QStringList currentJids; - for (int i = 0; i < rowCount(); ++i) - currentJids << record(i).value("jid").toString(); - - // add all JIDs to a delete list that are in the original list but not in the new one - QList rowsToDelete; - for (int i = 0; i < currentJids.length(); i++) { - if (!contactList.contains(currentJids.at(i))) - rowsToDelete << i; - } - - // remove rows - for (const int row : rowsToDelete) - removeRow(row); - - // Update all contact nicknames / add new contacts - for (auto &jid : contactList.keys()) { - QString name = contactList[jid]; - - if (currentJids.contains(jid)) { - // find row and set name - for (int i = 0; i < rowCount(); ++i) { - QSqlRecord rec = record(i); - if (rec.value("jid").toString() == jid) { - rec.setValue("name", name); - setRecord(i, rec); - break; - } + QVector newItems; + for (auto item : items.values()) { + // find old item + auto oldItem = std::find_if( + m_items.begin(), + m_items.end(), + [&] (const RosterItem &oldItem) { + return oldItem.jid() == item.jid(); } - } else { - // add new row - QSqlRecord rec = record(); - // set the given data - rec.setValue("jid", jid); - rec.setValue("name", name); - rec.setValue("lastExchanged", QDateTime::currentDateTime().toUTC() - .toString(Qt::ISODate)); - rec.setValue("unreadMessages", 0); - - if (!insertRecord(rowCount(), rec)) - qWarning() << "Failed to save Contact into DB:" << lastError().text(); + ); + + // 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; } - submitAll(); + // replace all items + handleItemsFetched(newItems); +} + +void RosterModel::insertContact(int i, const RosterItem &item) +{ + beginInsertRows(QModelIndex(), i, i); + m_items.insert(i, item); + endInsertRows(); } diff --git a/src/RosterModel.h b/src/RosterModel.h index bb3f334..c5a9122 100644 --- a/src/RosterModel.h +++ b/src/RosterModel.h @@ -1,72 +1,86 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 -// Qt -#include -#include "Globals.h" +#include +#include +#include "RosterItem.h" -class QSqlDatabase; -class Database; +class Kaidan; +class RosterDb; +class MessageModel; -class RosterModel : public QSqlTableModel +class RosterModel : public QAbstractListModel { Q_OBJECT - public: - RosterModel(QSqlDatabase *database, QObject *parent = nullptr); + enum RosterItemRoles { + JidRole, + NameRole, + LastExchangedRole, + UnreadMessagesRole, + LastMessageRole, + }; + + RosterModel(RosterDb *rosterDb, QObject *parent = nullptr); + + void setMessageModel(MessageModel *model); - QHash roleNames() const; - QVariant data(const QModelIndex &index, int role) const; + 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 insertContactRequested(QString jid, QString nickname); - void removeContactRequested(QString jid); - void setContactNameRequested(QString jid, QString nickname); - void setLastExchangedRequested(const QString jid, QString date); - void setUnreadMessageCountRequested(const QString jid, const int unreadMessageCount); - void setLastMessageRequested(const QString jid, QString message); - void newUnreadMessageRequested(const QString jid); - void replaceContactsRequested(const ContactMap &contactMap); + void addItemRequested(const RosterItem &item); + void removeItemRequested(const QString &jid); + void updateItemRequested(const QString &jid, + const std::function &updateItem); + void replaceItemsRequested(const QHash &items); private slots: - void insertContact(QString jid, QString nickname); - void removeContact(QString jid); - void setContactName(QString jid, QString nickname); - void setLastExchanged(const QString jid, QString date); - void setUnreadMessageCount(const QString jid, const int unreadMessageCount); - void newUnreadMessage(const QString jid); - void setLastMessage(const QString jid, QString message); - void replaceContacts(const ContactMap &contactMap); + 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); + +private: + void insertContact(int i, const RosterItem &item); + + RosterDb *rosterDb; + QVector m_items; }; #endif // ROSTERMODEL_H diff --git a/src/TransferCache.cpp b/src/TransferCache.cpp index 750abc0..ecb892e 100644 --- a/src/TransferCache.cpp +++ b/src/TransferCache.cpp @@ -1,135 +1,135 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2018 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 "TransferCache.h" #include "qxmpp-exts/QXmppUploadManager.h" #include TransferJob::TransferJob(qint64 bytesTotal) - : QObject(), progress(0.0), bytesSent(0), bytesTotal(bytesTotal) + : progress(0.0), bytesSent(0), bytesTotal(bytesTotal) { } void TransferJob::setProgress(qreal progress) { if (this->progress == progress) return; this->progress = progress; emit progressChanged(); } void TransferJob::setBytesSent(qint64 bytesSent) { if (this->bytesSent == bytesSent) return; this->bytesSent = bytesSent; emit bytesSentChanged(); if (bytesTotal != 0) - setProgress((qreal) bytesSent / (qreal) bytesTotal); + setProgress(qreal(bytesSent) / qreal(bytesTotal)); } void TransferJob::setBytesTotal(qint64 bytesTotal) { if (this->bytesTotal == bytesTotal) return; this->bytesTotal = bytesTotal; emit bytesTotalChanged(); if (bytesTotal != 0) - setProgress((qreal) bytesSent / (qreal) bytesTotal); + setProgress(qreal(bytesSent) / qreal(bytesTotal)); } TransferCache::TransferCache(QObject *parent) : QObject(parent) { connect(this, &TransferCache::addJobRequested, this, &TransferCache::addJob); connect(this, &TransferCache::removeJobRequested, this, &TransferCache::removeJob); connect(this, &TransferCache::setJobBytesSentRequested, this, &TransferCache::setJobBytesSent); connect(this, &TransferCache::setJobProgressRequested, this, &TransferCache::setJobBytesSent); } TransferCache::~TransferCache() { // wait for other threads to finish QMutexLocker locker(&mutex); } void TransferCache::addJob(const QString& msgId, qint64 bytesTotal) { QMutexLocker locker(&mutex); uploads.insert(msgId, new TransferJob(bytesTotal)); locker.unlock(); emit jobsChanged(); } void TransferCache::removeJob(const QString& msgId) { QMutexLocker locker(&mutex); delete uploads[msgId]; uploads.remove(msgId); locker.unlock(); emit jobsChanged(); } bool TransferCache::hasUpload(QString msgId) const { QMutexLocker locker(&mutex); return uploads.contains(msgId); } TransferJob* TransferCache::jobByMessageId(QString msgId) const { QMutexLocker locker(&mutex); TransferJob* job = uploads.value(msgId); if (job == nullptr) return emptyJob; return job; } void TransferCache::setJobProgress(const QString &msgId, qint64 bytesSent, qint64 bytesTotal) { TransferJob* job = jobByMessageId(msgId); QMutexLocker locker(&mutex); job->setBytesTotal(bytesTotal); job->setBytesSent(bytesSent); } void TransferCache::setJobBytesSent(const QString &msgId, qint64 bytesSent) { TransferJob* job = jobByMessageId(msgId); QMutexLocker locker(&mutex); job->setBytesSent(bytesSent); } diff --git a/src/UploadManager.cpp b/src/UploadManager.cpp index 09836e4..245633a 100644 --- a/src/UploadManager.cpp +++ b/src/UploadManager.cpp @@ -1,157 +1,162 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 "UploadManager.h" #include "Kaidan.h" #include "MessageHandler.h" #include "RosterManager.h" #include "TransferCache.h" // QXmpp #include // Qt #include #include #include #include #include #include #include #include UploadManager::UploadManager(Kaidan *kaidan, QXmppClient *client, MessageModel *msgModel, RosterManager* rosterManager, TransferCache* transfers, QObject* parent) : QObject(parent), kaidan(kaidan), client(client), msgModel(msgModel), rosterManager(rosterManager), transfers(transfers) { client->addExtension(&manager); connect(kaidan, &Kaidan::sendFile, this, &UploadManager::sendFile); connect(&manager, &QXmppUploadManager::serviceFoundChanged, [this]() { // needed because kaidan is in main thread QMetaObject::invokeMethod(this->kaidan, "setUploadServiceFound", Qt::QueuedConnection, Q_ARG(bool, manager.serviceFound())); }); connect(&manager, &QXmppUploadManager::uploadSucceeded, this, &UploadManager::handleUploadSucceeded); connect(&manager, &QXmppUploadManager::uploadFailed, this, &UploadManager::handleUploadFailed); } void UploadManager::sendFile(QString jid, QString fileUrl, QString body) { // TODO: Add offline media message cache and send when connnected again if (client->state() != QXmppClient::ConnectedState) { emit kaidan->passiveNotificationRequested( tr("Could not send file, as a result of not being connected.") ); qWarning() << "[client] [UploadManager] Could not send file, as a result of " "not being connected."; return; } qDebug() << "[client] [UploadManager] Adding upload for file:" << fileUrl; QFileInfo file(QUrl(fileUrl).toLocalFile()); const QXmppHttpUpload* upload = manager.uploadFile(file); QMimeType mimeType = QMimeDatabase().mimeTypeForFile(file); const QString msgId = QXmppUtils::generateStanzaHash(48); - auto *msg = new MessageModel::Message(); - msg->author = client->configuration().jidBare(); - msg->recipient = jid; - msg->id = msgId; - msg->sentByMe = true; - msg->message = body; - msg->type = MessageModel::messageTypeFromMimeType(mimeType); - msg->timestamp = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate); - msg->mediaSize = file.size(); - msg->mediaContentType = mimeType.name(); - msg->mediaLastModified = file.lastModified().currentMSecsSinceEpoch(); - msg->mediaLocation = file.filePath(); + auto *msg = new Message; + msg->setFrom(client->configuration().jidBare()); + msg->setTo(jid); + msg->setId(msgId); + msg->setSentByMe(true); + msg->setBody(body); + msg->setMediaType(Message::mediaTypeFromMimeType(mimeType)); + msg->setStamp(QDateTime::currentDateTimeUtc()); + msg->setMediaSize(file.size()); + msg->setMediaContentType(mimeType.name()); + msg->setMediaLastModified(file.lastModified()); + msg->setMediaLocation(file.filePath()); // cache message and upload emit transfers->addJobRequested(msgId, upload->bytesTotal()); messages.insert(upload->id(), msg); emit msgModel->addMessageRequested(*msg); // update last message QString lastMessage = tr("File"); if (!body.isEmpty()) lastMessage = lastMessage.append(": ").append(body); rosterManager->handleSendMessage(jid, lastMessage); connect(upload, &QXmppHttpUpload::bytesSentChanged, this, [upload, this, msgId] () { emit transfers->setJobBytesSentRequested(msgId, upload->bytesSent()); }); } void UploadManager::handleUploadSucceeded(const QXmppHttpUpload *upload) { qDebug() << "[client] [UploadManager] A file upload has succeeded. Now sending message."; - MessageModel::Message *originalMsg = messages.value(upload->id()); - MessageModel::Message msgUpdate; - msgUpdate.mediaUrl = upload->slot().getUrl().toEncoded(); - msgUpdate.message = upload->slot().getUrl().toDisplayString(); - if (!originalMsg->message.isEmpty()) - msgUpdate.message = msgUpdate.message.prepend(originalMsg->message + "\n"); + Message *originalMsg = messages.value(upload->id()); - emit msgModel->updateMessageRequested(originalMsg->id, msgUpdate); + const QString oobUrl = upload->slot().getUrl().toEncoded(); + const QString body = originalMsg->body().isEmpty() + ? oobUrl + : originalMsg->body() + "\n" + oobUrl; + + emit msgModel->updateMessageRequested(originalMsg->id(), [=] (Message &msg) { +#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) + msg.setOutOfBandUrl(oobUrl); +#endif + msg.setBody(body); + }); // send message - QXmppMessage m(originalMsg->author, originalMsg->recipient, msgUpdate.message); - m.setId(originalMsg->id); + QXmppMessage m(originalMsg->from(), originalMsg->to(), body); + m.setId(originalMsg->id()); m.setReceiptRequested(true); - m.setStamp(QXmppUtils::datetimeFromString(originalMsg->timestamp)); + m.setStamp(originalMsg->stamp()); #if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0) m.setOutOfBandUrl(upload->slot().getUrl().toEncoded()); #endif bool success = client->sendPacket(m); if (success) - emit msgModel->setMessageAsSentRequested(originalMsg->id); + emit msgModel->setMessageAsSentRequested(originalMsg->id()); // TODO: handle error messages.remove(upload->id()); - emit transfers->removeJobRequested(originalMsg->id); + emit transfers->removeJobRequested(originalMsg->id()); } void UploadManager::handleUploadFailed(const QXmppHttpUpload *upload) { qDebug() << "[client] [UploadManager] A file upload has failed."; - const QString &msgId = messages.value(upload->id())->id; + const QString &msgId = messages.value(upload->id())->id(); messages.remove(upload->id()); emit transfers->removeJobRequested(msgId); } diff --git a/src/UploadManager.h b/src/UploadManager.h index e2e5eed..6bc6bc0 100644 --- a/src/UploadManager.h +++ b/src/UploadManager.h @@ -1,91 +1,91 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 UPLOADMANAGER_H #define UPLOADMANAGER_H // QXmpp #include "qxmpp-exts/QXmppUploadManager.h" // Qt #include #include #include // Kaidan #include "Enums.h" #include "MessageModel.h" using namespace Enums; class Kaidan; class RosterManager; class TransferCache; /** * @class UploadManager Class for handling and starting HTTP File Uploads */ class UploadManager : public QObject { Q_OBJECT public: /** * Default constructor */ UploadManager(Kaidan* kaidan, QXmppClient* client, MessageModel* msgModel, RosterManager* rosterManager, TransferCache* transfers, QObject* parent = nullptr); signals: /** * Connect to it to be notified about progress on uploads */ void uploadProgressMade(QString msgId, unsigned long sent, unsigned long total); public slots: /** * Starts uploading a file */ void sendFile(QString jid, QString filePath, QString message); void handleUploadFailed(const QXmppHttpUpload *upload); void handleUploadSucceeded(const QXmppHttpUpload *upload); private: Kaidan *kaidan; QXmppClient *client; QXmppUploadManager manager; MessageModel *msgModel; RosterManager *rosterManager; TransferCache* transfers; - QMap messages; + QMap messages; }; #endif // UPLOADMANAGER_H diff --git a/src/Utils.cpp b/src/Utils.cpp new file mode 100644 index 0000000..309e446 --- /dev/null +++ b/src/Utils.cpp @@ -0,0 +1,109 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 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 "Utils.h" +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void Utils::prepareQuery(QSqlQuery &query, const QString &sql) +{ + if (!query.prepare(sql)) { + qDebug() << "Failed to prepare query:" << sql; + qFatal("QSqlError: %s", qPrintable(query.lastError().text())); + } +} + +void Utils::execQuery(QSqlQuery &query) +{ + if (!query.exec()) { + qDebug() << "Failed to execute query:" << query.executedQuery(); + qFatal("QSqlError: %s", qPrintable(query.lastError().text())); + } +} + +void Utils::execQuery(QSqlQuery &query, const QString &sql) +{ + prepareQuery(query, sql); + execQuery(query); +} + +void Utils::execQuery(QSqlQuery &query, + const QString &sql, + const QVector &bindValues) +{ + prepareQuery(query, sql); + + for (const auto &val : bindValues) + query.addBindValue(val); + + execQuery(query); +} + +void Utils::execQuery(QSqlQuery &query, + const QString &sql, + const QMap &bindValues) +{ + prepareQuery(query, sql); + + for (const auto &key : bindValues.keys()) + query.bindValue(key, bindValues.value(key)); + + execQuery(query); +} + +QSqlField Utils::createSqlField(const QString &key, const QVariant &val) +{ + QSqlField field(key, val.type()); + field.setValue(val); + return field; +} + +QString Utils::simpleWhereStatement(const QSqlDriver *driver, + const QString &key, + const QVariant &val) +{ + QSqlRecord rec; + rec.append(createSqlField(key, val)); + + return " " + driver->sqlStatement( + QSqlDriver::WhereStatement, + QString(), + rec, + false + ); +} diff --git a/src/Utils.h b/src/Utils.h new file mode 100644 index 0000000..183c48b --- /dev/null +++ b/src/Utils.h @@ -0,0 +1,120 @@ +/* + * Kaidan - A user-friendly XMPP client for every device! + * + * Copyright (C) 2016-2019 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 UTILS_H +#define UTILS_H + +template class QMap; +class QSqlDatabase; +class QSqlDriver; +class QSqlField; +class QSqlQuery; +class QString; +class QVariant; +template class QVector; + +/** + * @class Utils C++ utilities for the back end. + */ +class Utils +{ +public: + /** + * Prepares an SQL query for executing it by @c execQuery and handles possible + * errors. + * + * @param query SQL query + * @param sql SQL statement + */ + static void prepareQuery(QSqlQuery &query, const QString &sql); + + /** + * Executes an SQL query and handles possible errors. + * + * @param query SQL query + */ + static void execQuery(QSqlQuery &query); + + /** + * Prepares an SQL query, executes it and handles possible errors. + * + * @param query SQL query + * @param sql SQL statement + */ + static void execQuery(QSqlQuery &query, const QString &sql); + + /** + * Prepares an SQL query, sequentially binds values, executes the query and + * handles possible errors. + * + * @param query SQL query + * @param sql SQL statement + * @param bindValues values to be bound sequentially + */ + static void execQuery(QSqlQuery &query, + const QString &sql, + const QVector &bindValues); + + /** + * Prepares an SQL query, binds values by names, executes the query and handles + * possible errors. + * + * @param query SQL query + * @param sql SQL statement + * @param bindValues values to be bound as key-value pairs + */ + static void execQuery(QSqlQuery &query, + const QString &sql, + const QMap &bindValues); + + /** + * Creates an SQL field that may be used for an SQL statement. + * + * @param key name of the SQL field + * @param val value of the SQL field + * @return the SQL field. + */ + static QSqlField createSqlField(const QString &key, const QVariant &val); + + /** + * Creates a where clause with one parameter. + * + * @param driver SQL database driver + * @param key name of the where condition + * @param val value of the where condition + * @return the where clause with a space, so it can be directly appended to + * another statement. + */ + static QString simpleWhereStatement(const QSqlDriver *driver, + const QString &key, + const QVariant &val); +}; + +#endif // UTILS_H diff --git a/src/main.cpp b/src/main.cpp index 709128f..ade7ffb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,283 +1,289 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 . */ // Qt -#include #include +#include #include +#include +#include #include -#include #include #include #include -#include -#include +#include // QXmpp -#include #include "qxmpp-exts/QXmppUploadManager.h" +#include // Kaidan +#include "AvatarFileStorage.h" +#include "EmojiModel.h" +#include "Enums.h" +#include "Globals.h" #include "Kaidan.h" -#include "RosterModel.h" +#include "Message.h" #include "MessageModel.h" -#include "AvatarFileStorage.h" #include "PresenceCache.h" -#include "UploadManager.h" -#include "Globals.h" -#include "Enums.h" -#include "StatusBar.h" -#include "EmojiModel.h" #include "QmlUtils.h" +#include "RosterModel.h" +#include "StatusBar.h" +#include "UploadManager.h" #ifdef STATIC_BUILD #include "static_plugins.h" #endif #ifndef QAPPLICATION_CLASS #define QAPPLICATION_CLASS QApplication #endif #include QT_STRINGIFY(QAPPLICATION_CLASS) #if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) // SingleApplication (Qt5 replacement for QtSingleApplication) #include "singleapp/singleapplication.h" #endif #ifdef STATIC_BUILD #define KIRIGAMI_BUILD_TYPE_STATIC #include "./3rdparty/kirigami/src/kirigamiplugin.h" #endif #ifdef Q_OS_ANDROID #include #endif #ifdef Q_OS_WIN #include #endif enum CommandLineParseResult { CommandLineOk, CommandLineError, CommandLineVersionRequested, CommandLineHelpRequested }; CommandLineParseResult parseCommandLine(QCommandLineParser &parser, QString *errorMessage) { // application description parser.setApplicationDescription(QString(APPLICATION_DISPLAY_NAME) + " - " + QString(APPLICATION_DESCRIPTION)); // add all possible arguments QCommandLineOption helpOption = parser.addHelpOption(); QCommandLineOption versionOption = parser.addVersionOption(); parser.addOption({"disable-xml-log", "Disable output of full XMPP XML stream."}); parser.addOption({{"m", "multiple"}, "Allow multiple instances to be started."}); parser.addPositionalArgument("xmpp-uri", "An XMPP-URI to open (i.e. join a chat).", "[xmpp-uri]"); // parse arguments if (!parser.parse(QGuiApplication::arguments())) { *errorMessage = parser.errorText(); return CommandLineError; } // check for special cases if (parser.isSet(versionOption)) return CommandLineVersionRequested; if (parser.isSet(helpOption)) return CommandLineHelpRequested; // if nothing special happened, return OK return CommandLineOk; } Q_DECL_EXPORT int main(int argc, char *argv[]) { #ifdef Q_OS_WIN if (AttachConsole(ATTACH_PARENT_PROCESS)) { freopen("CONOUT$", "w", stdout); freopen("CONOUT$", "w", stderr); } #endif // initialize random generator qsrand(time(nullptr)); // // App // #ifdef UBUNTU_TOUCH qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "true"); qputenv("QT_QUICK_CONTROLS_MOBILE", "true"); #endif // name, display name, description QGuiApplication::setApplicationName(APPLICATION_NAME); QGuiApplication::setApplicationDisplayName(APPLICATION_DISPLAY_NAME); QGuiApplication::setApplicationVersion(VERSION_STRING); // attributes QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // create a qt app #if defined(Q_OS_IOS) || defined(Q_OS_ANDROID) QGuiApplication app(argc, argv); #else SingleApplication app(argc, argv, true); #endif // register qMetaTypes + qRegisterMetaType("RosterItem"); qRegisterMetaType("RosterModel*"); + qRegisterMetaType("Message"); qRegisterMetaType("MessageModel*"); - qRegisterMetaType("Message"); qRegisterMetaType("AvatarFileStorage*"); - qRegisterMetaType("ContactMap"); qRegisterMetaType("PresenceCache*"); qRegisterMetaType("QXmppPresence"); qRegisterMetaType("Credentials"); qRegisterMetaType("Qt::ApplicationState"); qRegisterMetaType("QXmppClient::State"); qRegisterMetaType("MessageType"); qRegisterMetaType("DisconnectionReason"); qRegisterMetaType("TransferJob*"); qRegisterMetaType("QmlUtils*"); + qRegisterMetaType>("QVector"); + qRegisterMetaType>("QVector"); + qRegisterMetaType>("QHash"); + qRegisterMetaType>("std::function"); + qRegisterMetaType>("std::function"); // Qt-Translator QTranslator qtTranslator; qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); - app.installTranslator(&qtTranslator); + QCoreApplication::installTranslator(&qtTranslator); // Kaidan-Translator QTranslator kaidanTranslator; // load the systems locale or none from resources kaidanTranslator.load(QLocale::system().name(), ":/i18n"); - app.installTranslator(&kaidanTranslator); + QCoreApplication::installTranslator(&kaidanTranslator); // // Command line arguments // // create parser and add a description QCommandLineParser parser; // parse the arguments QString commandLineErrorMessage; switch (parseCommandLine(parser, &commandLineErrorMessage)) { case CommandLineError: qWarning() << commandLineErrorMessage; return 1; case CommandLineVersionRequested: parser.showVersion(); return 0; case CommandLineHelpRequested: parser.showHelp(); return 0; case CommandLineOk: break; } #if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) // check if another instance already runs if (app.isSecondary() && !parser.isSet("multiple")) { qDebug().noquote() << QString("Another instance of %1 is already running.") .arg(APPLICATION_DISPLAY_NAME) << "You can enable multiple instances by specifying '--multiple'."; // send a possible link to the primary instance if (!parser.positionalArguments().isEmpty()) app.sendMessage(parser.positionalArguments()[0].toUtf8()); return 0; } #endif // // Kaidan back-end // Kaidan kaidan(&app, !parser.isSet("disable-xml-log")); #if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) // receive messages from other instances of Kaidan - kaidan.connect(&app, &SingleApplication::receivedMessage, - &kaidan, &Kaidan::receiveMessage); + Kaidan::connect(&app, &SingleApplication::receivedMessage, + &kaidan, &Kaidan::receiveMessage); #endif // open the XMPP-URI/link (if given) if (!parser.positionalArguments().isEmpty()) kaidan.addOpenUri(parser.positionalArguments()[0].toUtf8()); // // QML-GUI // if (QIcon::themeName().isEmpty()) { QIcon::setThemeName("breeze"); } QQmlApplicationEngine engine; // QtQuickControls2 Style if (qgetenv("QT_QUICK_CONTROLS_STYLE").isEmpty()) { #ifdef Q_OS_WIN QString defaultStyle = "Universal"; #else QString defaultStyle = "Material"; #endif qDebug() << "QT_QUICK_CONTROLS_STYLE not set, setting to" << defaultStyle; qputenv("QT_QUICK_CONTROLS_STYLE", defaultStyle.toLatin1()); } // QML type bindings #ifdef STATIC_BUILD KirigamiPlugin::getInstance().registerTypes(); #endif qmlRegisterType("StatusBar", 0, 1, "StatusBar"); qmlRegisterType("EmojiModel", 0, 1, "EmojiModel"); qmlRegisterType("EmojiModel", 0, 1, "EmojiProxyModel"); qmlRegisterUncreatableType("EmojiModel", 0, 1, "QAbstractItemModel", "Used by proxy models"); qmlRegisterUncreatableType("EmojiModel", 0, 1, "Emoji", "Used by emoji models"); qmlRegisterUncreatableMetaObject(Enums::staticMetaObject, APPLICATION_ID, 1, 0, "Enums", "Can't create object; only enums defined!"); engine.rootContext()->setContextProperty("kaidan", &kaidan); engine.load(QUrl("qrc:/qml/main.qml")); if(engine.rootObjects().isEmpty()) return -1; #ifdef Q_OS_ANDROID QtAndroid::hideSplashScreen(); #endif // enter qt main loop return app.exec(); } diff --git a/src/qml/ChatPage.qml b/src/qml/ChatPage.qml index 6aa43be..7d36623 100644 --- a/src/qml/ChatPage.qml +++ b/src/qml/ChatPage.qml @@ -1,358 +1,355 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 . */ import QtQuick 2.6 import QtQuick.Controls 2.0 as Controls import QtQuick.Layouts 1.3 import org.kde.kirigami 2.2 as Kirigami import QtGraphicalEffects 1.0 import im.kaidan.kaidan 1.0 import EmojiModel 0.1 import "elements" Kirigami.ScrollablePage { property string chatName property string recipientJid property bool isWritingSpoiler property string messageToCorrect title: chatName keyboardNavigationEnabled: true actions.contextualActions: [ Kirigami.Action { visible: !isWritingSpoiler iconSource: "password-show-off" text: qsTr("Send a spoiler message") onTriggered: isWritingSpoiler = true } ] SendMediaSheet { id: sendMediaSheet } FileChooser { id: fileChooser title: qsTr("Select a file") onAccepted: { sendMediaSheet.jid = recipientJid sendMediaSheet.fileUrl = fileUrl sendMediaSheet.open() } } function openFileDialog(filterName, filter) { fileChooser.filterName = filterName fileChooser.filter = filter fileChooser.open() mediaDrawer.close() } Kirigami.OverlayDrawer { id: mediaDrawer edge: Qt.BottomEdge height: Kirigami.Units.gridUnit * 8 contentItem: RowLayout { id: content Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true IconButton { buttonText: qsTr("Image") iconSource: "image-jpeg" onClicked: openFileDialog("Images", "*.jpg *.jpeg *.png *.gif") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Video") iconSource: "video-mp4" onClicked: openFileDialog("Videos", "*.mp4 *.mkv *.avi *.webm") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Audio") iconSource: "audio-mp3" onClicked: openFileDialog("Audio files", "*.mp3 *.wav *.flac *.ogg *.m4a *.mka") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Document") iconSource: "x-office-document" onClicked: openFileDialog("Documents", "*.doc *.docx *.odt") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Other file") iconSource: "text-x-plain" onClicked: openFileDialog("All files", "*") Layout.alignment: Qt.AlignHCenter } } } background: Image { id: bgimage source: kaidan.utils.getResourcePath("images/chat.png") anchors.fill: parent fillMode: Image.Tile horizontalAlignment: Image.AlignLeft verticalAlignment: Image.AlignTop } // Chat ListView { verticalLayoutDirection: ListView.BottomToTop spacing: Kirigami.Units.smallSpacing * 2 // connect the database model: kaidan.messageModel delegate: ChatMessage { msgId: model.id - sentByMe: model.recipient !== kaidan.jid - messageBody: model.message + sentByMe: model.sentByMe + messageBody: model.body dateTime: new Date(model.timestamp) isRead: model.isDelivered - recipientAvatarUrl: kaidan.avatarStorage.getAvatarUrl(author) + recipientAvatarUrl: kaidan.avatarStorage.getAvatarUrl(model.author) name: chatName - mediaType: model.type + mediaType: model.mediaType mediaGetUrl: model.mediaUrl mediaLocation: model.mediaLocation - isLastMessage: model.id === kaidan.messageModel.lastMessageId(recipientJid) - edited: model.edited + edited: model.isEdited isSpoiler: model.isSpoiler isShowingSpoiler: false spoilerHint: model.spoilerHint onMessageEditRequested: { messageToCorrect = id messageField.text = body messageField.state = "edit" } } } // Message Writing footer: Controls.Pane { id: sendingArea layer.enabled: sendingArea.enabled layer.effect: DropShadow { verticalOffset: 1 color: Kirigami.Theme.disabledTextColor samples: 20 spread: 0.3 cached: true // element is static } padding: 0 wheelEnabled: true background: Rectangle { color: Kirigami.Theme.backgroundColor } RowLayout { anchors.fill: parent Layout.preferredHeight: Kirigami.Units.gridUnit * 3 Controls.ToolButton { id: attachButton visible: kaidan.uploadServiceFound Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: "document-send-symbolic" isMask: true smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: { if (Kirigami.Settings.isMobile) mediaDrawer.open() else openFileDialog("All files", "(*)") } } ColumnLayout { Layout.minimumHeight: messageField.height + Kirigami.Units.smallSpacing * 2 Layout.fillWidth: true spacing: 0 RowLayout { visible: isWritingSpoiler Controls.TextArea { id: spoilerHintField Layout.fillWidth: true placeholderText: qsTr("Spoiler hint") wrapMode: Controls.TextArea.Wrap selectByMouse: true background: Item {} } Controls.ToolButton { Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5 Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5 padding: 0 Kirigami.Icon { source: "tab-close" smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 1.5 height: width } onClicked: { isWritingSpoiler = false spoilerHintField.text = "" } } } Kirigami.Separator { visible: isWritingSpoiler Layout.fillWidth: true } Controls.TextArea { id: messageField Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter placeholderText: qsTr("Compose message") wrapMode: Controls.TextArea.Wrap selectByMouse: true background: Item {} state: "compose" states: [ State { name: "compose" }, State { name: "edit" } ] Keys.onReturnPressed: { if (event.key === Qt.Key_Return) { if (event.modifiers & Qt.ControlModifier) { messageField.append("") } else { sendButton.onClicked() event.accepted = true } } } } } EmojiPicker { x: -width + parent.width y: -height - 16 width: Kirigami.Units.gridUnit * 20 height: Kirigami.Units.gridUnit * 15 id: emojiPicker model: EmojiProxyModel { group: Emoji.Group.People sourceModel: EmojiModel {} } textArea: messageField } Controls.ToolButton { id: emojiPickerButton Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: "preferences-desktop-emoticons" enabled: sendButton.enabled - color: "transparent" isMask: false smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open() } Controls.ToolButton { id: sendButton Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: { if (messageField.state == "compose") return "document-send" else if (messageField.state == "edit") return "edit-symbolic" } enabled: sendButton.enabled - color: "transparent" isMask: true smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: { // don't send empty messages if (!messageField.text.length) { return } // disable the button to prevent sending // the same message several times sendButton.enabled = false // send the message if (messageField.state == "compose") { kaidan.sendMessage(recipientJid, messageField.text, isWritingSpoiler, spoilerHintField.text) } else if (messageField.state == "edit") { kaidan.correctMessage(recipientJid, messageToCorrect, messageField.text) } // clean up the text fields messageField.text = "" messageField.state = "compose" spoilerHintField.text = "" isWritingSpoiler = false // reenable the button sendButton.enabled = true } } } } } diff --git a/src/qml/RosterPage.qml b/src/qml/RosterPage.qml index 7d40372..fcb5ec2 100644 --- a/src/qml/RosterPage.qml +++ b/src/qml/RosterPage.qml @@ -1,132 +1,126 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 . */ import QtQuick 2.7 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 as Controls import org.kde.kirigami 2.0 as Kirigami import im.kaidan.kaidan 1.0 import "elements" Kirigami.ScrollablePage { title: { kaidan.connectionState === Enums.StateConnecting ? qsTr("Connecting…") : kaidan.connectionState === Enums.StateDisconnected ? qsTr("Offline") : qsTr("Contacts") } RosterAddContactSheet { id: addContactSheet jid: "" } RosterRemoveContactSheet { id: removeContactSheet jid: "" } mainAction: Kirigami.Action { text: qsTr("Add new contact") iconName: "contact-new" onTriggered: { if (addContactSheet.sheetOpen) addContactSheet.close() else addContactSheet.open() } } ListView { verticalLayoutDirection: ListView.TopToBottom model: kaidan.rosterModel delegate: RosterListItem { id: rosterItem name: model.name ? model.name : model.jid jid: model.jid lastMessage: model.lastMessage presenceType: kaidan.presenceCache.getPresenceType(model.jid) statusMsg: kaidan.presenceCache.getStatusText(model.jid) unreadMessages: model.unreadMessages avatarImagePath: kaidan.avatarStorage.getAvatarUrl(model.jid) backgroundColor: { if (!Kirigami.Settings.isMobile && kaidan.chatPartner === model.jid) { Kirigami.Theme.highlightColor } else { Kirigami.Theme.backgroundColor } } onClicked: { - // first push the chat page + kaidan.messageModel.chatPartner = model.jid pageStack.push(chatPage, { "chatName": (model.name ? model.name : model.jid), "recipientJid": model.jid }) - - // then set the message filter for this jid - // this will update the unread message count, - // which will update the roster and will reset the - // model variable - kaidan.chatPartner = model.jid } actions: [ Kirigami.Action { iconName: "bookmark-remove" onTriggered: { removeContactSheet.jid = model.jid; removeContactSheet.open(); } } ] function newPresenceArrived(jid) { if (jid === model.jid) { rosterItem.presenceType = kaidan.presenceCache. getPresenceType(model.jid) rosterItem.statusMsg = kaidan.presenceCache. getStatusText(model.jid) } } 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) } } } } diff --git a/src/qml/elements/ChatMessage.qml b/src/qml/elements/ChatMessage.qml index 675a2c7..3e994e3 100644 --- a/src/qml/elements/ChatMessage.qml +++ b/src/qml/elements/ChatMessage.qml @@ -1,299 +1,301 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 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 . */ import QtQuick 2.6 import QtGraphicalEffects 1.0 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.2 as Controls import org.kde.kirigami 2.0 as Kirigami import im.kaidan.kaidan 1.0 RowLayout { id: root property string msgId property bool sentByMe: true property string messageBody property date dateTime property bool isRead: false property string recipientAvatarUrl property int mediaType property string mediaGetUrl property string mediaLocation - property bool isLastMessage property bool edited property bool isLoading: kaidan.transferCache.hasUpload(msgId) property string name property var upload: { if (mediaType !== Enums.MessageText && kaidan.transferCache.hasUpload(msgId)) { kaidan.transferCache.jobByMessageId(model.id) } } property bool isSpoiler - property bool isShowingSpoiler property string spoilerHint + property bool isShowingSpoiler: false signal messageEditRequested(string id, string body) // own messages are on the right, others on the left layoutDirection: sentByMe ? Qt.RightToLeft : Qt.LeftToRight spacing: 8 width: parent.width // placeholder Item { Layout.preferredWidth: 5 } RoundImage { id: avatar visible: !sentByMe && recipientAvatarUrl !== "" source: recipientAvatarUrl fillMode: Image.PreserveAspectFit mipmap: true height: width Layout.alignment: Qt.AlignHCenter | Qt.AlignTop Layout.preferredHeight: Kirigami.Units.gridUnit * 2.2 Layout.preferredWidth: Kirigami.Units.gridUnit * 2.2 sourceSize.height: Kirigami.Units.gridUnit * 2.2 sourceSize.width: Kirigami.Units.gridUnit * 2.2 } TextAvatar { id: textAvatar visible: !sentByMe && recipientAvatarUrl == "" name: root.name Layout.alignment: Qt.AlignHCenter | Qt.AlignTop Layout.preferredHeight: Kirigami.Units.gridUnit * 2.2 Layout.preferredWidth: Kirigami.Units.gridUnit * 2.2 } // message bubble/box Item { Layout.preferredWidth: content.width + 13 Layout.preferredHeight: content.height + 8 Rectangle { id: box anchors.fill: parent color: sentByMe ? Kirigami.Theme.complementaryTextColor : Kirigami.Theme.highlightColor radius: Kirigami.Units.smallSpacing * 2 layer.enabled: box.visible layer.effect: DropShadow { verticalOffset: Kirigami.Units.gridUnit * 0.08 horizontalOffset: Kirigami.Units.gridUnit * 0.08 color: Kirigami.Theme.disabledTextColor samples: 10 spread: 0.1 } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { if (mouse.button === Qt.RightButton) contextMenu.popup() } onPressAndHold: { contextMenu.popup() } } Controls.Menu { id: contextMenu Controls.MenuItem { text: qsTr("Copy Message") onTriggered: isShowingSpoiler ? kaidan.utils.copyToClipboard(messageBody) : kaidan.copyToClipboard(spoilerHint) } Controls.MenuItem { text: qsTr("Edit Message") - enabled: isLastMessage && sentByMe + enabled: kaidan.messageModel.canCorrectMessage(msgId) onTriggered: root.messageEditRequested(msgId, messageBody) } } } ColumnLayout { id: content spacing: 0 anchors.centerIn: parent anchors.margins: 4 RowLayout { id: spoilerHintRow visible: isSpoiler MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { if (mouse.button === Qt.LeftButton) { isShowingSpoiler = !isShowingSpoiler } } } Controls.Label { id: dateLabeltest text: spoilerHint == "" ? qsTr("Spoiler") : spoilerHint color: sentByMe ? Kirigami.Theme.buttonTextColor : Kirigami.Theme.complementaryTextColor font.pixelSize: Kirigami.Units.gridUnit * 0.8 } Item { Layout.fillWidth: true height: 1 } Kirigami.Icon { height: 28 width: 28 source: isShowingSpoiler ? "password-show-off" : "password-show-on" color: sentByMe ? Kirigami.Theme.buttonTextColor : Kirigami.Theme.complementaryTextColor } } Kirigami.Separator { visible: isSpoiler Layout.fillWidth: true color: { var bgColor = sentByMe ? Kirigami.Theme.backgroundColor : Kirigami.Theme.highlightColor var textColor = sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.buttonTextColor return Qt.tint(textColor, Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.7)) } } ColumnLayout { visible: isSpoiler && isShowingSpoiler || !isSpoiler Controls.ToolButton { visible: { - (mediaType !== Enums.MessageText && !isLoading && mediaLocation === "") + mediaType !== Enums.MessageText && + !isLoading && + mediaLocation === "" && + mediaGetUrl !== "" } text: qsTr("Download") onClicked: { - print("Donwload") + print("Downloading " + mediaGetUrl + "...") kaidan.downloadMedia(msgId, mediaGetUrl) } } // media loader Loader { id: media source: { - if (mediaType == Enums.MessageImage && + if (mediaType === Enums.MessageImage && mediaLocation !== "") "ChatMessageImage.qml" else "" } property string sourceUrl: "file://" + mediaLocation Layout.maximumWidth: root.width - Kirigami.Units.gridUnit * 6 Layout.preferredHeight: loaded ? item.paintedHeight : 0 } // message body Controls.Label { id: messageLabel visible: messageBody !== "" text: kaidan.utils.formatMessage(messageBody) textFormat: Text.StyledText wrapMode: Text.Wrap color: sentByMe ? Kirigami.Theme.buttonTextColor : Kirigami.Theme.complementaryTextColor onLinkActivated: Qt.openUrlExternally(link) Layout.maximumWidth: mediaType === Enums.MessageImage && media.width !== 0 ? media.width : root.width - Kirigami.Units.gridUnit * 6 } Kirigami.Separator { visible: isSpoiler && isShowingSpoiler Layout.fillWidth: true color: { var bgColor = sentByMe ? Kirigami.Theme.backgroundColor : Kirigami.Theme.highlightColor var textColor = sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.buttonTextColor return Qt.tint(textColor, Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.7)) } } } // message meta: date, isRead RowLayout { // progress bar for upload/download status Controls.ProgressBar { visible: isLoading - value: upload.progress + value: isLoading ? upload.progress : 0 } Controls.Label { id: dateLabel text: Qt.formatDateTime(dateTime, "dd. MMM yyyy, hh:mm") color: sentByMe ? Kirigami.Theme.disabledTextColor : Qt.darker(Kirigami.Theme.disabledTextColor, 1.3) font.pixelSize: Kirigami.Units.gridUnit * 0.8 } Image { id: checkmark visible: (sentByMe && isRead) source: kaidan.utils.getResourcePath("images/message_checkmark.svg") Layout.preferredHeight: Kirigami.Units.gridUnit * 0.65 Layout.preferredWidth: Kirigami.Units.gridUnit * 0.65 sourceSize.height: Kirigami.Units.gridUnit * 0.65 sourceSize.width: Kirigami.Units.gridUnit * 0.65 } Kirigami.Icon { source: "edit-symbolic" visible: edited Layout.preferredHeight: Kirigami.Units.gridUnit * 0.65 Layout.preferredWidth: Kirigami.Units.gridUnit * 0.65 } } } } // placeholder Item { Layout.fillWidth: true } function updateIsLoading() { isLoading = kaidan.transferCache.hasUpload(msgId) } Component.onCompleted: { kaidan.transferCache.jobsChanged.connect(updateIsLoading) } Component.onDestruction: { kaidan.transferCache.jobsChanged.disconnect(updateIsLoading) } }