Improve chat search performance

Simple text search performs significantly better that regular
expressions.

Disconnecting search filter model from the source when search is off
should be good for performance because the filter model won't have to
unnecessarily react to the source model changes.
This commit is contained in:
Slava Monich 2021-12-09 01:46:04 +02:00
parent 5309dda94b
commit 3c20eb7ca8
7 changed files with 178 additions and 28 deletions

View file

@ -38,6 +38,7 @@ SOURCES += src/harbour-fernschreiber.cpp \
src/tdlibfile.cpp \
src/tdlibreceiver.cpp \
src/tdlibwrapper.cpp \
src/textfiltermodel.cpp \
src/tgsplugin.cpp
DISTFILES += qml/harbour-fernschreiber.qml \
@ -228,6 +229,7 @@ HEADERS += \
src/tdlibreceiver.h \
src/tdlibsecrets.h \
src/tdlibwrapper.h \
src/textfiltermodel.h \
src/tgsplugin.h
# https://github.com/Samsung/rlottie.git

View file

@ -103,19 +103,11 @@ Page {
}
}
Timer {
id: searchChatTimer
interval: 300
running: false
repeat: false
onTriggered: {
if (chatSearchField.text === "") {
chatListView.model = chatListModel;
} else {
chatListView.model = chatListProxyModel;
}
chatListProxyModel.setFilterWildcard("*" + chatSearchField.text + "*");
}
TextFilterModel {
id: chatListProxyModel
sourceModel: (chatSearchField.opacity > 0) ? chatListModel : null
filterRoleName: "filter"
filterText: chatSearchField.text
}
function openMessage(chatId, messageId) {
@ -355,10 +347,6 @@ Page {
placeholderText: qsTr("Filter your chats...")
canHide: text === ""
onTextChanged: {
searchChatTimer.restart();
}
onHideClicked: {
resetFocus();
}
@ -380,7 +368,7 @@ Page {
clip: true
opacity: (overviewPage.chatListCreated && !overviewPage.logoutLoading) ? 1 : 0
Behavior on opacity { FadeAnimation {} }
model: chatListModel
model: chatListProxyModel.sourceModel ? chatListProxyModel : chatListModel
delegate: ChatListViewItem {
ownUserId: overviewPage.ownUserId
isVerified: is_verified
@ -394,7 +382,7 @@ Page {
ViewPlaceholder {
enabled: chatListView.count === 0
text: chatSearchField.text === "" ? qsTr("You don't have any chats yet.") : qsTr("No matching chats found.")
text: chatListModel.count === 0 ? qsTr("You don't have any chats yet.") : qsTr("No matching chats found.")
hintText: qsTr("You can search public chats or create a new chat via the pull-down menu.")
}

View file

@ -320,6 +320,7 @@ QVector<int> ChatListModel::ChatData::updateLastMessage(const QVariantMap &messa
changedRoles.append(RoleLastMessageDate);
}
if (prevSenderMessageText != senderMessageText()) {
changedRoles.append(RoleFilter);
changedRoles.append(RoleLastMessageText);
}
if (prevSenderMessageStatus != senderMessageStatus()) {
@ -388,6 +389,9 @@ ChatListModel::ChatListModel(TDLibWrapper *tdLibWrapper, AppSettings *appSetting
relativeTimeRefreshTimer->setSingleShot(false);
relativeTimeRefreshTimer->setInterval(30000);
connect(relativeTimeRefreshTimer, SIGNAL(timeout()), SLOT(handleRelativeTimeRefreshTimer()));
connect(this, SIGNAL(rowsInserted(QModelIndex,int,int)), SIGNAL(countChanged()));
connect(this, SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(countChanged()));
connect(this, SIGNAL(modelReset()), SIGNAL(countChanged()));
}
ChatListModel::~ChatListModel()
@ -457,7 +461,7 @@ QVariant ChatListModel::data(const QModelIndex &index, int role) const
case ChatListModel::RoleIsChannel: return data->isChannel();
case ChatListModel::RoleIsMarkedAsUnread: return data->isMarkedAsUnread();
case ChatListModel::RoleIsPinned: return data->isPinned();
case ChatListModel::RoleFilter: return QString(data->title() + " " + data->senderMessageText()).trimmed();
case ChatListModel::RoleFilter: return data->title() + " " + data->senderMessageText();
case ChatListModel::RoleDraftMessageText: return data->draftMessageText();
case ChatListModel::RoleDraftMessageDate: return data->draftMessageDate();
}
@ -891,6 +895,7 @@ void ChatListModel::handleChatTitleUpdated(const QString &chatId, const QString
chat->chatData.insert(TITLE, title);
QVector<int> changedRoles;
changedRoles.append(ChatListModel::RoleTitle);
changedRoles.append(ChatListModel::RoleFilter);
const QModelIndex modelIndex(index(chatIndex));
emit dataChanged(modelIndex, modelIndex, changedRoles);
} else {

View file

@ -28,6 +28,7 @@ class ChatListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool showAllChats READ showAllChats WRITE setShowAllChats NOTIFY showAllChatsChanged)
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
public:
@ -57,9 +58,9 @@ public:
ChatListModel(TDLibWrapper *tdLibWrapper, AppSettings *appSettings);
~ChatListModel() override;
virtual QHash<int,QByteArray> roleNames() const override;
virtual int rowCount(const QModelIndex&) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
QHash<int,QByteArray> roleNames() const Q_DECL_OVERRIDE;
int rowCount(const QModelIndex &index = QModelIndex()) const Q_DECL_OVERRIDE;
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
Q_INVOKABLE void redrawModel();
Q_INVOKABLE QVariantMap get(int row);
@ -90,6 +91,7 @@ private slots:
void handleRelativeTimeRefreshTimer();
signals:
void countChanged();
void showAllChatsChanged();
void chatChanged(const qlonglong &changedChatId);
void chatJoined(const qlonglong &chatId, const QString &chatTitle);

View file

@ -42,6 +42,7 @@
#include "dbusadaptor.h"
#include "processlauncher.h"
#include "stickermanager.h"
#include "textfiltermodel.h"
#include "tgsplugin.h"
#include "fernschreiberutils.h"
#include "knownusersmodel.h"
@ -69,6 +70,7 @@ int main(int argc, char *argv[])
const char *uri = "WerkWolf.Fernschreiber";
qmlRegisterType<TDLibFile>(uri, 1, 0, "TDLibFile");
qmlRegisterType<NamedAction>(uri, 1, 0, "NamedAction");
qmlRegisterType<TextFilterModel>(uri, 1, 0, "TextFilterModel");
qmlRegisterSingletonType<DebugLogJS>(uri, 1, 0, "DebugLog", DebugLogJS::createSingleton);
AppSettings *appSettings = new AppSettings(view.data());
@ -89,11 +91,6 @@ int main(int argc, char *argv[])
ChatListModel chatListModel(tdLibWrapper, appSettings);
context->setContextProperty("chatListModel", &chatListModel);
QSortFilterProxyModel chatListProxyModel(view.data());
chatListProxyModel.setSourceModel(&chatListModel);
chatListProxyModel.setFilterRole(ChatListModel::RoleFilter);
chatListProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
context->setContextProperty("chatListProxyModel", &chatListProxyModel);
ChatModel chatModel(tdLibWrapper);
context->setContextProperty("chatModel", &chatModel);

98
src/textfiltermodel.cpp Normal file
View file

@ -0,0 +1,98 @@
/*
This file is part of Fernschreiber.
Fernschreiber 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.
Fernschreiber 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 Fernschreiber. If not, see <http://www.gnu.org/licenses/>.
*/
#include "textfiltermodel.h"
#define DEBUG_MODULE TextFilterModel
#include "debuglog.h"
TextFilterModel::TextFilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
setDynamicSortFilter(true);
setFilterCaseSensitivity(Qt::CaseInsensitive);
setFilterFixedString(QString());
}
void TextFilterModel::setSource(QObject *model)
{
setSourceModel(qobject_cast<QAbstractItemModel*>(model));
}
void TextFilterModel::setSourceModel(QAbstractItemModel *model)
{
if (sourceModel() != model) {
LOG(model);
QSortFilterProxyModel::setSourceModel(model);
updateFilterRole();
emit sourceChanged();
}
}
QString TextFilterModel::getFilterRoleName() const
{
return filterRoleName;
}
void TextFilterModel::setFilterRoleName(QString role)
{
if (filterRoleName != role) {
filterRoleName = role;
LOG(role);
updateFilterRole();
emit filterRoleNameChanged();
}
}
QString TextFilterModel::getFilterText() const
{
return filterText;
}
void TextFilterModel::setFilterText(QString text)
{
if (filterText.compare(text, Qt::CaseInsensitive) != 0) {
filterText = text;
LOG(text);
setFilterFixedString(text);
emit filterTextChanged();
}
}
int TextFilterModel::findRole(QAbstractItemModel *model, QString role)
{
if (model && !role.isEmpty()) {
const QByteArray roleName(role.toUtf8());
const QHash<int,QByteArray> roleMap(model->roleNames());
const QList<int> roles(roleMap.keys());
const int n = roles.count();
for (int i = 0; i < n; i++) {
const QByteArray name(roleMap.value(roles.at(i)));
if (name == roleName) {
LOG(role << roles.at(i));
return roles.at(i);
}
}
LOG("Unknown role" << role);
}
return -1;
}
void TextFilterModel::updateFilterRole()
{
const int role = findRole(sourceModel(), filterRoleName);
setFilterRole((role >= 0) ? role : Qt::DisplayRole);
}

58
src/textfiltermodel.h Normal file
View file

@ -0,0 +1,58 @@
/*
This file is part of Fernschreiber.
Fernschreiber 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.
Fernschreiber 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 Fernschreiber. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TEXTFILTERMODEL_H
#define TEXTFILTERMODEL_H
#include <QSortFilterProxyModel>
class TextFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(QString filterRoleName READ getFilterRoleName WRITE setFilterRoleName NOTIFY filterRoleNameChanged)
Q_PROPERTY(QString filterText READ getFilterText WRITE setFilterText NOTIFY filterTextChanged)
Q_PROPERTY(QObject* sourceModel READ sourceModel WRITE setSource NOTIFY sourceChanged)
public:
TextFilterModel(QObject *parent = Q_NULLPTR);
void setSource(QObject* model);
void setSourceModel(QAbstractItemModel *model) Q_DECL_OVERRIDE;
QString getFilterRoleName() const;
void setFilterRoleName(QString role);
QString getFilterText() const;
void setFilterText(QString text);
signals:
void sourceChanged();
void filterRoleNameChanged();
void filterTextChanged();
private slots:
void updateFilterRole();
private:
static int findRole(QAbstractItemModel *model, QString role);
private:
QString filterRoleName;
QString filterText;
};
#endif // TEXTFILTERMODEL_H