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