diff --git a/src/MessageDb.cpp b/src/MessageDb.cpp index f5a46ee..adfcedf 100644 --- a/src/MessageDb.cpp +++ b/src/MessageDb.cpp @@ -1,290 +1,295 @@ /* * 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.id() != newMsg.id()) { + // TODO: remove as soon as 'NOT NULL' was removed from id column + if (newMsg.id().isEmpty()) + rec.append(Utils::createSqlField("id", QStringLiteral(" "))); + else + rec.append(Utils::createSqlField("id", newMsg.id())); + } if (oldMsg.body() != newMsg.body()) rec.append(Utils::createSqlField("message", newMsg.body())); if (oldMsg.isSent() != newMsg.isSent()) rec.append(Utils::createSqlField("isSent", newMsg.isSent())); if (oldMsg.isDelivered() != newMsg.isDelivered()) rec.append(Utils::createSqlField("isDelivered", newMsg.isDelivered())); if (oldMsg.mediaType() != newMsg.mediaType()) rec.append(Utils::createSqlField("type", int(newMsg.mediaType()))); if (oldMsg.outOfBandUrl() != newMsg.outOfBandUrl()) rec.append(Utils::createSqlField("mediaUrl", newMsg.outOfBandUrl())); if (oldMsg.mediaContentType() != newMsg.mediaContentType()) rec.append(Utils::createSqlField( "mediaContentType", newMsg.mediaContentType() )); if (oldMsg.mediaLocation() != newMsg.mediaLocation()) rec.append(Utils::createSqlField( "mediaLocation", newMsg.mediaLocation() )); if (oldMsg.mediaSize() != newMsg.mediaSize()) rec.append(Utils::createSqlField("mediaSize", newMsg.mediaSize())); if (oldMsg.mediaLastModified() != newMsg.mediaLastModified()) rec.append(Utils::createSqlField( "mediaLastModified", newMsg.mediaLastModified().toMSecsSinceEpoch() )); if (oldMsg.isEdited() != newMsg.isEdited()) rec.append(Utils::createSqlField("edited", newMsg.isEdited())); if (oldMsg.spoilerHint() != newMsg.spoilerHint()) rec.append(Utils::createSqlField("spoilerHint", newMsg.spoilerHint())); if (oldMsg.isSpoiler() != newMsg.isSpoiler()) rec.append(Utils::createSqlField("isSpoiler", newMsg.isSpoiler())); return rec; } void MessageDb::fetchMessages(const QString &user1, const QString &user2, int index) { QSqlQuery query(QSqlDatabase::database(DB_CONNECTION)); query.setForwardOnly(true); QMap bindValues; bindValues[":user1"] = user1; bindValues[":user2"] = user2; bindValues[":index"] = index; bindValues[":limit"] = DB_MSG_QUERY_LIMIT; Utils::execQuery( query, "SELECT * FROM Messages " "WHERE (author = :user1 AND recipient = :user2) OR " "(author = :user2 AND recipient = :user1) " "ORDER BY timestamp DESC " "LIMIT :index, :limit", bindValues ); QVector messages; parseMessagesFromQuery(query, messages); emit messagesFetched(messages); } void MessageDb::addMessage(const Message &msg) { QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlRecord record = db.record(DB_TABLE_MESSAGES); record.setValue("author", msg.from()); record.setValue("recipient", msg.to()); record.setValue("timestamp", msg.stamp().toString(Qt::ISODate)); record.setValue("message", msg.body()); record.setValue("id", msg.id().isEmpty() ? " " : msg.id()); record.setValue("isSent", msg.isSent()); record.setValue("isDelivered", msg.isDelivered()); record.setValue("type", int(msg.mediaType())); record.setValue("edited", msg.isEdited()); record.setValue("isSpoiler", msg.isSpoiler()); record.setValue("spoilerHint", msg.spoilerHint()); record.setValue("mediaUrl", msg.outOfBandUrl()); record.setValue("mediaContentType", msg.mediaContentType()); record.setValue("mediaLocation", msg.mediaLocation()); record.setValue("mediaSize", msg.mediaSize()); record.setValue("mediaLastModified", msg.mediaLastModified().toMSecsSinceEpoch()); QSqlQuery query(db); Utils::execQuery(query, db.driver()->sqlStatement( QSqlDriver::InsertStatement, DB_TABLE_MESSAGES, record, false )); } void MessageDb::removeMessage(const QString &id) { QSqlQuery query(QSqlDatabase::database(DB_CONNECTION)); Utils::execQuery( query, "DELETE FROM Messages WHERE id = ?", QVector() << id ); } void MessageDb::updateMessage(const QString &id, const std::function &updateMsg) { // load current message item from db QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlQuery query(db); query.setForwardOnly(true); Utils::execQuery( query, "SELECT * FROM Messages WHERE id = ? LIMIT 1", QVector() << id ); QVector msgs; parseMessagesFromQuery(query, msgs); // update loaded item if (!msgs.isEmpty()) { Message msg = msgs.first(); updateMsg(msg); // replace old message with updated one, if message has changed if (msgs.first() != msg) { // create an SQL record with only the differences QSqlRecord rec = createUpdateRecord(msgs.first(), msg); Utils::execQuery( query, db.driver()->sqlStatement( QSqlDriver::UpdateStatement, DB_TABLE_MESSAGES, rec, false ) + Utils::simpleWhereStatement(db.driver(), "id", id) ); } } } void MessageDb::updateMessageRecord(const QString &id, const QSqlRecord &updateRecord) { QSqlDatabase db = QSqlDatabase::database(DB_CONNECTION); QSqlQuery query(db); Utils::execQuery( query, db.driver()->sqlStatement( QSqlDriver::UpdateStatement, DB_TABLE_MESSAGES, updateRecord, false ) + Utils::simpleWhereStatement(db.driver(), "id", id) ); } void MessageDb::setMessageAsSent(const QString &msgId) { QSqlRecord rec; rec.append(Utils::createSqlField("isSent", true)); updateMessageRecord(msgId, rec); } void MessageDb::setMessageAsDelivered(const QString &msgId) { QSqlRecord rec; rec.append(Utils::createSqlField("isDelivered", true)); updateMessageRecord(msgId, rec); } diff --git a/src/RosterDb.cpp b/src/RosterDb.cpp index 31a5c3b..ce14c6c 100644 --- a/src/RosterDb.cpp +++ b/src/RosterDb.cpp @@ -1,256 +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) + "lastExchanged", + newItem.lastExchanged().toString(Qt::ISODateWithMs) )); if (oldItem.unreadMessages() != newItem.unreadMessages()) rec.append(Utils::createSqlField( - "unreadMessages", - newItem.unreadMessages() + "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"); + Utils::execQuery(query, "SELECT * FROM Roster"); QVector items; parseItemsFromQuery(query, items); emit itemsFetched(items); } diff --git a/src/RosterModel.cpp b/src/RosterModel.cpp index f89ab09..e18fdd1 100644 --- a/src/RosterModel.cpp +++ b/src/RosterModel.cpp @@ -1,237 +1,244 @@ /* * 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" // Kaidan #include "RosterDb.h" #include "MessageModel.h" // C++ #include // Qt #include #include #include #include RosterModel::RosterModel(RosterDb *rosterDb, QObject *parent) : QAbstractListModel(parent), rosterDb(rosterDb) { connect(rosterDb, &RosterDb::itemsFetched, this, &RosterModel::handleItemsFetched); connect(this, &RosterModel::addItemRequested, this, &RosterModel::addItem); connect(this, &RosterModel::addItemRequested, rosterDb, &RosterDb::addItem); connect(this, &RosterModel::removeItemRequested, this, &RosterModel::removeItem); connect(this, &RosterModel::removeItemRequested, rosterDb, &RosterDb::removeItem); connect(this, &RosterModel::updateItemRequested, this, &RosterModel::updateItem); connect(this, &RosterModel::updateItemRequested, rosterDb, &RosterDb::updateItem); connect(this, &RosterModel::replaceItemsRequested, this, &RosterModel::replaceItems); connect(this, &RosterModel::replaceItemsRequested, rosterDb, &RosterDb::replaceItems); emit rosterDb->fetchItemsRequested(); } void RosterModel::setMessageModel(MessageModel *model) { connect(model, &MessageModel::chatPartnerChanged, this, [=] (const QString &chatPartner) { // reset unread message counter emit updateItemRequested(chatPartner, [] (RosterItem &item) { item.setUnreadMessages(0); }); }); } bool RosterModel::isEmpty() const { return m_items.isEmpty(); } int RosterModel::rowCount(const QModelIndex&) const { return m_items.length(); } QHash RosterModel::roleNames() const { QHash roles; roles[JidRole] = "jid"; roles[NameRole] = "name"; roles[LastExchangedRole] = "lastExchanged"; roles[UnreadMessagesRole] = "unreadMessages"; roles[LastMessageRole] = "lastMessage"; return roles; } QVariant RosterModel::data(const QModelIndex &index, int role) const { if (!hasIndex(index.row(), index.column(), index.parent())) { qWarning() << "Could not get data from roster model." << index << role; return {}; } switch (role) { case JidRole: return m_items.at(index.row()).jid(); case NameRole: return m_items.at(index.row()).name(); case LastExchangedRole: return m_items.at(index.row()).lastExchanged(); case UnreadMessagesRole: return m_items.at(index.row()).unreadMessages(); case LastMessageRole: return m_items.at(index.row()).lastMessage(); } return {}; } void RosterModel::handleItemsFetched(const QVector &items) { beginResetModel(); m_items = items; + std::sort( + m_items.begin(), + m_items.end(), + [] (const RosterItem &a, const RosterItem &b) { + return a.lastExchanged() > b.lastExchanged(); + } + ); endResetModel(); } void RosterModel::addItem(const RosterItem &item) { // prepend the item, if no timestamp is set if (item.lastExchanged().isNull()) { insertContact(0, item); return; } // index where to add the new contact int i = 0; for (const auto &itrItem : qAsConst(m_items)) { if (item.lastExchanged().toMSecsSinceEpoch() >= itrItem.lastExchanged().toMSecsSinceEpoch()) { insertContact(i, item); return; } i++; } // append the item to the end of the list insertContact(i, item); } void RosterModel::removeItem(const QString &jid) { QMutableVectorIterator itr(m_items); int i = 0; while (itr.hasNext()) { if (itr.next().jid() == jid) { beginRemoveRows(QModelIndex(), i, i); itr.remove(); endRemoveRows(); return; } i++; } } void RosterModel::updateItem(const QString &jid, const std::function &updateItem) { for (int i = 0; i < m_items.length(); i++) { if (m_items.at(i).jid() == jid) { // update item RosterItem item = m_items.at(i); updateItem(item); // check if item was actually modified if (m_items.at(i) == item) return; // 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; } } } void RosterModel::replaceItems(const QHash &items) { QVector newItems; for (auto item : items) { // find old item auto oldItem = std::find_if( m_items.begin(), m_items.end(), [&] (const RosterItem &oldItem) { return oldItem.jid() == item.jid(); } ); // use the old item's values, if found if (oldItem != m_items.end()) { item.setLastMessage(oldItem->lastMessage()); item.setLastExchanged(oldItem->lastExchanged()); item.setUnreadMessages(oldItem->unreadMessages()); } newItems << item; } // replace all items handleItemsFetched(newItems); } void RosterModel::insertContact(int i, const RosterItem &item) { beginInsertRows(QModelIndex(), i, i); m_items.insert(i, item); endInsertRows(); }