Generic Search Page

Pull the generic aspects from Room search and join room pages into it's own component. This is done in anticipation of using the new generic search page for a user search functionality.

- `SearchPage` is now used for the generic version with the old one being renamed `RoomSearchPage`
- `JoinRoomPage` is renamed to `ExploreRoomsPage` inline with everywhere else in NeoChat

There is also some cleanup of the code for both search pages in here.
This commit is contained in:
James Graham
2024-01-19 17:59:45 +00:00
parent 80f3bd64b6
commit f6a5cc7c25
17 changed files with 459 additions and 393 deletions

View File

@@ -165,7 +165,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/UserInfoDesktop.qml
qml/RoomPage.qml
qml/RoomWindow.qml
qml/JoinRoomPage.qml
qml/ExploreRoomsPage.qml
qml/ManualRoomDialog.qml
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
@@ -271,7 +271,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/EmojiTonesPicker.qml
qml/EmojiDelegate.qml
qml/EmojiGrid.qml
qml/SearchPage.qml
qml/RoomSearchPage.qml
qml/LocationDelegate.qml
qml/LocationChooser.qml
qml/TimelineView.qml
@@ -302,6 +302,8 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/NotificationsView.qml
qml/LoadingDelegate.qml
qml/TimelineEndDelegate.qml
qml/SearchPage.qml
qml/ServerComboBox.qml
RESOURCES
qml/confetti.png
qml/glowdot.png

View File

@@ -41,7 +41,7 @@ void PublicRoomListModel::setConnection(Connection *conn)
if (job) {
job->abandon();
job = nullptr;
Q_EMIT loadingChanged();
Q_EMIT searchingChanged();
}
if (m_connection) {
@@ -50,7 +50,6 @@ void PublicRoomListModel::setConnection(Connection *conn)
Q_EMIT connectionChanged();
Q_EMIT serverChanged();
Q_EMIT hasMoreChanged();
}
QString PublicRoomListModel::server() const
@@ -71,14 +70,14 @@ void PublicRoomListModel::setServer(const QString &value)
nextBatch = QString();
attempted = false;
rooms.clear();
Q_EMIT loadingChanged();
Q_EMIT searchingChanged();
endResetModel();
if (job) {
job->abandon();
job = nullptr;
Q_EMIT loadingChanged();
Q_EMIT searchingChanged();
}
if (m_connection) {
@@ -86,21 +85,20 @@ void PublicRoomListModel::setServer(const QString &value)
}
Q_EMIT serverChanged();
Q_EMIT hasMoreChanged();
}
QString PublicRoomListModel::keyword() const
QString PublicRoomListModel::searchText() const
{
return m_keyword;
return m_searchText;
}
void PublicRoomListModel::setKeyword(const QString &value)
void PublicRoomListModel::setSearchText(const QString &value)
{
if (m_keyword == value) {
if (m_searchText == value) {
return;
}
m_keyword = value;
m_searchText = value;
beginResetModel();
@@ -113,15 +111,14 @@ void PublicRoomListModel::setKeyword(const QString &value)
if (job) {
job->abandon();
job = nullptr;
Q_EMIT loadingChanged();
Q_EMIT searchingChanged();
}
if (m_connection) {
next();
}
Q_EMIT keywordChanged();
Q_EMIT hasMoreChanged();
Q_EMIT searchTextChanged();
}
bool PublicRoomListModel::showOnlySpaces() const
@@ -154,8 +151,8 @@ void PublicRoomListModel::next(int count)
if (m_showOnlySpaces) {
roomTypes += QLatin1String("m.space");
}
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, roomTypes});
Q_EMIT loadingChanged();
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
Q_EMIT searchingChanged();
connect(job, &BaseJob::finished, this, [this] {
attempted = true;
@@ -166,14 +163,10 @@ void PublicRoomListModel::next(int count)
this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
rooms.append(job->chunk());
this->endInsertRows();
if (job->nextBatch().isEmpty()) {
Q_EMIT hasMoreChanged();
}
}
this->job = nullptr;
Q_EMIT loadingChanged();
Q_EMIT searchingChanged();
});
}
@@ -271,12 +264,19 @@ int PublicRoomListModel::rowCount(const QModelIndex &parent) const
return rooms.count();
}
bool PublicRoomListModel::hasMore() const
bool PublicRoomListModel::canFetchMore(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return !(attempted && nextBatch.isEmpty());
}
bool PublicRoomListModel::loading() const
void PublicRoomListModel::fetchMore(const QModelIndex &parent)
{
Q_UNUSED(parent)
next();
}
bool PublicRoomListModel::searching() const
{
return job != nullptr;
}

