Improve room searching

fixes #257
This commit is contained in:
Konstantinos Sideris 2018-04-27 01:57:46 +03:00
parent 6dfb824d11
commit b72e48cbab
8 changed files with 272 additions and 161 deletions

View file

@ -106,6 +106,14 @@ from_json(const json &j, MemberInfo &info)
info.avatar_url = j.at("avatar_url"); info.avatar_url = j.at("avatar_url");
} }
struct RoomSearchResult
{
std::string room_id;
RoomInfo info;
QImage img;
};
Q_DECLARE_METATYPE(RoomSearchResult)
Q_DECLARE_METATYPE(RoomInfo) Q_DECLARE_METATYPE(RoomInfo)
class Cache : public QObject class Cache : public QObject
@ -185,6 +193,11 @@ public:
UserReceipts readReceipts(const QString &event_id, const QString &room_id); UserReceipts readReceipts(const QString &event_id, const QString &room_id);
QByteArray image(const QString &url) const; QByteArray image(const QString &url) const;
QByteArray image(lmdb::txn &txn, const std::string &url) const;
QByteArray image(const std::string &url) const
{
return image(QString::fromStdString(url));
}
void saveImage(const QString &url, const QByteArray &data); void saveImage(const QString &url, const QByteArray &data);
std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
@ -194,9 +207,11 @@ public:
return getRoomInfo(roomsWithStateUpdates(sync)); return getRoomInfo(roomsWithStateUpdates(sync));
} }
QVector<SearchResult> getAutocompleteMatches(const std::string &room_id, QVector<SearchResult> searchUsers(const std::string &room_id,
const std::string &query, const std::string &query,
std::uint8_t max_items = 5); std::uint8_t max_items = 5);
std::vector<RoomSearchResult> searchRooms(const std::string &query,
std::uint8_t max_items = 5);
private: private:
//! Save an invited room. //! Save an invited room.

View file

@ -22,8 +22,12 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include "Cache.h"
#include "SuggestionsPopup.hpp"
#include "TextField.h" #include "TextField.h"
Q_DECLARE_METATYPE(std::vector<RoomSearchResult>)
class RoomSearchInput : public TextField class RoomSearchInput : public TextField
{ {
Q_OBJECT Q_OBJECT
@ -38,20 +42,20 @@ signals:
protected: protected:
void keyPressEvent(QKeyEvent *event) override; void keyPressEvent(QKeyEvent *event) override;
void hideEvent(QHideEvent *event) override; void hideEvent(QHideEvent *event) override;
bool focusNextPrevChild(bool next) override; bool focusNextPrevChild(bool) override { return false; };
}; };
class QuickSwitcher : public QWidget class QuickSwitcher : public QWidget
{ {
Q_OBJECT Q_OBJECT
public:
explicit QuickSwitcher(QWidget *parent = nullptr);
void setRoomList(const std::map<QString, QString> &rooms); public:
QuickSwitcher(QSharedPointer<Cache> cache, QWidget *parent = nullptr);
signals: signals:
void closing(); void closing();
void roomSelected(const QString &roomid); void roomSelected(const QString &roomid);
void queryResults(const std::vector<RoomSearchResult> &rooms);
protected: protected:
void keyPressEvent(QKeyEvent *event) override; void keyPressEvent(QKeyEvent *event) override;
@ -64,7 +68,9 @@ private:
QVBoxLayout *topLayout_; QVBoxLayout *topLayout_;
RoomSearchInput *roomSearch_; RoomSearchInput *roomSearch_;
QCompleter *completer_;
std::map<QString, QString> rooms_; //! Autocomplete popup box with the room suggestions.
SuggestionsPopup popup_;
//! Cache client for room quering.
QSharedPointer<Cache> cache_;
}; };

View file

