From f10d8a4045b03e3618148a2d239c49dc8bf631fd Mon Sep 17 00:00:00 2001 From: Slava Monich Date: Sun, 27 Sep 2020 17:18:14 +0300 Subject: [PATCH] Fix chat ordering and optimize chat data storage --- qml/pages/OverviewPage.qml | 1 - src/chatlistmodel.cpp | 325 ++++++++++++++++++++----------------- src/chatlistmodel.h | 19 +-- 3 files changed, 183 insertions(+), 162 deletions(-) diff --git a/qml/pages/OverviewPage.qml b/qml/pages/OverviewPage.qml index 5a8cf49..0ac6e7b 100644 --- a/qml/pages/OverviewPage.qml +++ b/qml/pages/OverviewPage.qml @@ -63,7 +63,6 @@ Page { repeat: false onTriggered: { overviewPage.chatListCreated = true; - chatListModel.enableDeltaUpdates(); } } diff --git a/src/chatlistmodel.cpp b/src/chatlistmodel.cpp index 22c1d54..f6e5de6 100644 --- a/src/chatlistmodel.cpp +++ b/src/chatlistmodel.cpp @@ -21,22 +21,76 @@ #include #include +#define LOG(x) qDebug() << "[ChatListModel]" << x + +namespace { + const QString ID("id"); + const QString ORDER("order"); + const QString CHAT_ID("chat_id"); + const QString LAST_MESSAGE("last_message"); + const QString UNREAD_COUNT("unread_count"); + const QString NOTIFICATION_SETTINGS("notification_settings"); + const QString LAST_READ_INBOX_MESSAGE_ID("last_read_inbox_message_id"); + const QString LAST_READ_OUTBOX_MESSAGE_ID("last_read_outbox_message_id"); +} + +class ChatListModel::ChatData +{ +public: + ChatData(const QVariantMap &data); + + int compareTo(const ChatData *chat) const; + bool setOrder(const QString &order); + +public: + QVariantMap chatData; + QString chatId; + qlonglong order; +}; + +ChatListModel::ChatData::ChatData(const QVariantMap &data) : + chatData(data), + chatId(data.value(ID).toString()), + order(data.value(ORDER).toLongLong()) +{ +} + +int ChatListModel::ChatData::compareTo(const ChatData *other) const +{ + if (order == other->order) { + return chatId.compare(other->chatId); + } else { + // This puts most recent ones to the top of the list + return (order < other->order) ? 1 : -1; + } +} + +bool ChatListModel::ChatData::setOrder(const QString &newOrder) +{ + if (!newOrder.isEmpty()) { + chatData.insert(ORDER, newOrder); + order = newOrder.toLongLong(); + return true; + } + return false; +} + ChatListModel::ChatListModel(TDLibWrapper *tdLibWrapper) { - this->tdLibWrapper = tdLibWrapper; - this->deltaUpdates = false; - connect(this->tdLibWrapper, SIGNAL(newChatDiscovered(QString, QVariantMap)), this, SLOT(handleChatDiscovered(QString, QVariantMap))); - connect(this->tdLibWrapper, SIGNAL(chatLastMessageUpdated(QString, QString, QVariantMap)), this, SLOT(handleChatLastMessageUpdated(QString, QString, QVariantMap))); - connect(this->tdLibWrapper, SIGNAL(chatOrderUpdated(QString, QString)), this, SLOT(handleChatOrderUpdated(QString, QString))); - connect(this->tdLibWrapper, SIGNAL(chatReadInboxUpdated(QString, QString, int)), this, SLOT(handleChatReadInboxUpdated(QString, QString, int))); - connect(this->tdLibWrapper, SIGNAL(chatReadOutboxUpdated(QString, QString)), this, SLOT(handleChatReadOutboxUpdated(QString, QString))); - connect(this->tdLibWrapper, SIGNAL(messageSendSucceeded(QString, QString, QVariantMap)), this, SLOT(handleMessageSendSucceeded(QString, QString, QVariantMap))); - connect(this->tdLibWrapper, SIGNAL(chatNotificationSettingsUpdated(QString, QVariantMap)), this, SLOT(handleChatNotificationSettingsUpdated(QString, QVariantMap))); + tdLibWrapper = tdLibWrapper; + connect(tdLibWrapper, SIGNAL(newChatDiscovered(QString,QVariantMap)), this, SLOT(handleChatDiscovered(QString,QVariantMap))); + connect(tdLibWrapper, SIGNAL(chatLastMessageUpdated(QString,QString,QVariantMap)), this, SLOT(handleChatLastMessageUpdated(QString,QString,QVariantMap))); + connect(tdLibWrapper, SIGNAL(chatOrderUpdated(QString,QString)), this, SLOT(handleChatOrderUpdated(QString,QString))); + connect(tdLibWrapper, SIGNAL(chatReadInboxUpdated(QString,QString,int)), this, SLOT(handleChatReadInboxUpdated(QString,QString,int))); + connect(tdLibWrapper, SIGNAL(chatReadOutboxUpdated(QString,QString)), this, SLOT(handleChatReadOutboxUpdated(QString,QString))); + connect(tdLibWrapper, SIGNAL(messageSendSucceeded(QString,QString,QVariantMap)), this, SLOT(handleMessageSendSucceeded(QString,QString,QVariantMap))); + connect(tdLibWrapper, SIGNAL(chatNotificationSettingsUpdated(QString,QVariantMap)), this, SLOT(handleChatNotificationSettingsUpdated(QString,QVariantMap))); } ChatListModel::~ChatListModel() { - qDebug() << "[ChatListModel] Destroying myself..."; + LOG("Destroying myself..."); + qDeleteAll(chatList); } int ChatListModel::rowCount(const QModelIndex &) const @@ -46,185 +100,156 @@ int ChatListModel::rowCount(const QModelIndex &) const QVariant ChatListModel::data(const QModelIndex &index, int role) const { - if(index.isValid() && role == Qt::DisplayRole) { - return QVariant(chatList.value(index.row())); + const int row = index.row(); + if (row >= 0 && row < chatList.size() && role == Qt::DisplayRole) { + return chatList.at(row)->chatData; } return QVariant(); } -bool ChatListModel::insertRows(int row, int count, const QModelIndex &parent) -{ - qDebug() << "[ChatListModel] Inserting at " << row << ", row count: " << count; - beginInsertRows(parent, rowCount(QModelIndex()), rowCount(QModelIndex()) + count - 1); - this->chatList.append(this->chatToBeAdded); - this->chatIndexMap.insert(this->chatToBeAdded.value("id").toString(), row); - endInsertRows(); - return true; -} - -void ChatListModel::enableDeltaUpdates() -{ - qDebug() << "[ChatListModel] Enabling delta updates and enforcing UI redraw..."; - layoutChanged(); - this->deltaUpdates = true; -} - void ChatListModel::redrawModel() { - qDebug() << "[ChatListModel] Enforcing UI redraw..."; + LOG("Enforcing UI redraw..."); layoutChanged(); } -bool compareChats(const QVariant &chat1, const QVariant &chat2) +int ChatListModel::updateChatOrder(int chatIndex) { - QVariantMap chatMap1 = chat1.toMap(); - QVariantMap chatMap2 = chat2.toMap(); - // Order comes first... - if (chatMap1.value("order").toLongLong() > chatMap2.value("order").toLongLong()) { - return true; + ChatData* chat = chatList.at(chatIndex); + + const int n = chatList.size(); + int newIndex = chatIndex; + while (newIndex > 0 && chat->compareTo(chatList.at(newIndex-1)) < 0) { + newIndex--; } - // ID comes next... - if (chatMap1.value("order").toLongLong() == chatMap2.value("order").toLongLong() && chatMap1.value("id").toLongLong() > chatMap2.value("id").toLongLong()) { - return true; + if (newIndex == chatIndex) { + while (newIndex < n-1 && chat->compareTo(chatList.at(newIndex+1)) > 0) { + newIndex++; + } + } + if (newIndex != chatIndex) { + LOG("Moving chat" << chat->chatId << "from position" << chatIndex << "to" << newIndex); + beginMoveRows(QModelIndex(), chatIndex, chatIndex, QModelIndex(), (newIndex < chatIndex) ? newIndex : (newIndex+1)); + chatList.move(chatIndex, newIndex); + chatIndexMap.insert(chat->chatId, newIndex); + // Update damaged part of the map + const int last = qMax(chatIndex, newIndex); + if (newIndex < chatIndex) { + // First index is already correct + for (int i = newIndex + 1; i <= last; i++) { + chatIndexMap.insert(chatList.at(i)->chatId, i); + } + } else { + // Last index is already correct + for (int i = chatIndex; i < last; i++) { + chatIndexMap.insert(chatList.at(i)->chatId, i); + } + } + endMoveRows(); } else { - return false; + LOG("Chat" << chat->chatId << "stays at position" << chatIndex); } + + return newIndex; } -void ChatListModel::handleChatDiscovered(const QString &chatId, const QVariantMap &chatInformation) +void ChatListModel::handleChatDiscovered(const QString &chatId, const QVariantMap &chatToBeAdded) { - this->chatListMutex.lock(); - qDebug() << "[ChatListModel] Adding new chat " << chatId; - this->chatToBeAdded = chatInformation; - insertRows(rowCount(QModelIndex()), 1); - this->chatListMutex.unlock(); + ChatData* chat = new ChatData(chatToBeAdded); + const int n = chatList.size(); + int chatIndex; + for (chatIndex = 0; chatIndex < n && chat->compareTo(chatList.at(chatIndex)) >= 0; chatIndex++); + LOG("Adding new chat" << chatId << "at" << chatIndex); + beginInsertRows(QModelIndex(), chatIndex, chatIndex); + chatList.insert(chatIndex, chat); + chatIndexMap.insert(chat->chatId, chatIndex); + // Update damaged part of the map + for (int i = chatIndex + 1; i <= n; i++) { + chatIndexMap.insert(chatList.at(i)->chatId, i); + } + endInsertRows(); } - - void ChatListModel::handleChatLastMessageUpdated(const QString &chatId, const QString &order, const QVariantMap &lastMessage) { - this->chatListMutex.lock(); - int chatIndex = this->chatIndexMap.value(chatId).toInt(); - qDebug() << "[ChatListModel] Updating last message for chat " << chatId << " at index " << chatIndex; - QVariantMap currentChat = this->chatList.value(chatIndex).toMap(); - currentChat.insert("last_message", lastMessage); - currentChat.insert("order", order); - this->chatList.replace(chatIndex, currentChat); - emit dataChanged(this->index(chatIndex), this->index(chatIndex)); - - this->updateChatOrder(chatIndex, currentChat); - emit chatChanged(chatId); - - this->chatListMutex.unlock(); + if (chatIndexMap.contains(chatId)) { + int chatIndex = chatIndexMap.value(chatId); + LOG("Updating last message for chat" << chatId <<" at index" << chatIndex << "new order" << order); + ChatData* chat = chatList.at(chatIndex); + chat->chatData.insert(LAST_MESSAGE, lastMessage); + if (chat->setOrder(order)) { + chatIndex = updateChatOrder(chatIndex); + } + const QModelIndex modelIndex(index(chatIndex)); + emit dataChanged(modelIndex, modelIndex); + emit chatChanged(chatId); + } } void ChatListModel::handleChatOrderUpdated(const QString &chatId, const QString &order) { - this->chatListMutex.lock(); - qDebug() << "[ChatListModel] Updating chat order because of " << chatId << " new order " << order; - int chatIndex = this->chatIndexMap.value(chatId).toInt(); - QVariantMap currentChat = this->chatList.at(chatIndex).toMap(); - currentChat.insert("order", order); - this->chatList.replace(chatIndex, currentChat); - emit dataChanged(this->index(chatIndex), this->index(chatIndex)); - - this->updateChatOrder(chatIndex, currentChat); - emit chatChanged(chatId); - - this->chatListMutex.unlock(); + if (chatIndexMap.contains(chatId)) { + LOG("Updating chat order of" << chatId << "to" << order); + int chatIndex = chatIndexMap.value(chatId); + if (chatList.at(chatIndex)->setOrder(order)) { + chatIndex = updateChatOrder(chatIndex); + } + const QModelIndex modelIndex(index(chatIndex)); + emit dataChanged(modelIndex, modelIndex); + emit chatChanged(chatId); + } } void ChatListModel::handleChatReadInboxUpdated(const QString &chatId, const QString &lastReadInboxMessageId, const int &unreadCount) { - this->chatListMutex.lock(); - qDebug() << "[ChatListModel] Updating chat unread count for " << chatId << " unread messages " << unreadCount << ", last read message ID: " << lastReadInboxMessageId; - int chatIndex = this->chatIndexMap.value(chatId).toInt(); - QVariantMap currentChat = this->chatList.at(chatIndex).toMap(); - currentChat.insert("unread_count", unreadCount); - currentChat.insert("last_read_inbox_message_id", lastReadInboxMessageId); - this->chatList.replace(chatIndex, currentChat); - emit dataChanged(this->index(chatIndex), this->index(chatIndex)); - emit chatChanged(chatId); - this->chatListMutex.unlock(); + if (chatIndexMap.contains(chatId)) { + LOG("Updating chat unread count for" << chatId << "unread messages" << unreadCount << ", last read message ID: " << lastReadInboxMessageId); + const int chatIndex = chatIndexMap.value(chatId); + ChatData* chat = chatList.at(chatIndex); + chat->chatData.insert(UNREAD_COUNT, unreadCount); + chat->chatData.insert(LAST_READ_INBOX_MESSAGE_ID, lastReadInboxMessageId); + const QModelIndex modelIndex(index(chatIndex)); + emit dataChanged(modelIndex, modelIndex); + emit chatChanged(chatId); + } } void ChatListModel::handleChatReadOutboxUpdated(const QString &chatId, const QString &lastReadOutboxMessageId) { - this->chatListMutex.lock(); - qDebug() << "[ChatListModel] Updating last read message for " << chatId << " last ID " << lastReadOutboxMessageId; - int chatIndex = this->chatIndexMap.value(chatId).toInt(); - QVariantMap currentChat = this->chatList.at(chatIndex).toMap(); - currentChat.insert("last_read_outbox_message_id", lastReadOutboxMessageId); - this->chatList.replace(chatIndex, currentChat); - emit dataChanged(this->index(chatIndex), this->index(chatIndex)); - emit chatChanged(chatId); - this->chatListMutex.unlock(); + if (chatIndexMap.contains(chatId)) { + LOG("Updating last read message for" << chatId << "last ID" << lastReadOutboxMessageId); + const int chatIndex = chatIndexMap.value(chatId); + ChatData* chat = chatList.at(chatIndex); + chat->chatData.insert(LAST_READ_OUTBOX_MESSAGE_ID, lastReadOutboxMessageId); + const QModelIndex modelIndex(index(chatIndex)); + emit dataChanged(modelIndex, modelIndex); + emit chatChanged(chatId); + } } void ChatListModel::handleMessageSendSucceeded(const QString &messageId, const QString &oldMessageId, const QVariantMap &message) { - this->chatListMutex.lock(); - QString chatId = message.value("chat_id").toString(); - int chatIndex = this->chatIndexMap.value(chatId).toInt(); - qDebug() << "[ChatListModel] Updating last message for chat " << chatId << " at index " << chatIndex << ", as message was sent, old ID: " << oldMessageId << ", new ID: " << messageId; - QVariantMap currentChat = this->chatList.value(chatIndex).toMap(); - currentChat.insert("last_message", message); - this->chatList.replace(chatIndex, currentChat); - emit dataChanged(this->index(chatIndex), this->index(chatIndex)); - emit chatChanged(chatId); - - this->chatListMutex.unlock(); + const QString chatId(message.value(CHAT_ID).toString()); + if (chatIndexMap.contains(chatId)) { + const int chatIndex = chatIndexMap.value(chatId); + LOG("Updating last message for chat" << chatId << "at index" << chatIndex << ", as message was sent, old ID:" << oldMessageId << ", new ID:" << messageId); + ChatData* chat = chatList.at(chatIndex); + chat->chatData.insert(LAST_MESSAGE, message); + const QModelIndex modelIndex(index(chatIndex)); + emit dataChanged(modelIndex, modelIndex); + emit chatChanged(chatId); + } } void ChatListModel::handleChatNotificationSettingsUpdated(const QString &chatId, const QVariantMap &chatNotificationSettings) { - this->chatListMutex.lock(); - int chatIndex = this->chatIndexMap.value(chatId).toInt(); - qDebug() << "[ChatListModel] Updating notification settings for chat " << chatId << " at index " << chatIndex; - QVariantMap currentChat = this->chatList.value(chatIndex).toMap(); - currentChat.insert("notification_settings", chatNotificationSettings); - this->chatList.replace(chatIndex, currentChat); - emit dataChanged(this->index(chatIndex), this->index(chatIndex)); - emit chatChanged(chatId); - - this->chatListMutex.unlock(); -} - -void ChatListModel::updateChatOrder(const int ¤tChatIndex, const QVariantMap &updatedChat) -{ - // Finding the new position manually as information is needed by beginMoveRows() - // This seems to be the most convenient way of persisting the list position while changing the items - // Other alternative layoutChanged() after sorting resets the index position - there we would need to calculate the new position as well - // If somebody has a better solution - go for it ;) - int newChatIndex = 0; - QVariantMap previousChat; - for (int i = 0; i < this->chatList.length(); i++) { - QVariantMap otherChat = this->chatList.at(i).toMap(); - if (compareChats(updatedChat, otherChat)) { - if (previousChat.value("id") == updatedChat.value("id")) { - newChatIndex = currentChatIndex; - } else { - newChatIndex = i; - } - break; - } - previousChat = otherChat; - } - if (newChatIndex != currentChatIndex) { - // The updated chat now needs to go to the position of the other chat - qDebug() << "[ChatListModel] Chat " << updatedChat.value("id").toString() << " will be moved from position " << currentChatIndex << " to " << newChatIndex; - if (deltaUpdates) { - beginMoveRows(QModelIndex(), currentChatIndex, currentChatIndex, QModelIndex(), (( newChatIndex < currentChatIndex ) ? newChatIndex : ( newChatIndex + 1 ))); - } - std::sort(this->chatList.begin(), this->chatList.end(), compareChats); - this->chatIndexMap.clear(); - for (int i = 0; i < this->chatList.length(); i++) { - this->chatIndexMap.insert(this->chatList.at(i).toMap().value("id").toString(), i); - } - if (deltaUpdates) { - endMoveRows(); - } - + if (chatIndexMap.contains(chatId)) { + const int chatIndex = chatIndexMap.value(chatId); + LOG("Updating notification settings for chat" << chatId << "at index" << chatIndex); + ChatData* chat = chatList.at(chatIndex); + chat->chatData.insert(NOTIFICATION_SETTINGS, chatNotificationSettings); + const QModelIndex modelIndex(index(chatIndex)); + emit dataChanged(modelIndex, modelIndex); + emit chatChanged(chatId); } } diff --git a/src/chatlistmodel.h b/src/chatlistmodel.h index be54536..00f084c 100644 --- a/src/chatlistmodel.h +++ b/src/chatlistmodel.h @@ -34,15 +34,13 @@ public: virtual int rowCount(const QModelIndex&) const override; virtual QVariant data(const QModelIndex &index, int role) const override; - virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; - Q_INVOKABLE void enableDeltaUpdates(); Q_INVOKABLE void redrawModel(); signals: void chatChanged(const QString &chatId); -public slots: +private slots: void handleChatDiscovered(const QString &chatId, const QVariantMap &chatInformation); void handleChatLastMessageUpdated(const QString &chatId, const QString &order, const QVariantMap &lastMessage); void handleChatOrderUpdated(const QString &chatId, const QString &order); @@ -52,15 +50,14 @@ public slots: void handleChatNotificationSettingsUpdated(const QString &chatId, const QVariantMap &chatNotificationSettings); private: + int updateChatOrder(int chatIndex); + +private: + class ChatData; + TDLibWrapper *tdLibWrapper; - QVariantList chatList; - QVariantMap chatToBeAdded; - QVariantMap chatIndexMap; - QMutex chatListMutex; - bool deltaUpdates; - - void updateChatOrder(const int ¤tChatIndex, const QVariantMap &updatedChat); - + QList chatList; + QHash chatIndexMap; }; #endif // CHATLISTMODEL_H