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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ void SearchModel::setSearchText(const QString &searchText)
void SearchModel::search() void SearchModel::search()
{ {
Q_ASSERT(m_connection); Q_ASSERT(m_room);
setSearching(true); setSearching(true);
if (m_job) { if (m_job) {
m_job->abandon(); 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; m_job = job;
connect(job, &BaseJob::finished, this, [this, job] { connect(job, &BaseJob::finished, this, [this, job] {
beginResetModel(); 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 QVariant SearchModel::data(const QModelIndex &index, int role) const
{ {
auto row = index.row(); auto row = index.row();

View File

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

View File

@@ -103,7 +103,7 @@ FormCard.FormCardPage {
visible: !chosenRoomDelegate.visible visible: !chosenRoomDelegate.visible
text: i18nc("@action:button", "Pick room") text: i18nc("@action:button", "Pick room")
onClicked: { 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) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId; chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName; chosenRoomDelegate.displayName = displayName;
@@ -182,7 +182,7 @@ FormCard.FormCardPage {
} }
onClicked: { 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) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId; chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName; chosenRoomDelegate.displayName = displayName;

View File

@@ -21,7 +21,7 @@ RowLayout {
text: i18n("Explore rooms") text: i18n("Explore rooms")
icon.name: "compass" icon.name: "compass"
onTriggered: { 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) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) { if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId)) RoomManager.enterRoom(root.connection.room(roomId))

View File

@@ -52,7 +52,7 @@ ColumnLayout {
text: i18n("Explore rooms") text: i18n("Explore rooms")
icon.name: "compass" icon.name: "compass"
onTriggered: { 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) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) { if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId)); 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 { Labs.MenuItem {
text: i18nc("menu", "Browse Chats…") text: i18nc("menu", "Browse Chats…")
onTriggered: { 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) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) { if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId)) 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 Layout.fillWidth: true
onClicked: { onClicked: {
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/SearchPage.qml", { pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/RoomSearchPage.qml", {
currentRoom: root.room, room: root.room
connection: root.connection
}, { }, {
title: i18nc("@action:title", "Search") title: i18nc("@action:title", "Search")
}) })

View File

@@ -164,7 +164,7 @@ Kirigami.Page {
icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add" icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add"
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms") text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
onTriggered: { 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, connection: root.connection,
keyword: sortFilterRoomListModel.filterText 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-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
@@ -7,29 +7,87 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami 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 { Kirigami.ScrollablePage {
id: root 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 * @brief The delegate to use to visualize the model data.
connection: root.connection */
searchText: searchField.text property alias modelDelegate: listView.delegate
room: root.currentRoom
/**
* @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 { header: QQC2.Control {
padding: Kirigami.Units.largeSpacing padding: Kirigami.Units.largeSpacing
background: Rectangle { background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
Kirigami.Separator { Kirigami.Separator {
@@ -42,6 +100,7 @@ Kirigami.ScrollablePage {
} }
contentItem: RowLayout { contentItem: RowLayout {
id: headerContent
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
Kirigami.SearchField { Kirigami.SearchField {
@@ -50,44 +109,48 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true Layout.fillWidth: true
Keys.onEnterPressed: searchButton.clicked() Keys.onEnterPressed: searchButton.clicked()
Keys.onReturnPressed: searchButton.clicked() Keys.onReturnPressed: searchButton.clicked()
onTextChanged: {
if (model) {
model.searchText = text;
}
}
} }
QQC2.Button { QQC2.Button {
id: searchButton id: searchButton
onClicked: searchModel.search()
icon.name: "search" icon.name: "search"
onClicked: {
if (typeof model.search === 'function') {
model.search()
}
}
} }
} }
} }
ListView { ListView {
id: messageListView id: listView
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
spacing: 0 spacing: 0
verticalLayoutDirection: ListView.BottomToTop
section.property: "section" section.property: "section"
Kirigami.PlaceholderMessage { Kirigami.PlaceholderMessage {
id: noSearchMessage
anchors.centerIn: parent anchors.centerIn: parent
visible: searchField.text.length === 0 && messageListView.count === 0 visible: searchField.text.length === 0 && listView.count === 0
text: i18n("Enter a text to start searching")
} }
Kirigami.PlaceholderMessage { Kirigami.PlaceholderMessage {
id: noResultMessage
anchors.centerIn: parent anchors.centerIn: parent
visible: searchField.text.length > 0 && messageListView.count === 0 && !searchModel.searching visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching
text: i18n("No results found")
} }
Kirigami.LoadingPlaceholder { Kirigami.LoadingPlaceholder {
anchors.centerIn: parent anchors.centerIn: parent
visible: searchModel.searching visible: root.model.searching
}
model: searchModel
delegate: EventDelegate {
room: root.currentRoom
} }
} }
} }

View File

@@ -49,7 +49,7 @@ Kirigami.Dialog {
visible: !chosenRoomDelegate.visible visible: !chosenRoomDelegate.visible
text: i18nc("@action:button", "Pick room") text: i18nc("@action:button", "Pick room")
onClicked: { 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) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId; chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName; chosenRoomDelegate.displayName = displayName;
@@ -128,7 +128,7 @@ Kirigami.Dialog {
} }
onClicked: { 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) => { dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
chosenRoomDelegate.roomId = roomId; chosenRoomDelegate.roomId = roomId;
chosenRoomDelegate.displayName = displayName; 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();
}
}
}
}
}