View File

@@ -41,9 +41,9 @@ class PublicRoomListModel : public QAbstractListModel
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
/**
* @brief The filter keyword for the list of public rooms.
* @brief The text to search the public room list for.
*/
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/**
* @brief Whether only space rooms should be shown.
@@ -51,14 +51,9 @@ class PublicRoomListModel : public QAbstractListModel
Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged)
/**
* @brief Whether the model has more items to load.
* @brief Whether the model is searching.
*/
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged)
/**
* @biref Whether the model is still loading.
*/
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
public:
/**
@@ -105,31 +100,31 @@ public:
[[nodiscard]] QString server() const;
void setServer(const QString &value);
[[nodiscard]] QString keyword() const;
void setKeyword(const QString &value);
[[nodiscard]] QString searchText() const;
void setSearchText(const QString &searchText);
[[nodiscard]] bool showOnlySpaces() const;
void setShowOnlySpaces(bool showOnlySpaces);
[[nodiscard]] bool hasMore() const;
[[nodiscard]] bool searching() const;
[[nodiscard]] bool loading() const;
private:
QPointer<Quotient::Connection> m_connection = nullptr;
QString m_server;
QString m_searchText;
bool m_showOnlySpaces = false;
/**
* @brief Load the next set of rooms.
*
* @param count the maximum number of rooms to load.
*/
Q_INVOKABLE void next(int count = 50);
private:
Quotient::Connection *m_connection = nullptr;
QString m_server;
QString m_keyword;
bool m_showOnlySpaces = false;
void next(int count = 50);
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
bool attempted = false;
bool m_loading = false;
bool m_searching = false;
QString nextBatch;
QList<Quotient::PublicRoomsChunk> rooms;
@@ -139,8 +134,7 @@ private:
Q_SIGNALS:
void connectionChanged();
void serverChanged();
void keywordChanged();
void searchTextChanged();
void showOnlySpacesChanged();
void hasMoreChanged();
void loadingChanged();
void searchingChanged();
};

View File

@@ -36,7 +36,7 @@ void SearchModel::setSearchText(const QString &searchText)
void SearchModel::search()
{
Q_ASSERT(m_connection);
Q_ASSERT(m_room);
setSearching(true);
if (m_job) {
m_job->abandon();
@@ -62,7 +62,7 @@ void SearchModel::search()
};
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria});
auto job = m_room->connection()->callApi<SearchJob>(SearchJob::Categories{criteria});
m_job = job;
connect(job, &BaseJob::finished, this, [this, job] {
beginResetModel();
@@ -74,17 +74,6 @@ void SearchModel::search()
});
}
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
{
auto row = index.row();

View File

@@ -31,11 +31,6 @@ class SearchModel : public QAbstractListModel
*/
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/**
* @brief The current connection that the model is using to search for messages.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/**
* @brief The current room that the search is being done from.
*/
@@ -94,9 +89,6 @@ public:
QString searchText() const;
void setSearchText(const QString &searchText);
Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *connection);
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
@@ -130,7 +122,6 @@ public:
Q_SIGNALS:
void searchTextChanged();
void connectionChanged();
void roomChanged();
void searchingChanged();
@@ -141,7 +132,6 @@ private:
void setSearching(bool searching);
QString m_searchText;
Quotient::Connection *m_connection = nullptr;
NeoChatRoom *m_room = nullptr;
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
Quotient::SearchJob *m_job = nullptr;

