diff --git a/src/RosterManager.cpp b/src/RosterManager.cpp index 79f645a..e050d47 100644 --- a/src/RosterManager.cpp +++ b/src/RosterManager.cpp @@ -1,209 +1,209 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2020 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Kaidan. If not, see . */ #include "RosterManager.h" // Kaidan #include "ClientWorker.h" #include "Globals.h" #include "Kaidan.h" #include "VCardManager.h" // QXmpp #include #include #include RosterManager::RosterManager(Kaidan *kaidan, QXmppClient *client, RosterModel *model, AvatarFileStorage *avatarStorage, VCardManager *vCardManager, QObject *parent) : QObject(parent), kaidan(kaidan), client(client), model(model), avatarStorage(avatarStorage), vCardManager(vCardManager), manager(client->findExtension()) { connect(manager, &QXmppRosterManager::rosterReceived, this, &RosterManager::populateRoster); connect(manager, &QXmppRosterManager::itemAdded, this, [this, vCardManager, model] (const QString &jid) { emit model->addItemRequested(RosterItem(manager->getRosterEntry(jid))); vCardManager->fetchVCard(jid); }); connect(manager, &QXmppRosterManager::itemChanged, this, [this, model] (const QString &jid) { - emit model->updateItemRequested(m_chatPartner, [=] (RosterItem &item) { + emit model->updateItemRequested(jid, [=] (RosterItem &item) { item.setName(manager->getRosterEntry(jid).name()); }); }); connect(manager, &QXmppRosterManager::itemRemoved, model, &RosterModel::removeItemRequested); connect(manager, &QXmppRosterManager::subscriptionReceived, this, [kaidan] (const QString &jid) { // emit signal to ask user emit kaidan->subscriptionRequestReceived(jid, QString()); }); connect(kaidan, &Kaidan::subscriptionRequestAnswered, this, [=] (QString jid, bool accepted) { if (accepted) { manager->acceptSubscription(jid); // do not send a subscription request if both users have already subscribed // each others presence if (manager->getRosterEntry(jid).subscriptionType() != QXmppRosterIq::Item::Both) manager->subscribe(jid); } else { manager->refuseSubscription(jid); } }); // update local copy of chat partner connect(kaidan->getMessageModel(), &MessageModel::chatPartnerChanged, this, [=] (const QString &jid) { m_chatPartner = jid; } ); // user actions connect(kaidan, &Kaidan::addContact, this, &RosterManager::addContact); connect(kaidan, &Kaidan::removeContact, this, &RosterManager::removeContact); connect(kaidan, &Kaidan::renameContact, this, &RosterManager::renameContact); connect(kaidan, &Kaidan::sendMessage, this, &RosterManager::handleSendMessage); connect(client, &QXmppClient::messageReceived, this, &RosterManager::handleMessage); } void RosterManager::populateRoster() { qDebug() << "[client] [RosterManager] Populating roster"; // create a new list of contacts QHash items; const QStringList bareJids = manager->getRosterBareJids(); const auto currentTime = QDateTime::currentDateTimeUtc(); for (const auto &jid : bareJids) { items[jid] = RosterItem(manager->getRosterEntry(jid), currentTime); if (avatarStorage->getHashOfJid(jid).isEmpty()) vCardManager->fetchVCard(jid); } // replace current contacts with new ones from server emit model->replaceItemsRequested(items); } void RosterManager::addContact(const QString &jid, const QString &name, const QString &msg) { if (client->state() == QXmppClient::ConnectedState) { manager->addItem(jid, name); manager->subscribe(jid, msg); } else { emit kaidan->passiveNotificationRequested( tr("Could not add contact, as a result of not being connected.") ); qWarning() << "[client] [RosterManager] Could not add contact, as a result of " "not being connected."; } } void RosterManager::removeContact(const QString &jid) { if (client->state() == QXmppClient::ConnectedState) { manager->unsubscribe(jid); manager->removeItem(jid); } else { emit kaidan->passiveNotificationRequested( tr("Could not remove contact, as a result of not being connected.") ); qWarning() << "[client] [RosterManager] Could not remove contact, as a result of " "not being connected."; } } void RosterManager::renameContact(const QString &jid, const QString &newContactName) { if (client->state() == QXmppClient::ConnectedState) { manager->renameItem(jid, newContactName); } else { emit kaidan->passiveNotificationRequested( tr("Could not rename contact, as a result of not being connected.") ); qWarning() << "[client] [RosterManager] Could not rename contact, as a result of " "not being connected."; } } void RosterManager::handleSendMessage(const QString &jid, const QString &message, bool isSpoiler, const QString &spoilerHint) { if (client->state() == QXmppClient::ConnectedState) { // update roster item const QString lastMessage = isSpoiler ? spoilerHint.isEmpty() ? tr("Spoiler") : spoilerHint : message; // sorting order in contact list emit model->setLastExchangedRequested(jid, QDateTime::currentDateTimeUtc()); emit model->setLastMessageRequested(jid, lastMessage); } } void RosterManager::handleMessage(const QXmppMessage &msg) { if (msg.body().isEmpty() || msg.type() == QXmppMessage::Error) return; // msg.from() can be our JID, if it's a carbon/forward from another client QString fromJid = QXmppUtils::jidToBareJid(msg.from()); bool sentByMe = fromJid == client->configuration().jidBare(); QString contactJid = sentByMe ? QXmppUtils::jidToBareJid(msg.to()) : fromJid; // update last exchanged datetime (sorting order in contact list) emit model->setLastExchangedRequested(contactJid, QDateTime::currentDateTimeUtc()); // update unread message counter, if chat is not active if (sentByMe) { // if we sent a message (with another device), reset counter emit model->updateItemRequested(contactJid, [](RosterItem &item) { item.setUnreadMessages(0); }); } else if (m_chatPartner != contactJid) { emit model->updateItemRequested(contactJid, [](RosterItem &item) { item.setUnreadMessages(item.unreadMessages() + 1); }); } } diff --git a/src/RosterModel.cpp b/src/RosterModel.cpp index 24b90ba..9177486 100644 --- a/src/RosterModel.cpp +++ b/src/RosterModel.cpp @@ -1,278 +1,280 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2020 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Kaidan. If not, see . */ #include "RosterModel.h" // Kaidan #include "RosterDb.h" #include "MessageModel.h" #include "Kaidan.h" // C++ #include // Qt #include #include #include #include RosterModel::RosterModel(RosterDb *rosterDb, QObject *parent) : QAbstractListModel(parent), rosterDb(rosterDb) { connect(rosterDb, &RosterDb::itemsFetched, this, &RosterModel::handleItemsFetched); connect(this, &RosterModel::addItemRequested, this, &RosterModel::addItem); connect(this, &RosterModel::addItemRequested, rosterDb, &RosterDb::addItem); connect(this, &RosterModel::removeItemRequested, this, &RosterModel::removeItem); connect(this, &RosterModel::removeItemRequested, rosterDb, &RosterDb::removeItem); connect(this, &RosterModel::updateItemRequested, this, &RosterModel::updateItem); connect(this, &RosterModel::updateItemRequested, rosterDb, &RosterDb::updateItem); connect(this, &RosterModel::replaceItemsRequested, this, &RosterModel::replaceItems); connect(this, &RosterModel::replaceItemsRequested, rosterDb, &RosterDb::replaceItems); // This is only done in the model, the database is updated automatically by the new // messages: connect(this, &RosterModel::setLastMessageRequested, this, &RosterModel::setLastMessage); connect(this, &RosterModel::setLastExchangedRequested, this, &RosterModel::setLastExchanged); connect(Kaidan::instance(), &Kaidan::jidChanged, this, [=]() { beginResetModel(); m_items.clear(); endResetModel(); emit rosterDb->fetchItemsRequested(Kaidan::instance()->getJid()); }); } void RosterModel::setMessageModel(MessageModel *model) { connect(model, &MessageModel::chatPartnerChanged, this, [=] (const QString &chatPartner) { // reset unread message counter emit updateItemRequested(chatPartner, [] (RosterItem &item) { item.setUnreadMessages(0); }); }); } bool RosterModel::isEmpty() const { return m_items.isEmpty(); } int RosterModel::rowCount(const QModelIndex&) const { return m_items.length(); } QHash RosterModel::roleNames() const { QHash roles; roles[JidRole] = "jid"; roles[NameRole] = "name"; roles[LastExchangedRole] = "lastExchanged"; roles[UnreadMessagesRole] = "unreadMessages"; roles[LastMessageRole] = "lastMessage"; return roles; } QVariant RosterModel::data(const QModelIndex &index, int role) const { if (!hasIndex(index.row(), index.column(), index.parent())) { qWarning() << "Could not get data from roster model." << index << role; return {}; } switch (role) { case JidRole: return m_items.at(index.row()).jid(); case NameRole: return m_items.at(index.row()).name(); case LastExchangedRole: return m_items.at(index.row()).lastExchanged(); case UnreadMessagesRole: return m_items.at(index.row()).unreadMessages(); case LastMessageRole: return m_items.at(index.row()).lastMessage(); } return {}; } void RosterModel::handleItemsFetched(const QVector &items) { beginResetModel(); m_items = items; std::sort(m_items.begin(), m_items.end()); endResetModel(); } void RosterModel::addItem(const RosterItem &item) { insertContact(positionToInsert(item), item); } void RosterModel::removeItem(const QString &jid) { QMutableVectorIterator itr(m_items); int i = 0; while (itr.hasNext()) { if (itr.next().jid() == jid) { beginRemoveRows(QModelIndex(), i, i); itr.remove(); endRemoveRows(); return; } i++; } } void RosterModel::updateItem(const QString &jid, const std::function &updateItem) { for (int i = 0; i < m_items.length(); i++) { if (m_items.at(i).jid() == jid) { // update item RosterItem item = m_items.at(i); updateItem(item); // check if item was actually modified if (m_items.at(i) == item) return; + m_items.replace(i, item); + // item was changed: refresh all roles emit dataChanged(index(i), index(i), {}); // check, if the position of the new item may be different updateItemPosition(i); return; } } } void RosterModel::replaceItems(const QHash &items) { QVector newItems; for (auto item : qAsConst(items)) { // find old item auto oldItem = std::find_if( m_items.begin(), m_items.end(), [&] (const RosterItem &oldItem) { return oldItem.jid() == item.jid(); } ); // use the old item's values, if found if (oldItem != m_items.end()) { item.setLastMessage(oldItem->lastMessage()); item.setLastExchanged(oldItem->lastExchanged()); item.setUnreadMessages(oldItem->unreadMessages()); } newItems << item; } // replace all items handleItemsFetched(newItems); } void RosterModel::setLastMessage(const QString &contactJid, const QString &newLastMessage) { for (int i = 0; i < m_items.length(); i++) { if (m_items.at(i).jid() == contactJid) { m_items[i].setLastMessage(newLastMessage); emit dataChanged(index(i), index(i), QVector() << int(LastMessageRole)); return; } } } void RosterModel::setLastExchanged(const QString &contactJid, const QDateTime &newLastExchanged) { for (int i = 0; i < m_items.length(); i++) { if (m_items.at(i).jid() == contactJid) { // update item m_items[i].setLastExchanged(newLastExchanged); emit dataChanged(index(i), index(i), QVector() << int(LastExchangedRole)); // Move row to correct position updateItemPosition(i); return; } } } void RosterModel::insertContact(int i, const RosterItem &item) { beginInsertRows(QModelIndex(), i, i); m_items.insert(i, item); endInsertRows(); } int RosterModel::updateItemPosition(int currentPosition) { const int newPosition = positionToInsert(m_items.at(currentPosition)); if (currentPosition == newPosition) return currentPosition; beginMoveRows(QModelIndex(), currentPosition, currentPosition, QModelIndex(), newPosition); m_items.move(currentPosition, newPosition); endMoveRows(); return newPosition; } int RosterModel::positionToInsert(const RosterItem &item) { // prepend the item, if no timestamp is set if (item.lastExchanged().isNull()) return 0; for (int i = 0; i < m_items.size(); i++) { if (item <= m_items.at(i)) return i; } // append return m_items.size(); }