Implement searching in rooms
BUG: 457839
This commit is contained in:
@@ -45,6 +45,7 @@ add_library(neochat STATIC
|
|||||||
serverlistmodel.cpp
|
serverlistmodel.cpp
|
||||||
statemodel.cpp
|
statemodel.cpp
|
||||||
filetransferpseudojob.cpp
|
filetransferpseudojob.cpp
|
||||||
|
searchmodel.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(neochat-app
|
add_executable(neochat-app
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "neochatuser.h"
|
#include "neochatuser.h"
|
||||||
#include "notificationsmanager.h"
|
#include "notificationsmanager.h"
|
||||||
|
#include "searchmodel.h"
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
#include "pollhandler.h"
|
#include "pollhandler.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -228,6 +229,7 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
|
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
|
||||||
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
|
||||||
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
|
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
|
||||||
|
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
|
||||||
#ifdef QUOTIENT_07
|
#ifdef QUOTIENT_07
|
||||||
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
|
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ ColumnLayout {
|
|||||||
|
|
||||||
default property alias innerObject : column.children
|
default property alias innerObject : column.children
|
||||||
|
|
||||||
property Item hoverComponent: hoverActions
|
property Item hoverComponent: hoverActions ?? null
|
||||||
property bool isEmote: false
|
property bool isEmote: false
|
||||||
property bool cardBackground: true
|
property bool cardBackground: true
|
||||||
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !Config.compactLayout
|
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !Config.compactLayout
|
||||||
@@ -106,6 +106,9 @@ ColumnLayout {
|
|||||||
|
|
||||||
// Show hover actions by updating the global hover component to this delegate
|
// Show hover actions by updating the global hover component to this delegate
|
||||||
function updateHoverComponent() {
|
function updateHoverComponent() {
|
||||||
|
if (!hoverComponent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (hovered && !Kirigami.Settings.isMobile) {
|
if (hovered && !Kirigami.Settings.isMobile) {
|
||||||
hoverComponent.delegate = root
|
hoverComponent.delegate = root
|
||||||
hoverComponent.bubble = bubble
|
hoverComponent.bubble = bubble
|
||||||
@@ -229,10 +232,10 @@ ColumnLayout {
|
|||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: timeLabel
|
id: timeLabel
|
||||||
|
|
||||||
text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
|
text: visible ? model.time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
|
||||||
color: Kirigami.Theme.disabledTextColor
|
color: Kirigami.Theme.disabledTextColor
|
||||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||||
QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat)
|
QQC2.ToolTip.text: model.time.toLocaleString(Qt.locale(), Locale.LongFormat)
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
@@ -268,6 +271,7 @@ ColumnLayout {
|
|||||||
id: bubbleBackground
|
id: bubbleBackground
|
||||||
visible: cardBackground && !Config.compactLayout
|
visible: cardBackground && !Config.compactLayout
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
color: {
|
color: {
|
||||||
if (model.author.isLocalUser) {
|
if (model.author.isLocalUser) {
|
||||||
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
|
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
|
||||||
|
|||||||
75
src/qml/Page/SearchPage.qml
Normal file
75
src/qml/Page/SearchPage.qml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.20 as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
Kirigami.ScrollablePage {
|
||||||
|
id: searchPage
|
||||||
|
|
||||||
|
property var currentRoom
|
||||||
|
|
||||||
|
title: i18nc("@action:title", "Search Messages")
|
||||||
|
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||||
|
|
||||||
|
SearchModel {
|
||||||
|
id: searchModel
|
||||||
|
connection: Controller.activeConnection
|
||||||
|
searchText: searchField.text
|
||||||
|
room: searchPage.currentRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
header: RowLayout {
|
||||||
|
Kirigami.SearchField {
|
||||||
|
id: searchField
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Keys.onEnterPressed: searchButton.clicked()
|
||||||
|
Keys.onReturnPressed: searchButton.clicked()
|
||||||
|
}
|
||||||
|
QQC2.Button {
|
||||||
|
id: searchButton
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||||
|
onClicked: searchModel.search()
|
||||||
|
icon.name: "search"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: messageListView
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
spacing: 0
|
||||||
|
verticalLayoutDirection: ListView.BottomToTop
|
||||||
|
|
||||||
|
section.property: "section"
|
||||||
|
|
||||||
|
Kirigami.PlaceholderMessage {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: searchField.text.length === 0 && messageListView.count === 0
|
||||||
|
text: i18n("Enter a text to start searching")
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.PlaceholderMessage {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: searchField.text.length > 0 && messageListView.count === 0 && !searchModel.searching
|
||||||
|
text: i18n("No results found")
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.LoadingPlaceholder {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: searchModel.searching
|
||||||
|
}
|
||||||
|
|
||||||
|
model: searchModel
|
||||||
|
delegate: EventDelegate {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,6 +111,23 @@ Kirigami.OverlayDrawer {
|
|||||||
text: devtoolsButton.text
|
text: devtoolsButton.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
id: searchButton
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
icon.name: "search"
|
||||||
|
text: i18n("Search in this room")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
visible: Controller.quotientMinorVersion > 6
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
pageStack.pushDialogLayer("qrc:/SearchPage.qml", {
|
||||||
|
currentRoom: room
|
||||||
|
}, {
|
||||||
|
title: i18nc("@action:title", "Search")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
QQC2.ToolButton {
|
QQC2.ToolButton {
|
||||||
id: inviteButton
|
id: inviteButton
|
||||||
|
|
||||||
|
|||||||
@@ -96,5 +96,6 @@
|
|||||||
<file alias="EmojiTonesPicker.qml">qml/Component/Emoji/EmojiTonesPicker.qml</file>
|
<file alias="EmojiTonesPicker.qml">qml/Component/Emoji/EmojiTonesPicker.qml</file>
|
||||||
<file alias="EmojiDelegate.qml">qml/Component/Emoji/EmojiDelegate.qml</file>
|
<file alias="EmojiDelegate.qml">qml/Component/Emoji/EmojiDelegate.qml</file>
|
||||||
<file alias="EmojiGrid.qml">qml/Component/Emoji/EmojiGrid.qml</file>
|
<file alias="EmojiGrid.qml">qml/Component/Emoji/EmojiGrid.qml</file>
|
||||||
|
<file alias="SearchPage.qml">qml/Page/SearchPage.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
180
src/searchmodel.cpp
Normal file
180
src/searchmodel.cpp
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "searchmodel.h"
|
||||||
|
#include "messageeventmodel.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
#include "neochatuser.h"
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <connection.h>
|
||||||
|
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
#include <csapi/search.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
// TODO search only in the current room
|
||||||
|
|
||||||
|
SearchModel::SearchModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SearchModel::searchText() const
|
||||||
|
{
|
||||||
|
return m_searchText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchModel::setSearchText(const QString &searchText)
|
||||||
|
{
|
||||||
|
m_searchText = searchText;
|
||||||
|
Q_EMIT searchTextChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchModel::search()
|
||||||
|
{
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
Q_ASSERT(m_connection);
|
||||||
|
setSearching(true);
|
||||||
|
if (m_job) {
|
||||||
|
m_job->abandon();
|
||||||
|
m_job = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchJob::RoomEventsCriteria criteria{
|
||||||
|
m_searchText,
|
||||||
|
{},
|
||||||
|
RoomEventFilter{
|
||||||
|
.rooms = {m_room->id()},
|
||||||
|
},
|
||||||
|
"recent",
|
||||||
|
SearchJob::IncludeEventContext{3, 3, true},
|
||||||
|
false,
|
||||||
|
none,
|
||||||
|
};
|
||||||
|
|
||||||
|
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria});
|
||||||
|
m_job = job;
|
||||||
|
connect(job, &BaseJob::finished, this, [=] {
|
||||||
|
beginResetModel();
|
||||||
|
m_result = job->searchCategories().roomEvents;
|
||||||
|
endResetModel();
|
||||||
|
setSearching(false);
|
||||||
|
m_job = nullptr;
|
||||||
|
// TODO error handling
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection *SearchModel::connection() const
|
||||||
|
{
|
||||||
|
return m_connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchModel::setConnection(Connection *connection)
|
||||||
|
{
|
||||||
|
m_connection = connection;
|
||||||
|
Q_EMIT connectionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
auto row = index.row();
|
||||||
|
const auto &event = *m_result->results[row].result;
|
||||||
|
switch (role) {
|
||||||
|
case DisplayRole:
|
||||||
|
return m_room->eventToString(*m_result->results[row].result);
|
||||||
|
case ShowAuthorRole:
|
||||||
|
return true;
|
||||||
|
case AuthorRole:
|
||||||
|
return QVariantMap{
|
||||||
|
{"isLocalUser", event.senderId() == m_room->localUser()->id()},
|
||||||
|
{"id", event.senderId()},
|
||||||
|
{"avatarMediaId", m_connection->user(event.senderId())->avatarMediaId(m_room)},
|
||||||
|
{"avatarUrl", m_connection->user(event.senderId())->avatarUrl(m_room)},
|
||||||
|
{"displayName", m_connection->user(event.senderId())->displayname(m_room)},
|
||||||
|
{"display", m_connection->user(event.senderId())->name()},
|
||||||
|
{"color", dynamic_cast<NeoChatUser *>(m_connection->user(event.senderId()))->color()},
|
||||||
|
{"object", QVariant::fromValue(m_connection->user(event.senderId()))},
|
||||||
|
};
|
||||||
|
case ShowSectionRole:
|
||||||
|
if (row == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
|
||||||
|
case SectionRole:
|
||||||
|
return renderDate(event.originTimestamp());
|
||||||
|
case TimeRole:
|
||||||
|
return event.originTimestamp();
|
||||||
|
}
|
||||||
|
return MessageEventModel::DelegateType::Message;
|
||||||
|
#endif
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int SearchModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
if (m_result.has_value()) {
|
||||||
|
return m_result->results.size();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> SearchModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{EventTypeRole, "eventType"},
|
||||||
|
{DisplayRole, "display"},
|
||||||
|
{AuthorRole, "author"},
|
||||||
|
{ShowSectionRole, "showSection"},
|
||||||
|
{SectionRole, "section"},
|
||||||
|
{TimeRole, "time"},
|
||||||
|
{ShowAuthorRole, "showAuthor"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
NeoChatRoom *SearchModel::room() const
|
||||||
|
{
|
||||||
|
return m_room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchModel::setRoom(NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
m_room = room;
|
||||||
|
Q_EMIT roomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO deduplicate with messageeventmodel
|
||||||
|
QString renderDate(const QDateTime ×tamp)
|
||||||
|
{
|
||||||
|
auto date = timestamp.toLocalTime().date();
|
||||||
|
if (date == QDate::currentDate()) {
|
||||||
|
return i18n("Today");
|
||||||
|
}
|
||||||
|
if (date == QDate::currentDate().addDays(-1)) {
|
||||||
|
return i18n("Yesterday");
|
||||||
|
}
|
||||||
|
if (date == QDate::currentDate().addDays(-2)) {
|
||||||
|
return i18n("The day before yesterday");
|
||||||
|
}
|
||||||
|
if (date > QDate::currentDate().addDays(-7)) {
|
||||||
|
return date.toString("dddd");
|
||||||
|
}
|
||||||
|
|
||||||
|
return QLocale::system().toString(date, QLocale::ShortFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SearchModel::searching() const
|
||||||
|
{
|
||||||
|
return m_searching;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchModel::setSearching(bool searching)
|
||||||
|
{
|
||||||
|
m_searching = searching;
|
||||||
|
Q_EMIT searchingChanged();
|
||||||
|
}
|
||||||
78
src/searchmodel.h
Normal file
78
src/searchmodel.h
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
#include <csapi/search.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Quotient
|
||||||
|
{
|
||||||
|
class Connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NeoChatRoom;
|
||||||
|
|
||||||
|
class SearchModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
|
||||||
|
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||||
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
DisplayRole = Qt::DisplayRole,
|
||||||
|
EventTypeRole,
|
||||||
|
ShowAuthorRole,
|
||||||
|
AuthorRole,
|
||||||
|
ShowSectionRole,
|
||||||
|
SectionRole,
|
||||||
|
TimeRole,
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles);
|
||||||
|
SearchModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString searchText() const;
|
||||||
|
void setSearchText(const QString &searchText);
|
||||||
|
|
||||||
|
Quotient::Connection *connection() const;
|
||||||
|
void setConnection(Quotient::Connection *connection);
|
||||||
|
|
||||||
|
NeoChatRoom *room() const;
|
||||||
|
void setRoom(NeoChatRoom *room);
|
||||||
|
|
||||||
|
Q_INVOKABLE void search();
|
||||||
|
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
bool searching() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void searchTextChanged();
|
||||||
|
void connectionChanged();
|
||||||
|
void roomChanged();
|
||||||
|
void searchingChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setSearching(bool searching);
|
||||||
|
|
||||||
|
QString m_searchText;
|
||||||
|
Quotient::Connection *m_connection = nullptr;
|
||||||
|
NeoChatRoom *m_room = nullptr;
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
|
||||||
|
Quotient::SearchJob *m_job = nullptr;
|
||||||
|
#endif
|
||||||
|
bool m_searching = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
QString renderDate(const QDateTime &dateTime);
|
||||||
Reference in New Issue
Block a user