View File

@@ -103,7 +103,7 @@ FormCard.FormCardPage {
visible: !chosenRoomDelegate.visible
text: i18nc("@action:button", "Pick room")
onClicked: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName;
@@ -182,7 +182,7 @@ FormCard.FormCardPage {
}
onClicked: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName;

View File

@@ -21,7 +21,7 @@ RowLayout {
text: i18n("Explore rooms")
icon.name: "compass"
onTriggered: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId))

View File

@@ -52,7 +52,7 @@ ColumnLayout {
text: i18n("Explore rooms")
icon.name: "compass"
onTriggered: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId));

View File

@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
/**
* @brief Component for finding rooms for the public list.
*
* This component is based on a SearchPage, adding the functionality to select or
* enter a server in the header, as well as the ability to manually type a room in
* if the public room search cannot find it.
*
* @sa SearchPage
*/
SearchPage {
id: root
/**
* @brief The connection for the current local user.
*/
required property NeoChatConnection connection
/**
* @brief Whether results should only includes spaces.
*/
property bool showOnlySpaces: false
/**
* @brief Signal emitted when a room is selected.
*
* The signal contains all the room's info so that it can be acted
* upon as required, e.g. joining or entering the room or adding the room as
* the child of a space.
*/
signal roomSelected(string roomId,
string displayName,
url avatarUrl,
string alias,
string topic,
int memberCount,
bool isJoined)
title: i18nc("@action:title", "Explore Rooms")
Component.onCompleted: focusSearch()
headerTrailing: ServerComboBox {
id: serverComboBox
connection: root.connection
}
model: PublicRoomListModel {
id: publicRoomListModel
connection: root.connection
server: serverComboBox.server
showOnlySpaces: root.showOnlySpaces
}
modelDelegate: ExplorerDelegate {
onRoomSelected: (roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();
}
}
listHeaderDelegate: Delegates.RoundedItemDelegate {
onClicked: _private.openManualRoomDialog()
text: i18n("Enter a room address")
icon.name: "compass"
icon.width: Kirigami.Units.gridUnit * 2
icon.height: Kirigami.Units.gridUnit * 2
}
listFooterDelegate: QQC2.ProgressBar {
width: ListView.view.width
leftInset: Kirigami.Units.largeSpacing
rightInset: Kirigami.Units.largeSpacing
visible: root.count !== 0 && publicRoomListModel.searching
indeterminate: true
}
searchFieldPlaceholder: i18n("Find a room...")
noResultPlaceholderMessage: i18nc("@info:label", "No public rooms found")
Component {
id: manualRoomDialog
ManualRoomDialog {}
}
QtObject {
id: _private
function openManualRoomDialog() {
let dialog = manualRoomDialog.createObject(applicationWindow().overlay, {connection: root.connection});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();
});
dialog.open();
}
}
}

View File

@@ -61,7 +61,7 @@ Labs.MenuBar {
Labs.MenuItem {
text: i18nc("menu", "Browse Chats…")
onTriggered: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId))

View File

