diff --git a/include/Cache.h b/include/Cache.h index acad45e2..a3e2c6c1 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -106,6 +106,14 @@ from_json(const json &j, MemberInfo &info) 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) class Cache : public QObject @@ -185,6 +193,11 @@ public: UserReceipts readReceipts(const QString &event_id, const QString &room_id); 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); std::vector roomsWithStateUpdates(const mtx::responses::Sync &res); @@ -194,9 +207,11 @@ public: return getRoomInfo(roomsWithStateUpdates(sync)); } - QVector getAutocompleteMatches(const std::string &room_id, - const std::string &query, - std::uint8_t max_items = 5); + QVector searchUsers(const std::string &room_id, + const std::string &query, + std::uint8_t max_items = 5); + std::vector searchRooms(const std::string &query, + std::uint8_t max_items = 5); private: //! Save an invited room. diff --git a/include/QuickSwitcher.h b/include/QuickSwitcher.h index 01c5e6da..ce0ed00b 100644 --- a/include/QuickSwitcher.h +++ b/include/QuickSwitcher.h @@ -22,8 +22,12 @@ #include #include +#include "Cache.h" +#include "SuggestionsPopup.hpp" #include "TextField.h" +Q_DECLARE_METATYPE(std::vector) + class RoomSearchInput : public TextField { Q_OBJECT @@ -38,20 +42,20 @@ signals: protected: void keyPressEvent(QKeyEvent *event) override; void hideEvent(QHideEvent *event) override; - bool focusNextPrevChild(bool next) override; + bool focusNextPrevChild(bool) override { return false; }; }; class QuickSwitcher : public QWidget { Q_OBJECT -public: - explicit QuickSwitcher(QWidget *parent = nullptr); - void setRoomList(const std::map &rooms); +public: + QuickSwitcher(QSharedPointer cache, QWidget *parent = nullptr); signals: void closing(); void roomSelected(const QString &roomid); + void queryResults(const std::vector &rooms); protected: void keyPressEvent(QKeyEvent *event) override; @@ -64,7 +68,9 @@ private: QVBoxLayout *topLayout_; RoomSearchInput *roomSearch_; - QCompleter *completer_; - std::map rooms_; + //! Autocomplete popup box with the room suggestions. + SuggestionsPopup popup_; + //! Cache client for room quering. + QSharedPointer cache_; }; diff --git a/include/SuggestionsPopup.hpp b/include/SuggestionsPopup.hpp index 32964cb3..e949ce7c 100644 --- a/include/SuggestionsPopup.hpp +++ b/include/SuggestionsPopup.hpp @@ -5,6 +5,11 @@ #include #include +#include "Avatar.h" +#include "AvatarProvider.h" +#include "Cache.h" +#include "ChatPage.h" + class Avatar; struct SearchResult; @@ -16,9 +21,9 @@ class PopupItem : public QWidget Q_PROPERTY(bool hovering READ hovering WRITE setHovering) 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_; } void setHoverColor(QColor &color) { hoverColor_ = color; } @@ -30,14 +35,12 @@ protected: void mousePressEvent(QMouseEvent *event) override; signals: - void clicked(const QString &display_name); + void clicked(const QString &text); -private: +protected: QHBoxLayout *topLayout_; Avatar *avatar_; - QLabel *userName_; - QString user_id_; QColor hoverColor_; @@ -45,6 +48,33 @@ private: 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 { Q_OBJECT @@ -52,9 +82,24 @@ class SuggestionsPopup : public QWidget public: explicit SuggestionsPopup(QWidget *parent = nullptr); + template + void selectHoveredSuggestion() + { + const auto item = layout_->itemAt(selectedItem_); + if (!item) + return; + + const auto &widget = qobject_cast(item->widget()); + emit itemSelected( + Cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); + + resetSelection(); + } + public slots: void addUsers(const QVector &users); - void selectHoveredSuggestion(); + void addRooms(const std::vector &rooms); + //! Move to the next available suggestion item. void selectNextSuggestion(); //! Move to the previous available suggestion item. @@ -75,6 +120,14 @@ private: void resetSelection() { selectedItem_ = -1; } void selectFirstItem() { selectedItem_ = 0; } void selectLastItem() { selectedItem_ = layout_->count() - 1; } + void removeItems() + { + QLayoutItem *item; + while ((item = layout_->takeAt(0)) != 0) { + delete item->widget(); + delete item; + } + } QVBoxLayout *layout_; diff --git a/src/Cache.cc b/src/Cache.cc index ea31e749..416d95a6 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -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 Cache::image(const QString &url) const { @@ -945,10 +966,47 @@ Cache::populateMembers() txn.commit(); } +std::vector +Cache::searchRooms(const std::string &query, std::uint8_t max_items) +{ + std::multimap> 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 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 -Cache::getAutocompleteMatches(const std::string &room_id, - const std::string &query, - std::uint8_t max_items) +Cache::searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items) { std::multimap> items; diff --git a/src/ChatPage.cc b/src/ChatPage.cc index f64f332d..8981cb98 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -699,7 +699,7 @@ ChatPage::showQuickSwitcher() { if (quickSwitcher_.isNull()) { quickSwitcher_ = QSharedPointer( - new QuickSwitcher(this), + new QuickSwitcher(cache_, this), [](QuickSwitcher *switcher) { switcher->deleteLater(); }); connect(quickSwitcher_.data(), @@ -721,17 +721,7 @@ ChatPage::showQuickSwitcher() quickSwitcherModal_->setColor(QColor(30, 30, 30, 170)); } - try { - std::map 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)); - } + quickSwitcherModal_->show(); } void diff --git a/src/QuickSwitcher.cc b/src/QuickSwitcher.cc index e8ef1cd9..d406a6de 100644 --- a/src/QuickSwitcher.cc +++ b/src/QuickSwitcher.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include "QuickSwitcher.h" @@ -27,14 +28,6 @@ RoomSearchInput::RoomSearchInput(QWidget *parent) : TextField(parent) {} -bool -RoomSearchInput::focusNextPrevChild(bool next) -{ - Q_UNUSED(next); - - return false; -} - void RoomSearchInput::keyPressEvent(QKeyEvent *event) { @@ -58,9 +51,11 @@ RoomSearchInput::hideEvent(QHideEvent *event) TextField::hideEvent(event); } -QuickSwitcher::QuickSwitcher(QWidget *parent) +QuickSwitcher::QuickSwitcher(QSharedPointer cache, QWidget *parent) : QWidget(parent) + , cache_{cache} { + qRegisterMetaType>(); setMaximumWidth(450); QFont font; @@ -68,88 +63,55 @@ QuickSwitcher::QuickSwitcher(QWidget *parent) roomSearch_ = new RoomSearchInput(this); roomSearch_->setFont(font); - roomSearch_->setPlaceholderText(tr("Find a room...")); - - completer_ = new QCompleter(); - completer_->setCaseSensitivity(Qt::CaseInsensitive); - completer_->setCompletionMode(QCompleter::PopupCompletion); - completer_->setWidget(this); + roomSearch_->setPlaceholderText(tr("Search for a room...")); topLayout_ = new QVBoxLayout(this); topLayout_->addWidget(roomSearch_); - connect(completer_, SIGNAL(highlighted(QString)), roomSearch_, SLOT(setText(QString))); - connect(roomSearch_, &QLineEdit::textEdited, this, [this](const QString &prefix) { - if (prefix.isEmpty()) { - completer_->popup()->hide(); - selection_ = -1; + connect(this, + &QuickSwitcher::queryResults, + this, + [this](const std::vector &rooms) { + 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; } - if (prefix != completer_->completionPrefix()) { - completer_->setCompletionPrefix(prefix); - selection_ = -1; - } - - completer_->popup()->setWindowFlags(completer_->popup()->windowFlags() | - Qt::ToolTip | Qt::NoDropShadowWindowHint); - completer_->popup()->setAttribute(Qt::WA_ShowWithoutActivating); - completer_->complete(); + QtConcurrent::run([this, query = query.toLower()]() { + try { + emit queryResults(cache_->searchRooms(query.toStdString())); + } catch (const lmdb::error &e) { + qWarning() << "room search failed:" << e.what(); + } + }); }); - connect(roomSearch_, &RoomSearchInput::selectNextCompletion, this, [this]() { - selection_ += 1; - - if (!completer_->setCurrentRow(selection_)) { - selection_ = 0; - completer_->setCurrentRow(selection_); - } - - completer_->popup()->setCurrentIndex(completer_->currentIndex()); - }); - - 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_, + &RoomSearchInput::selectNextCompletion, + &popup_, + &SuggestionsPopup::selectNextSuggestion); + connect(roomSearch_, + &RoomSearchInput::selectPreviousCompletion, + &popup_, + &SuggestionsPopup::selectPreviousSuggestion); + connect(&popup_, &SuggestionsPopup::itemSelected, this, &QuickSwitcher::roomSelected); + connect(roomSearch_, &RoomSearchInput::hiding, this, [this]() { popup_.hide(); }); connect(roomSearch_, &QLineEdit::returnPressed, this, [this]() { 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(); + popup_.selectHoveredSuggestion(); }); } -void -QuickSwitcher::setRoomList(const std::map &rooms) -{ - rooms_ = rooms; - - QStringList items; - for (const auto &room : rooms) - items << room.first; - - completer_->setModel(new QStringListModel(items)); -} - void QuickSwitcher::paintEvent(QPaintEvent *) { diff --git a/src/SuggestionsPopup.cpp b/src/SuggestionsPopup.cpp index 49495e71..cb569ddf 100644 --- a/src/SuggestionsPopup.cpp +++ b/src/SuggestionsPopup.cpp @@ -1,7 +1,5 @@ #include "Avatar.h" #include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" #include "Config.h" #include "DropShadow.h" #include "SuggestionsPopup.hpp" @@ -15,10 +13,9 @@ constexpr int PopupHMargin = 5; constexpr int PopupItemMargin = 4; -PopupItem::PopupItem(QWidget *parent, const QString &user_id) +PopupItem::PopupItem(QWidget *parent) : QWidget(parent) , avatar_{new Avatar(this)} - , user_id_{user_id} , hovering_{false} { setMouseTracking(true); @@ -27,29 +24,6 @@ PopupItem::PopupItem(QWidget *parent, const QString &user_id) topLayout_ = new QHBoxLayout(this); topLayout_->setContentsMargins( 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 @@ -68,11 +42,61 @@ void PopupItem::mousePressEvent(QMouseEvent *event) { 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); } +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) : QWidget(parent) { @@ -84,15 +108,32 @@ SuggestionsPopup::SuggestionsPopup(QWidget *parent) layout_->setSpacing(0); } +void +SuggestionsPopup::addRooms(const std::vector &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 SuggestionsPopup::addUsers(const QVector &users) { - // Remove all items from the layout. - QLayoutItem *item; - while ((item = layout_->takeAt(0)) != 0) { - delete item->widget(); - delete item; - } + removeItems(); if (users.isEmpty()) { hide(); @@ -100,9 +141,9 @@ SuggestionsPopup::addUsers(const QVector &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); - connect(user, &PopupItem::clicked, this, &SuggestionsPopup::itemSelected); + connect(user, &UserItem::clicked, this, &SuggestionsPopup::itemSelected); } resetSelection(); @@ -160,19 +201,6 @@ SuggestionsPopup::setHovering(int pos) widget->setHovering(true); } -void -SuggestionsPopup::selectHoveredSuggestion() -{ - const auto item = layout_->itemAt(selectedItem_); - if (!item) - return; - - const auto &widget = qobject_cast(item->widget()); - emit itemSelected(Cache::displayName(ChatPage::instance()->currentRoom(), widget->user())); - - resetSelection(); -} - void SuggestionsPopup::paintEvent(QPaintEvent *) { diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc index 1535f563..beae9aab 100644 --- a/src/TextInputWidget.cc +++ b/src/TextInputWidget.cc @@ -95,10 +95,9 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) &FilteredTextEdit::selectPreviousSuggestion, &popup_, &SuggestionsPopup::selectPreviousSuggestion); - connect(this, - &FilteredTextEdit::selectHoveredSuggestion, - &popup_, - &SuggestionsPopup::selectHoveredSuggestion); + connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() { + popup_.selectHoveredSuggestion(); + }); previewDialog_.hide(); } @@ -459,7 +458,7 @@ TextInputWidget::TextInputWidget(QWidget *parent) QtConcurrent::run([this, q = q.toLower().toStdString()]() { try { - emit input_->resultsRetrieved(cache_->getAutocompleteMatches( + emit input_->resultsRetrieved(cache_->searchUsers( ChatPage::instance()->currentRoom().toStdString(), q)); } catch (const lmdb::error &e) { std::cout << e.what() << '\n';