@ -5,6 +5,11 @@
#include <QPoint> #include <QPoint>
#include <QWidget> #include <QWidget>
#include "Avatar.h"
#include "AvatarProvider.h"
#include "Cache.h"
#include "ChatPage.h"
class Avatar; class Avatar;
struct SearchResult; struct SearchResult;
@ -16,9 +21,9 @@ class PopupItem : public QWidget
Q_PROPERTY(bool hovering READ hovering WRITE setHovering) Q_PROPERTY(bool hovering READ hovering WRITE setHovering)
public: public:
PopupItem(QWidget *parent, const QString &user_id); PopupItem(QWidget *parent);
QString user() const { return user_id_; } QString selectedText() const { return QString(); }
QColor hoverColor() const { return hoverColor_; } QColor hoverColor() const { return hoverColor_; }
void setHoverColor(QColor &color) { hoverColor_ = color; } void setHoverColor(QColor &color) { hoverColor_ = color; }
@ -30,14 +35,12 @@ protected:
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
signals: signals:
void clicked(const QString &display_name); void clicked(const QString &text);
private: protected:
QHBoxLayout *topLayout_; QHBoxLayout *topLayout_;
Avatar *avatar_; Avatar *avatar_;
QLabel *userName_;
QString user_id_;
QColor hoverColor_; QColor hoverColor_;
@ -45,6 +48,33 @@ private:
bool hovering_; bool hovering_;
}; };
class UserItem : public PopupItem
{
Q_OBJECT
public:
UserItem(QWidget *parent, const QString &user_id);
QString selectedText() const { return userId_; }
private:
QLabel *userName_;
QString userId_;
};
class RoomItem : public PopupItem
{
Q_OBJECT
public:
RoomItem(QWidget *parent, const RoomSearchResult &res);
QString selectedText() const { return roomId_; }
private:
QLabel *roomName_;
QString roomId_;
RoomSearchResult info_;
};
class SuggestionsPopup : public QWidget class SuggestionsPopup : public QWidget
{ {
Q_OBJECT Q_OBJECT
@ -52,9 +82,24 @@ class SuggestionsPopup : public QWidget
public: public:
explicit SuggestionsPopup(QWidget *parent = nullptr); explicit SuggestionsPopup(QWidget *parent = nullptr);
template<class Item>
void selectHoveredSuggestion()
{
const auto item = layout_->itemAt(selectedItem_);
if (!item)
return;
const auto &widget = qobject_cast<Item *>(item->widget());
emit itemSelected(
Cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText()));
resetSelection();
}
public slots: public slots:
void addUsers(const QVector<SearchResult> &users); void addUsers(const QVector<SearchResult> &users);
void selectHoveredSuggestion(); void addRooms(const std::vector<RoomSearchResult> &rooms);
//! Move to the next available suggestion item. //! Move to the next available suggestion item.
void selectNextSuggestion(); void selectNextSuggestion();
//! Move to the previous available suggestion item. //! Move to the previous available suggestion item.
@ -75,6 +120,14 @@ private:
void resetSelection() { selectedItem_ = -1; } void resetSelection() { selectedItem_ = -1; }
void selectFirstItem() { selectedItem_ = 0; } void selectFirstItem() { selectedItem_ = 0; }
void selectLastItem() { selectedItem_ = layout_->count() - 1; } void selectLastItem() { selectedItem_ = layout_->count() - 1; }
void removeItems()
{
QLayoutItem *item;
while ((item = layout_->takeAt(0)) != 0) {
delete item->widget();
delete item;
}
}
QVBoxLayout *layout_; QVBoxLayout *layout_;

View file

@ -141,6 +141,27 @@ Cache::saveImage(const QString &url, const QByteArray &image)
} }
} }
QByteArray
Cache::image(lmdb::txn &txn, const std::string &url) const
{
if (url.empty())
return QByteArray();
try {
lmdb::val image;
bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(url), image);
if (!res)
return QByteArray();
return QByteArray(image.data(), image.size());
} catch (const lmdb::error &e) {
qCritical() << "image:" << e.what() << QString::fromStdString(url);
}
return QByteArray();
}
QByteArray QByteArray
Cache::image(const QString &url) const Cache::image(const QString &url) const
{ {
@ -945,10 +966,47 @@ Cache::populateMembers()
txn.commit(); txn.commit();
} }
std::vector<RoomSearchResult>
Cache::searchRooms(const std::string &query, std::uint8_t max_items)
{
std::multimap<int, std::pair<std::string, RoomInfo>> items;
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
auto cursor = lmdb::cursor::open(txn, roomsDb_);
std::string room_id, room_data;
while (cursor.get(room_id, room_data, MDB_NEXT)) {
RoomInfo tmp = json::parse(std::move(room_data));
const int score = utils::levenshtein_distance(
query, QString::fromStdString(tmp.name).toLower().toStdString());
items.emplace(score, std::make_pair(room_id, tmp));
}
cursor.close();
auto end = items.begin();
if (items.size() >= max_items)
std::advance(end, max_items);
else if (items.size() > 0)
std::advance(end, items.size());
std::vector<RoomSearchResult> results;
for (auto it = items.begin(); it != end; it++) {
results.push_back(
RoomSearchResult{it->second.first,
it->second.second,
QImage::fromData(image(txn, it->second.second.avatar_url))});
}
txn.commit();
return results;
}
QVector<SearchResult> QVector<SearchResult>
Cache::getAutocompleteMatches(const std::string &room_id, Cache::searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items)
const std::string &query,
std::uint8_t max_items)
{ {
std::multimap<int, std::pair<std::string, std::string>> items; std::multimap<int, std::pair<std::string, std::string>> items;

View file

@ -699,7 +699,7 @@ ChatPage::showQuickSwitcher()
{ {
if (quickSwitcher_.isNull()) { if (quickSwitcher_.isNull()) {
quickSwitcher_ = QSharedPointer<QuickSwitcher>( quickSwitcher_ = QSharedPointer<QuickSwitcher>(
new QuickSwitcher(this), new QuickSwitcher(cache_, this),
[](QuickSwitcher *switcher) { switcher->deleteLater(); }); [](QuickSwitcher *switcher) { switcher->deleteLater(); });
connect(quickSwitcher_.data(), connect(quickSwitcher_.data(),
@ -721,17 +721,7 @@ ChatPage::showQuickSwitcher()
quickSwitcherModal_->setColor(QColor(30, 30, 30, 170)); quickSwitcherModal_->setColor(QColor(30, 30, 30, 170));
} }
try { quickSwitcherModal_->show();
std::map<QString, QString> rooms;
auto info = cache_->roomInfo(false);
for (auto it = info.begin(); it != info.end(); ++it)
rooms.emplace(QString::fromStdString(it.value().name).trimmed(), it.key());
quickSwitcher_->setRoomList(rooms);
quickSwitcherModal_->show();
} catch (const lmdb::error &e) {
const auto err = QString::fromStdString(e.what());
emit showNotification(QString("Failed to load room list: %1").arg(err));
}
} }
void void

View file

@ -20,6 +20,7 @@
#include <QStringListModel> #include <QStringListModel>
#include <QStyleOption> #include <QStyleOption>
#include <QTimer> #include <QTimer>
#include <QtConcurrent>
#include "QuickSwitcher.h" #include "QuickSwitcher.h"
@ -27,14 +28,6 @@ RoomSearchInput::RoomSearchInput(QWidget *parent)
: TextField(parent) : TextField(parent)
{} {}
bool
RoomSearchInput::focusNextPrevChild(bool next)
{
Q_UNUSED(next);
return false;
}
void void
RoomSearchInput::keyPressEvent(QKeyEvent *event) RoomSearchInput::keyPressEvent(QKeyEvent *event)
{ {
@ -58,9 +51,11 @@ RoomSearchInput::hideEvent(QHideEvent *event)
TextField::hideEvent(event); TextField::hideEvent(event);
} }
QuickSwitcher::QuickSwitcher(QWidget *parent) QuickSwitcher::QuickSwitcher(QSharedPointer<Cache> cache, QWidget *parent)
: QWidget(parent) : QWidget(parent)
, cache_{cache}
{ {
qRegisterMetaType<std::vector<RoomSearchResult>>();
setMaximumWidth(450); setMaximumWidth(450);
QFont font; QFont font;
@ -68,88 +63,55 @@ QuickSwitcher::QuickSwitcher(QWidget *parent)
roomSearch_ = new RoomSearchInput(this); roomSearch_ = new RoomSearchInput(this);
roomSearch_->setFont(font); roomSearch_->setFont(font);
roomSearch_->setPlaceholderText(tr("Find a room...")); roomSearch_->setPlaceholderText(tr("Search for a room..."));
completer_ = new QCompleter();
completer_->setCaseSensitivity(Qt::CaseInsensitive);
completer_->setCompletionMode(QCompleter::PopupCompletion);
completer_->setWidget(this);
topLayout_ = new QVBoxLayout(this); topLayout_ = new QVBoxLayout(this);
topLayout_->addWidget(roomSearch_); topLayout_->addWidget(roomSearch_);
connect(completer_, SIGNAL(highlighted(QString)), roomSearch_, SLOT(setText(QString))); connect(this,
connect(roomSearch_, &QLineEdit::textEdited, this, [this](const QString &prefix) { &QuickSwitcher::queryResults,
if (prefix.isEmpty()) { this,
completer_->popup()->hide(); [this](const std::vector<RoomSearchResult> &rooms) {
selection_ = -1; auto pos = mapToGlobal(roomSearch_->geometry().bottomLeft());
popup_.setFixedWidth(width());
popup_.addRooms(rooms);
popup_.move(pos.x() - topLayout_->margin(), pos.y() + topLayout_->margin());
popup_.show();
});
connect(roomSearch_, &QLineEdit::textEdited, this, [this](const QString &query) {
if (query.isEmpty()) {
popup_.hide();
return; return;
} }
if (prefix != completer_->completionPrefix()) { QtConcurrent::run([this, query = query.toLower()]() {
completer_->setCompletionPrefix(prefix); try {
selection_ = -1; emit queryResults(cache_->searchRooms(query.toStdString()));
} } catch (const lmdb::error &e) {
qWarning() << "room search failed:" << e.what();
completer_->popup()->setWindowFlags(completer_->popup()->windowFlags() | }
Qt::ToolTip | Qt::NoDropShadowWindowHint); });
completer_->popup()->setAttribute(Qt::WA_ShowWithoutActivating);
completer_->complete();
}); });
connect(roomSearch_, &RoomSearchInput::selectNextCompletion, this, [this]() { connect(roomSearch_,
selection_ += 1; &RoomSearchInput::selectNextCompletion,
&popup_,
if (!completer_->setCurrentRow(selection_)) { &SuggestionsPopup::selectNextSuggestion);
selection_ = 0; connect(roomSearch_,
completer_->setCurrentRow(selection_); &RoomSearchInput::selectPreviousCompletion,
} &popup_,
&SuggestionsPopup::selectPreviousSuggestion);
completer_->popup()->setCurrentIndex(completer_->currentIndex()); connect(&popup_, &SuggestionsPopup::itemSelected, this, &QuickSwitcher::roomSelected);
}); connect(roomSearch_, &RoomSearchInput::hiding, this, [this]() { popup_.hide(); });
connect(roomSearch_, &RoomSearchInput::selectPreviousCompletion, this, [this]() {
selection_ -= 1;
if (!completer_->setCurrentRow(selection_)) {
selection_ = completer_->completionCount() - 1;
completer_->setCurrentRow(selection_);
}
completer_->popup()->setCurrentIndex(completer_->currentIndex());
});
connect(
roomSearch_, &RoomSearchInput::hiding, this, [this]() { completer_->popup()->hide(); });
connect(roomSearch_, &QLineEdit::returnPressed, this, [this]() { connect(roomSearch_, &QLineEdit::returnPressed, this, [this]() {
emit closing(); emit closing();
QString text("");
if (selection_ == -1) {
completer_->setCurrentRow(0);
text = completer_->currentCompletion();
} else {
text = this->roomSearch_->text().trimmed();
}
emit roomSelected(rooms_[text]);
roomSearch_->clear(); roomSearch_->clear();
popup_.selectHoveredSuggestion<RoomItem>();
}); });
} }
void
QuickSwitcher::setRoomList(const std::map<QString, QString> &rooms)
{
rooms_ = rooms;
QStringList items;
for (const auto &room : rooms)
items << room.first;
completer_->setModel(new QStringListModel(items));
}
void void
QuickSwitcher::paintEvent(QPaintEvent *) QuickSwitcher::paintEvent(QPaintEvent *)
{ {

View file

@ -1,7 +1,5 @@
#include "Avatar.h" #include "Avatar.h"
#include "AvatarProvider.h" #include "AvatarProvider.h"
#include "Cache.h"
#include "ChatPage.h"
#include "Config.h" #include "Config.h"
#include "DropShadow.h" #include "DropShadow.h"
#include "SuggestionsPopup.hpp" #include "SuggestionsPopup.hpp"
@ -15,10 +13,9 @@
constexpr int PopupHMargin = 5; constexpr int PopupHMargin = 5;
constexpr int PopupItemMargin = 4; constexpr int PopupItemMargin = 4;
PopupItem::PopupItem(QWidget *parent, const QString &user_id) PopupItem::PopupItem(QWidget *parent)
: QWidget(parent) : QWidget(parent)
, avatar_{new Avatar(this)} , avatar_{new Avatar(this)}
, user_id_{user_id}
, hovering_{false} , hovering_{false}
{ {
setMouseTracking(true); setMouseTracking(true);
@ -27,29 +24,6 @@ PopupItem::PopupItem(QWidget *parent, const QString &user_id)
topLayout_ = new QHBoxLayout(this); topLayout_ = new QHBoxLayout(this);
topLayout_->setContentsMargins( topLayout_->setContentsMargins(
PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin); PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin);
QFont font;
font.setPixelSize(conf::popup::font);
auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), user_id);
avatar_->setSize(conf::popup::avatar);
avatar_->setLetter(utils::firstChar(displayName));
// If it's a matrix id we use the second letter.
if (displayName.size() > 1 && displayName.at(0) == '@')
avatar_->setLetter(QChar(displayName.at(1)));
userName_ = new QLabel(displayName, this);
userName_->setFont(font);
topLayout_->addWidget(avatar_);
topLayout_->addWidget(userName_, 1);
AvatarProvider::resolve(ChatPage::instance()->currentRoom(),
user_id,
this,
[this](const QImage &img) { avatar_->setImage(img); });
} }
void void
@ -68,11 +42,61 @@ void
PopupItem::mousePressEvent(QMouseEvent *event) PopupItem::mousePressEvent(QMouseEvent *event)
{ {
if (event->buttons() != Qt::RightButton) if (event->buttons() != Qt::RightButton)
emit clicked(Cache::displayName(ChatPage::instance()->currentRoom(), user_id_)); // TODO: should be abstracted.
emit clicked(
Cache::displayName(ChatPage::instance()->currentRoom(), selectedText()));
QWidget::mousePressEvent(event); QWidget::mousePressEvent(event);
} }
UserItem::UserItem(QWidget *parent, const QString &user_id)
: PopupItem(parent)
, userId_{user_id}
{
QFont font;
font.setPixelSize(conf::popup::font);
auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_);
avatar_->setSize(conf::popup::avatar);
avatar_->setLetter(utils::firstChar(displayName));
// If it's a matrix id we use the second letter.
if (displayName.size() > 1 && displayName.at(0) == '@')
avatar_->setLetter(QChar(displayName.at(1)));
userName_ = new QLabel(displayName, this);
userName_->setFont(font);
topLayout_->addWidget(avatar_);
topLayout_->addWidget(userName_, 1);
AvatarProvider::resolve(ChatPage::instance()->currentRoom(),
userId_,
this,
[this](const QImage &img) { avatar_->setImage(img); });
}
RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res)
: PopupItem(parent)
, roomId_{QString::fromStdString(res.room_id)}
{
auto name = QFontMetrics(QFont()).elidedText(
QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10);
avatar_->setSize(conf::popup::avatar + 6);
avatar_->setLetter(utils::firstChar(name));
roomName_ = new QLabel(name, this);
roomName_->setMargin(0);
topLayout_->addWidget(avatar_);
topLayout_->addWidget(roomName_, 1);
if (!res.img.isNull())
avatar_->setImage(res.img);
}
SuggestionsPopup::SuggestionsPopup(QWidget *parent) SuggestionsPopup::SuggestionsPopup(QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
@ -84,15 +108,32 @@ SuggestionsPopup::SuggestionsPopup(QWidget *parent)
layout_->setSpacing(0); layout_->setSpacing(0);
} }
void
SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms)
{
removeItems();
if (rooms.empty()) {
hide();
return;
}
for (const auto &r : rooms) {
auto room = new RoomItem(this, r);
layout_->addWidget(room);
connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected);
}
resetSelection();
adjustSize();
resize(geometry().width(), 40 * rooms.size());
}
void void
SuggestionsPopup::addUsers(const QVector<SearchResult> &users) SuggestionsPopup::addUsers(const QVector<SearchResult> &users)
{ {
// Remove all items from the layout. removeItems();
QLayoutItem *item;
while ((item = layout_->takeAt(0)) != 0) {
delete item->widget();
delete item;
}
if (users.isEmpty()) { if (users.isEmpty()) {
hide(); hide();
@ -100,9 +141,9 @@ SuggestionsPopup::addUsers(const QVector<SearchResult> &users)
} }
for (const auto &u : users) { for (const auto &u : users) {
auto user = new PopupItem(this, u.user_id); auto user = new UserItem(this, u.user_id);
layout_->addWidget(user); layout_->addWidget(user);
connect(user, &PopupItem::clicked, this, &SuggestionsPopup::itemSelected); connect(user, &UserItem::clicked, this, &SuggestionsPopup::itemSelected);
} }
resetSelection(); resetSelection();
@ -160,19 +201,6 @@ SuggestionsPopup::setHovering(int pos)
widget->setHovering(true); widget->setHovering(true);
} }
void
SuggestionsPopup::selectHoveredSuggestion()
{
const auto item = layout_->itemAt(selectedItem_);
if (!item)
return;
const auto &widget = qobject_cast<PopupItem *>(item->widget());
emit itemSelected(Cache::displayName(ChatPage::instance()->currentRoom(), widget->user()));
resetSelection();
}
void void
SuggestionsPopup::paintEvent(QPaintEvent *) SuggestionsPopup::paintEvent(QPaintEvent *)
{ {

View file

@ -95,10 +95,9 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
&FilteredTextEdit::selectPreviousSuggestion, &FilteredTextEdit::selectPreviousSuggestion,
&popup_, &popup_,
&SuggestionsPopup::selectPreviousSuggestion); &SuggestionsPopup::selectPreviousSuggestion);
connect(this, connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() {
&FilteredTextEdit::selectHoveredSuggestion, popup_.selectHoveredSuggestion<UserItem>();
&popup_, });
&SuggestionsPopup::selectHoveredSuggestion);
previewDialog_.hide(); previewDialog_.hide();
} }
@ -459,7 +458,7 @@ TextInputWidget::TextInputWidget(QWidget *parent)
QtConcurrent::run([this, q = q.toLower().toStdString()]() { QtConcurrent::run([this, q = q.toLower().toStdString()]() {
try { try {
emit input_->resultsRetrieved(cache_->getAutocompleteMatches( emit input_->resultsRetrieved(cache_->searchUsers(
ChatPage::instance()->currentRoom().toStdString(), q)); ChatPage::instance()->currentRoom().toStdString(), q));
} catch (const lmdb::error &e) { } catch (const lmdb::error &e) {
std::cout << e.what() << '\n'; std::cout << e.what() << '\n';