@@ -1,285 +0,0 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
Kirigami.ScrollablePage {
id: root
required property NeoChatConnection connection
property bool showOnlySpaces: false
property alias keyword: identifierField.text
property string server
/**
* @brief Signal emitted when a room is selected.
*
* The signal contains all the room's info so that it can be acted
* upon as required, e.g. joinng or entering the room or adding the room as
* the child of a space.
*/
signal roomSelected(string roomId,
string displayName,
url avatarUrl,
string alias,
string topic,
int memberCount,
bool isJoined)
title: i18n("Explore Rooms")
Component.onCompleted: identifierField.forceActiveFocus()
header: QQC2.Control {
padding: Kirigami.Units.largeSpacing
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
color: Kirigami.Theme.backgroundColor
}
contentItem: RowLayout {
Kirigami.SearchField {
id: identifierField
Layout.fillWidth: true
placeholderText: i18n("Find a room...")
}
QQC2.ComboBox {
id: serverField
// TODO: in KF6 we should be able to switch to using implicitContentWidthPolicy
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
Component.onCompleted: currentIndex = 0
textRole: "url"
valueRole: "url"
model: ServerListModel {
id: serverListModel
connection: root.connection
}
delegate: Delegates.RoundedItemDelegate {
id: serverItem
required property int index
required property string url
required property bool isAddServerDelegate
required property bool isHomeServer
required property bool isDeletable
text: isAddServerDelegate ? i18n("Add New Server") : url
highlighted: false
topInset: index === 0 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
bottomInset: index === ListView.view.count - 1 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
onClicked: if (isAddServerDelegate) {
addServerSheet.open()
}
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
Delegates.SubtitleContentItem {
itemDelegate: serverItem
subtitle: serverItem.isHomeServer ? i18n("Home Server") : ""
Layout.fillWidth: true
}
QQC2.ToolButton {
visible: serverItem.isAddServerDelegate || serverItem.isDeletable
icon.name: serverItem.isAddServerDelegate ? "list-add" : "dialog-close"
text: i18nc("@action:button", "Add new server")
Accessible.name: text
display: QQC2.AbstractButton.IconOnly
onClicked: {
if (serverField.currentIndex === serverItem.index && serverItem.isDeletable) {
serverField.currentIndex = 0;
server = serverField.currentValue;
serverField.popup.close();
}
if (serverItem.isAddServerDelegate) {
addServerSheet.open();
serverItem.clicked();
} else {
serverListModel.removeServerAtIndex(serverItem.index);
}
}
}
}
}
onActivated: {
if (currentIndex !== count - 1) {
server = currentValue
}
}
Kirigami.OverlaySheet {
id: addServerSheet
parent: applicationWindow().overlay
title: i18nc("@title:window", "Add server")
onOpened: if (!serverUrlField.isValidServer && !addServerSheet.opened) {
serverField.currentIndex = 0
server = serverField.currentValue
} else if (addServerSheet.opened) {
serverUrlField.forceActiveFocus()
}
contentItem: Kirigami.FormLayout {
QQC2.Label {
Layout.minimumWidth: Kirigami.Units.gridUnit * 20
text: serverUrlField.length > 0 ? (serverUrlField.acceptableInput ? (serverUrlField.isValidServer ? i18n("Valid server entered") : i18n("This server cannot be resolved or has already been added")) : i18n("The entered text is not a valid url")) : i18n("Enter server url e.g. kde.org")
color: serverUrlField.length > 0 ? (serverUrlField.acceptableInput ? (serverUrlField.isValidServer ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.negativeTextColor) : Kirigami.Theme.negativeTextColor) : Kirigami.Theme.textColor
}
QQC2.TextField {
id: serverUrlField
property bool isValidServer: false
Kirigami.FormData.label: i18n("Server URL")
onTextChanged: {
if(acceptableInput) {
serverListModel.checkServer(text)
}
}
validator: RegularExpressionValidator {
regularExpression: /^[a-zA-Z0-9-]{1,61}\.([a-zA-Z]{2,}|[a-zA-Z0-9-]{2,}\.[a-zA-Z]{2,3})$/
}
Connections {
target: serverListModel
function onServerCheckComplete(url, valid) {
if (url == serverUrlField.text && valid) {
serverUrlField.isValidServer = true
}
}
}
}
QQC2.Button {
id: okButton
text: i18nc("@action:button", "Ok")
enabled: serverUrlField.acceptableInput && serverUrlField.isValidServer
onClicked: {
serverListModel.addServer(serverUrlField.text)
serverField.currentIndex = serverField.indexOfValue(serverUrlField.text)
server = serverField.currentValue
serverUrlField.text = ""
addServerSheet.close();
}
}
}
}
}
}
Kirigami.Separator {
z: 999
anchors {
left: parent.left
right: parent.right
top: parent.bottom
}
}
}
ListView {
id: publicRoomsListView
topMargin: Math.round(Kirigami.Units.smallSpacing / 2)
bottomMargin: Math.round(Kirigami.Units.smallSpacing / 2)
model: PublicRoomListModel {
id: publicRoomListModel
connection: root.connection
server: root.server
keyword: root.keyword
showOnlySpaces: root.showOnlySpaces
}
onContentYChanged: {
if(publicRoomListModel.hasMore && contentHeight - contentY < publicRoomsListView.height + 200)
publicRoomListModel.next();
}
delegate: ExplorerDelegate {
onRoomSelected: (roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();
}
}
header: Delegates.RoundedItemDelegate {
Layout.fillWidth: true
onClicked: _private.openManualRoomDialog()
text: i18n("Enter a room address")
icon.name: "compass"
icon.width: Kirigami.Units.gridUnit * 2
icon.height: Kirigami.Units.gridUnit * 2
}
footer: QQC2.ProgressBar {
width: parent.width
visible: publicRoomsListView.count !== 0 && publicRoomsListView.model.loading
indeterminate: true
padding: Kirigami.Units.largeSpacing * 2
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
}
Kirigami.LoadingPlaceholder {
anchors.centerIn: parent
visible: publicRoomsListView.model.loading && publicRoomsListView.count === 0
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
visible: !publicRoomsListView.model.loading && publicRoomsListView.count === 0
text: i18nc("@info:label", "No public rooms found")
}
}
Component {
id: manualRoomDialog
ManualRoomDialog {}
}
QtObject {
id: _private
function openManualRoomDialog() {
let dialog = manualRoomDialog.createObject(applicationWindow().overlay, {connection: root.connection});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.closeDialog();
});
dialog.open();
}
}
}

