diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 481561d2..3cc2ab15 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -36,15 +36,7 @@ Page { id: emojiPopup colors: palette - - model: EmojiProxyModel { - category: Emoji.Category.People - - sourceModel: EmojiModel { - } - - } - + model: TimelineManager.completerFor("allemoji", "") } Component { diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml index dc79d0b1..4aad832d 100644 --- a/resources/qml/emoji/EmojiPicker.qml +++ b/resources/qml/emoji/EmojiPicker.qml @@ -49,6 +49,54 @@ Menu { anchors.right: parent.right anchors.topMargin: 2 + // Search field + TextField { + id: emojiSearch + + //width: gridView.width - 6 + Layout.topMargin: 3 + Layout.preferredWidth: 7 * 52 + 20 - 6 + placeholderText: qsTr("Search") + selectByMouse: true + rightPadding: clearSearch.width + onTextChanged: searchTimer.restart() + onVisibleChanged: { + if (visible) + forceActiveFocus(); + + } + + Timer { + id: searchTimer + + interval: 350 // tweak as needed? + onTriggered: { + emojiPopup.model.searchString = emojiSearch.text; + emojiPopup.model.category = Emoji.Category.Search; + } + } + + ToolButton { + id: clearSearch + + visible: emojiSearch.text !== '' + icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText) + focusPolicy: Qt.NoFocus + onClicked: emojiSearch.clear() + + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + // clear the default hover effects. + + background: Item { + } + + } + + } + // emoji grid GridView { id: gridView @@ -104,54 +152,6 @@ Menu { } - // Search field - header: TextField { - id: emojiSearch - - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: emojiScroll.width + 4 - placeholderText: qsTr("Search") - selectByMouse: true - rightPadding: clearSearch.width - onTextChanged: searchTimer.restart() - onVisibleChanged: { - if (visible) - forceActiveFocus(); - - } - - Timer { - id: searchTimer - - interval: 350 // tweak as needed? - onTriggered: { - emojiPopup.model.filter = emojiSearch.text; - emojiPopup.model.category = Emoji.Category.Search; - } - } - - ToolButton { - id: clearSearch - - visible: emojiSearch.text !== '' - icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText) - focusPolicy: Qt.NoFocus - onClicked: emojiSearch.clear() - - anchors { - verticalCenter: parent.verticalCenter - right: parent.right - } - // clear the default hover effects. - - background: Item { - } - - } - - } - ScrollBar.vertical: ScrollBar { id: emojiScroll } @@ -160,6 +160,7 @@ Menu { // Separator Rectangle { + visible: emojiSearch.text === '' Layout.fillWidth: true Layout.preferredHeight: 1 color: emojiPopup.colors.alternateBase @@ -167,6 +168,7 @@ Menu { // Category picker row RowLayout { + visible: emojiSearch.text === '' Layout.bottomMargin: 0 Layout.preferredHeight: 42 implicitHeight: 42 @@ -245,7 +247,8 @@ Menu { } ToolTip.visible: hovered onClicked: { - emojiPopup.model.category = model.category; + //emojiPopup.model.category = model.category; + gridView.positionViewAtIndex(emojiPopup.model.sourceModel.categoryToIndex(model.category), GridView.Beginning); } MouseArea { @@ -276,56 +279,6 @@ Menu { } - // Separator - Rectangle { - Layout.fillHeight: true - Layout.preferredWidth: 1 - implicitWidth: 1 - height: parent.height - color: emojiPopup.colors.alternateBase - } - - // Search Button is special - AbstractButton { - id: searchBtn - - hoverEnabled: true - Layout.alignment: Qt.AlignRight - Layout.bottomMargin: 0 - ToolTip.text: qsTr("Search") - ToolTip.visible: hovered - onClicked: { - // clear any filters - emojiPopup.model.category = Emoji.Category.Search; - gridView.positionViewAtBeginning(); - emojiSearch.forceActiveFocus(); - } - Layout.preferredWidth: 36 - Layout.preferredHeight: 36 - implicitWidth: 36 - implicitHeight: 36 - - MouseArea { - id: mouseArea - - anchors.fill: parent - onPressed: mouse.accepted = false - cursorShape: Qt.PointingHandCursor - } - - contentItem: Image { - anchors.right: parent.right - horizontalAlignment: Image.AlignHCenter - verticalAlignment: Image.AlignVCenter - sourceSize.width: 32 - sourceSize.height: 32 - fillMode: Image.Pad - smooth: true - source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText) - } - - } - } } diff --git a/src/Cache_p.h b/src/Cache_p.h index 51dc4cbc..1e388e77 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -405,6 +405,50 @@ private: event); } + template + std::optional> getStateEvent(lmdb::txn txn, + const std::string &room_id, + std::string_view state_key = "") + { + constexpr auto type = mtx::events::state_content_to_type; + static_assert(type != mtx::events::EventType::Unsupported, + "Not a supported type in state events."); + + if (room_id.empty()) + return std::nullopt; + + std::string_view value; + if (state_key.empty()) { + auto db = getStatesDb(txn, room_id); + if (!db.get(txn, to_string(type), value)) { + return std::nullopt; + } + } else { + auto db = getStatesKeyDb(txn, room_id); + std::string d = json::object({{"key", state_key}}).dump(); + std::string_view data = d; + + auto cursor = lmdb::cursor::open(txn, db); + if (!cursor.get(state_key, data, MDB_GET_BOTH)) + return std::nullopt; + + try { + auto eventsDb = getEventsDb(txn, room_id); + if (!eventsDb.get( + txn, json::parse(data)["id"].get(), value)) + return std::nullopt; + } catch (std::exception &e) { + return std::nullopt; + } + } + + try { + return json::parse(value).get>(); + } catch (std::exception &e) { + return std::nullopt; + } + } + void saveInvites(lmdb::txn &txn, const std::map &rooms); diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp index 2341c292..412708a2 100644 --- a/src/CompletionProxyModel.cpp +++ b/src/CompletionProxyModel.cpp @@ -12,16 +12,18 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, int max_mistakes, + size_t max_completions, QObject *parent) : QAbstractProxyModel(parent) , maxMistakes_(max_mistakes) + , max_completions_(max_completions) { setSourceModel(model); QRegularExpression splitPoints("\\s+|-"); // insert all the full texts for (int i = 0; i < sourceModel()->rowCount(); i++) { - if (i < 7) + if (static_cast(i) < max_completions_) mapping.push_back(i); auto string1 = sourceModel() @@ -82,14 +84,9 @@ CompletionProxyModel::invalidate() { auto key = searchString_.toUcs4(); beginResetModel(); - mapping = trie_.search(key, 7, maxMistakes_); + if (!key.empty()) // return default model data, if no search string + mapping = trie_.search(key, max_completions_, maxMistakes_); endResetModel(); - - std::string temp; - for (auto v : mapping) { - temp += std::to_string(v) + ", "; - } - nhlog::ui()->debug("mapping: {}", temp); } QHash @@ -101,12 +98,22 @@ CompletionProxyModel::roleNames() const int CompletionProxyModel::rowCount(const QModelIndex &) const { - return (int)mapping.size(); + if (searchString_.isEmpty()) + return std::min(static_cast(std::min(max_completions_, + std::numeric_limits::max())), + sourceModel()->rowCount()); + else + return (int)mapping.size(); } QModelIndex CompletionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const { + // return default model data, if no search string + if (searchString_.isEmpty()) { + return index(sourceIndex.row(), 0); + } + for (int i = 0; i < (int)mapping.size(); i++) { if (mapping[i] == sourceIndex.row()) { return index(i, 0); @@ -119,6 +126,12 @@ QModelIndex CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const { auto row = proxyIndex.row(); + + // return default model data, if no search string + if (searchString_.isEmpty()) { + return index(row, 0); + } + if (row < 0 || row >= (int)mapping.size()) return QModelIndex(); diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index f19e4d93..d85d9343 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -153,8 +153,9 @@ class CompletionProxyModel : public QAbstractProxyModel QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString) public: CompletionProxyModel(QAbstractItemModel *model, - int max_mistakes = 2, - QObject *parent = nullptr); + int max_mistakes = 2, + size_t max_completions = 7, + QObject *parent = nullptr); void invalidate(); @@ -184,4 +185,5 @@ private: trie trie_; std::vector mapping; int maxMistakes_; + size_t max_completions_; }; diff --git a/src/emoji/EmojiModel.cpp b/src/emoji/EmojiModel.cpp index 70b85934..66e7aeda 100644 --- a/src/emoji/EmojiModel.cpp +++ b/src/emoji/EmojiModel.cpp @@ -11,6 +11,20 @@ using namespace emoji; +int +EmojiModel::categoryToIndex(int category) +{ + auto dist = std::distance(Provider::emoji.begin(), + std::lower_bound(Provider::emoji.begin(), + Provider::emoji.end(), + static_cast(category), + [](const struct Emoji &e, Emoji::Category c) { + return e.category < c; + })); + + return static_cast(dist); +} + QHash EmojiModel::roleNames() const { @@ -60,59 +74,3 @@ EmojiModel::data(const QModelIndex &index, int role) const return {}; } - -EmojiProxyModel::EmojiProxyModel(QObject *parent) - : QSortFilterProxyModel(parent) -{} - -EmojiProxyModel::~EmojiProxyModel() {} - -Emoji::Category -EmojiProxyModel::category() const -{ - return category_; -} - -void -EmojiProxyModel::setCategory(Emoji::Category cat) -{ - if (category_ == cat) { - return; - } - - category_ = cat; - emit categoryChanged(); - - invalidateFilter(); -} - -QString -EmojiProxyModel::filter() const -{ - return filterRegExp().pattern(); -} - -void -EmojiProxyModel::setFilter(const QString &filter) -{ - if (filterRegExp().pattern() == filter) { - return; - } - - setFilterWildcard(filter); - emit filterChanged(); -} - -bool -EmojiProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const -{ - const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - const Emoji emoji = index.data(static_cast(EmojiModel::Roles::Emoji)).value(); - - // TODO: Add favorites / recently used - if (category_ != Emoji::Category::Search) { - return emoji.category == category_; - } - - return filterRegExp().isEmpty() ? true : filterRegExp().indexIn(emoji.shortName) != -1; -} diff --git a/src/emoji/EmojiModel.h b/src/emoji/EmojiModel.h index 1a8bf029..679563f1 100644 --- a/src/emoji/EmojiModel.h +++ b/src/emoji/EmojiModel.h @@ -30,38 +30,10 @@ public: using QAbstractListModel::QAbstractListModel; + Q_INVOKABLE int categoryToIndex(int category); + QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; }; - -class EmojiProxyModel : public QSortFilterProxyModel -{ - Q_OBJECT - - Q_PROPERTY( - emoji::Emoji::Category category READ category WRITE setCategory NOTIFY categoryChanged) - Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged) - -public: - explicit EmojiProxyModel(QObject *parent = nullptr); - ~EmojiProxyModel() override; - - Emoji::Category category() const; - void setCategory(Emoji::Category cat); - - QString filter() const; - void setFilter(const QString &filter); - -signals: - void categoryChanged(); - void filterChanged(); - -protected: - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; - -private: - Emoji::Category category_ = Emoji::Category::Search; - emoji::Provider emoji_provider_; -}; } diff --git a/src/emoji/EmojiSearchModel.h b/src/emoji/EmojiSearchModel.h deleted file mode 100644 index 64af83dd..00000000 --- a/src/emoji/EmojiSearchModel.h +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include "EmojiModel.h" - -#include -#include -#include -#include - -namespace emoji { - -// Map emoji data to searchable data -class EmojiSearchModel : public QSortFilterProxyModel -{ -public: - EmojiSearchModel(QObject *parent = nullptr) - : QSortFilterProxyModel(parent) - { - setSourceModel(new EmojiModel(this)); - } - QVariant data(const QModelIndex &index, int role = Qt::UserRole + 1) const override - { - switch (role) { - case Qt::DisplayRole: { - auto emoji = QSortFilterProxyModel::data(index, role).toString(); - return emoji + " :" + - toShortcode(data(index, EmojiModel::ShortName).toString()) + ":"; - } - case CompletionModel::CompletionRole: - return QSortFilterProxyModel::data(index, EmojiModel::Unicode); - case CompletionModel::SearchRole: { - return toShortcode( - QSortFilterProxyModel::data(index, EmojiModel::ShortName).toString()); - } - default: - return QSortFilterProxyModel::data(index, role); - } - } - -private: - QString toShortcode(QString shortname) const - { - return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower(); - } -}; -} diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index f15b0b14..e9986c7e 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -172,9 +172,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qRegisterMetaType>(); qmlRegisterType("im.nheko.EmojiModel", 1, 0, "EmojiModel"); - qmlRegisterType("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel"); - qmlRegisterUncreatableType( - "im.nheko.EmojiModel", 1, 0, "QAbstractItemModel", "Used by proxy models"); qmlRegisterUncreatableType( "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models"); qmlRegisterUncreatableMetaObject(emoji::staticMetaObject, @@ -595,6 +592,11 @@ TimelineViewManager::completerFor(QString completerName, QString roomId) auto proxy = new CompletionProxyModel(emojiModel); emojiModel->setParent(proxy); return proxy; + } else if (completerName == "allemoji") { + auto emojiModel = new emoji::EmojiModel(); + auto proxy = new CompletionProxyModel(emojiModel, 1, static_cast(-1) / 4); + emojiModel->setParent(proxy); + return proxy; } else if (completerName == "room") { auto roomModel = new RoomsModel(false); auto proxy = new CompletionProxyModel(roomModel, 4);