View File

@@ -95,9 +95,8 @@ QQC2.ScrollView {
Layout.fillWidth: true
onClicked: {
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/SearchPage.qml", {
currentRoom: root.room,
connection: root.connection
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/RoomSearchPage.qml", {
room: root.room
}, {
title: i18nc("@action:title", "Search")
})

View File

@@ -164,7 +164,7 @@ Kirigami.Page {
icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add"
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
onTriggered: {
let dialog = pageStack.layers.push("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {
let dialog = pageStack.layers.push("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {
connection: root.connection,
keyword: sortFilterRoomListModel.filterText
}, {

View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import org.kde.neochat
/**
* @brief Component for finding messages in a room.
*
* This component is based on a SearchPage and allows the user to enter a search
* term into the input field and then search the room for messages with text that
* matches the input.
*
* @sa SearchPage
*/
SearchPage {
id: root
/**
* @brief The room the search is being performed in.
*/
required property NeoChatRoom room
title: i18nc("@action:title", "Search Messages")
model: SearchModel {
id: searchModel
room: root.room
}
modelDelegate: EventDelegate {
room: root.room
}
searchFieldPlaceholder: i18n("Find messages…")
noSearchPlaceholderMessage: i18n("Enter text to start searching")
noResultPlaceholderMessage: i18n("No messages found")
listVerticalLayoutDirection: ListView.BottomToTop
}

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
@@ -7,29 +7,87 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief Component for a generic search page.
*
* This component provides a header with the search field and a ListView to visualise
* search results from the given model.
*/
Kirigami.ScrollablePage {
id: root
property NeoChatRoom currentRoom
required property NeoChatConnection connection
/**
* @brief Any additional controls after the search button.
*/
property alias headerTrailing: headerContent.children
title: i18nc("@action:title", "Search Messages")
/**
* @brief The model that provides the search results.
*
* The model needs to provide the following properties:
* - searchText
* - searching
* Where searchText is the text from the searchField and is used to match results
* and searching is true while the model is finding results.
*
* The model must also provide a search() function to start the search if
* it doesn't do so when the searchText is changed.
*/
property alias model: listView.model
Kirigami.Theme.colorSet: Kirigami.Theme.Window
/**
* @brief The number of delegates currently in the view.
*/
property alias count: listView.count
SearchModel {
id: searchModel
connection: root.connection
searchText: searchField.text
room: root.currentRoom
/**
* @brief The delegate to use to visualize the model data.
*/
property alias modelDelegate: listView.delegate
/**
* @brief The delegate to appear as the header of the list.
*/
property alias listHeaderDelegate: listView.header
/**
* @brief The delegate to appear as the footer of the list.
*/
property alias listFooterDelegate: listView.footer
/**
* @brief The placeholder text in the search field.
*/
property alias searchFieldPlaceholder: searchField.placeholderText
/**
* @brief The text to show when no search term has been entered.
*/
property alias noSearchPlaceholderMessage: noSearchMessage.text
/**
* @brief The text to show when no results have been found.
*/
property alias noResultPlaceholderMessage: noResultMessage.text
/**
* @brief The verticalLayoutDirection property of the internal ListView.
*/
property alias listVerticalLayoutDirection: listView.verticalLayoutDirection
/**
* @brief Force the search field to be focussed.
*/
function focusSearch() {
searchField.forceActiveFocus();
}
header: QQC2.Control {
padding: Kirigami.Units.largeSpacing
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
color: Kirigami.Theme.backgroundColor
Kirigami.Separator {
@@ -42,6 +100,7 @@ Kirigami.ScrollablePage {
}
contentItem: RowLayout {
id: headerContent
spacing: Kirigami.Units.largeSpacing
Kirigami.SearchField {
@@ -50,44 +109,48 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true
Keys.onEnterPressed: searchButton.clicked()
Keys.onReturnPressed: searchButton.clicked()
onTextChanged: {
if (model) {
model.searchText = text;
}
}
}
QQC2.Button {
id: searchButton
onClicked: searchModel.search()
icon.name: "search"
onClicked: {
if (typeof model.search === 'function') {
model.search()
}
}
}
}
}
ListView {
id: messageListView
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
verticalLayoutDirection: ListView.BottomToTop
section.property: "section"
Kirigami.PlaceholderMessage {
id: noSearchMessage
anchors.centerIn: parent
visible: searchField.text.length === 0 && messageListView.count === 0
text: i18n("Enter a text to start searching")
visible: searchField.text.length === 0 && listView.count === 0
}
Kirigami.PlaceholderMessage {
id: noResultMessage
anchors.centerIn: parent
visible: searchField.text.length > 0 && messageListView.count === 0 && !searchModel.searching
text: i18n("No results found")
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching
}
Kirigami.LoadingPlaceholder {
anchors.centerIn: parent
visible: searchModel.searching
}
model: searchModel
delegate: EventDelegate {
room: root.currentRoom
visible: root.model.searching
}
}
}

View File

@@ -49,7 +49,7 @@ Kirigami.Dialog {
visible: !chosenRoomDelegate.visible
text: i18nc("@action:button", "Pick room")
onClicked: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.room.connection, showOnlySpaces: true}, {title: i18nc("@title", "Choose Parent Space")})
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.room.connection, showOnlySpaces: true}, {title: i18nc("@title", "Choose Parent Space")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName;
@@ -128,7 +128,7 @@ Kirigami.Dialog {
}
onClicked: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.room.connection, showOnlySpaces: true}, {title: i18nc("@title", "Explore Rooms")})
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/ExploreRoomsPage.qml", {connection: root.room.connection, showOnlySpaces: true}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName;

161
src/qml/ServerComboBox.qml Normal file
View File

@@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
QQC2.ComboBox {
id: root
/**
* @brief The connection for the current local user.
*/
required property NeoChatConnection connection
/**
* @brief The server to get the search results from.
*/
property string server
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
Component.onCompleted: currentIndex = 0
textRole: "url"
valueRole: "url"
model: ServerListModel {
id: serverListModel
connection: root.connection
}
delegate: Delegates.RoundedItemDelegate {
id: serverItem
required property int index
required property string url
required property bool isAddServerDelegate
required property bool isHomeServer
required property bool isDeletable
text: isAddServerDelegate ? i18n("Add New Server") : url
highlighted: false
topInset: index === 0 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
bottomInset: index === ListView.view.count - 1 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
onClicked: if (isAddServerDelegate) {
addServerSheet.open()
}
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
Delegates.SubtitleContentItem {
itemDelegate: serverItem
subtitle: serverItem.isHomeServer ? i18n("Home Server") : ""
Layout.fillWidth: true
}
QQC2.ToolButton {
visible: serverItem.isAddServerDelegate || serverItem.isDeletable
icon.name: serverItem.isAddServerDelegate ? "list-add" : "dialog-close"
text: i18nc("@action:button", "Add new server")
Accessible.name: text
display: QQC2.AbstractButton.IconOnly
onClicked: {
if (root.currentIndex === serverItem.index && serverItem.isDeletable) {
root.currentIndex = 0;
root.server = root.currentValue
root.popup.close();
}
if (serverItem.isAddServerDelegate) {
addServerSheet.open();
serverItem.clicked();
} else {
serverListModel.removeServerAtIndex(serverItem.index);
}
}
}
}
}
onActivated: {
if (currentIndex !== count - 1) {
root.server = root.currentValue
}
}
Kirigami.OverlaySheet {
id: addServerSheet
parent: applicationWindow().overlay
title: i18nc("@title:window", "Add server")
onOpened: if (!serverUrlField.isValidServer && !addServerSheet.opened) {
root.currentIndex = 0
root.server = root.currentValue
} else if (addServerSheet.opened) {
serverUrlField.forceActiveFocus()
}
onClosed: if (serverUrlField.length <= 0) {
root.currentIndex = root.indexOfValue(root.server)
}
contentItem: Kirigami.FormLayout {
QQC2.Label {
Layout.minimumWidth: Kirigami.Units.gridUnit * 20
text: serverUrlField.length > 0 ? (serverUrlField.acceptableInput ? (serverUrlField.isValidServer ? i18n("Valid server entered") : i18n("This server cannot be resolved or has already been added")) : i18n("The entered text is not a valid url")) : i18n("Enter server url e.g. kde.org")
color: serverUrlField.length > 0 ? (serverUrlField.acceptableInput ? (serverUrlField.isValidServer ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.negativeTextColor) : Kirigami.Theme.negativeTextColor) : Kirigami.Theme.textColor
}
QQC2.TextField {
id: serverUrlField
property bool isValidServer: false
Kirigami.FormData.label: i18n("Server URL")
onTextChanged: {
if(acceptableInput) {
serverListModel.checkServer(text)
}
}
validator: RegularExpressionValidator {
regularExpression: /^[a-zA-Z0-9-]{1,61}\.([a-zA-Z]{2,}|[a-zA-Z0-9-]{2,}\.[a-zA-Z]{2,3})$/
}
Connections {
target: serverListModel
function onServerCheckComplete(url, valid) {
if (url == serverUrlField.text && valid) {
serverUrlField.isValidServer = true
}
}
}
}
QQC2.Button {
id: okButton
text: i18nc("@action:button", "Ok")
enabled: serverUrlField.acceptableInput && serverUrlField.isValidServer
onClicked: {
serverListModel.addServer(serverUrlField.text)
root.currentIndex = root.indexOfValue(serverUrlField.text)
root.server = root.currentValue
serverUrlField.text = ""
addServerSheet.close();
}
}
}
}
}