diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 47ea681c4..b0a8ce63e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/models/publicroomlistmodel.cpp b/src/models/publicroomlistmodel.cpp index 3084c11a6..ad41d0315 100644 --- a/src/models/publicroomlistmodel.cpp +++ b/src/models/publicroomlistmodel.cpp @@ -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(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, roomTypes}); - Q_EMIT loadingChanged(); + job = m_connection->callApi(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; } diff --git a/src/models/publicroomlistmodel.h b/src/models/publicroomlistmodel.h index 73a1d0626..3e50c20d3 100644 --- a/src/models/publicroomlistmodel.h +++ b/src/models/publicroomlistmodel.h @@ -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 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 rooms; @@ -139,8 +134,7 @@ private: Q_SIGNALS: void connectionChanged(); void serverChanged(); - void keywordChanged(); + void searchTextChanged(); void showOnlySpacesChanged(); - void hasMoreChanged(); - void loadingChanged(); + void searchingChanged(); }; diff --git a/src/models/searchmodel.cpp b/src/models/searchmodel.cpp index 528afc151..ba00eb607 100644 --- a/src/models/searchmodel.cpp +++ b/src/models/searchmodel.cpp @@ -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::Categories{criteria}); + auto job = m_room->connection()->callApi(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(); diff --git a/src/models/searchmodel.h b/src/models/searchmodel.h index 17bf17fe6..c140dfc1f 100644 --- a/src/models/searchmodel.h +++ b/src/models/searchmodel.h @@ -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 m_result = Quotient::none; Quotient::SearchJob *m_job = nullptr; diff --git a/src/qml/CreateRoomDialog.qml b/src/qml/CreateRoomDialog.qml index fa8663caa..e5b77ef48 100644 --- a/src/qml/CreateRoomDialog.qml +++ b/src/qml/CreateRoomDialog.qml @@ -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; diff --git a/src/qml/ExploreComponent.qml b/src/qml/ExploreComponent.qml index 03038ce92..d66808683 100644 --- a/src/qml/ExploreComponent.qml +++ b/src/qml/ExploreComponent.qml @@ -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)) diff --git a/src/qml/ExploreComponentMobile.qml b/src/qml/ExploreComponentMobile.qml index 420a1a99b..ddb29128a 100644 --- a/src/qml/ExploreComponentMobile.qml +++ b/src/qml/ExploreComponentMobile.qml @@ -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)); diff --git a/src/qml/ExploreRoomsPage.qml b/src/qml/ExploreRoomsPage.qml new file mode 100644 index 000000000..54657ce1b --- /dev/null +++ b/src/qml/ExploreRoomsPage.qml @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2019 Black Hat +// SPDX-FileCopyrightText: 2020 Carl Schwan +// 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(); + } + } +} diff --git a/src/qml/GlobalMenu.qml b/src/qml/GlobalMenu.qml index 2af597529..0aa8b7657 100644 --- a/src/qml/GlobalMenu.qml +++ b/src/qml/GlobalMenu.qml @@ -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)) diff --git a/src/qml/JoinRoomPage.qml b/src/qml/JoinRoomPage.qml deleted file mode 100644 index 5ee442d20..000000000 --- a/src/qml/JoinRoomPage.qml +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Black Hat -// SPDX-FileCopyrightText: 2020 Carl Schwan -// 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(); - } - } -} diff --git a/src/qml/RoomInformation.qml b/src/qml/RoomInformation.qml index 2cfbeffe1..9458440b5 100644 --- a/src/qml/RoomInformation.qml +++ b/src/qml/RoomInformation.qml @@ -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") }) diff --git a/src/qml/RoomListPage.qml b/src/qml/RoomListPage.qml index 919faf488..cd8e9088f 100644 --- a/src/qml/RoomListPage.qml +++ b/src/qml/RoomListPage.qml @@ -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 }, { diff --git a/src/qml/RoomSearchPage.qml b/src/qml/RoomSearchPage.qml new file mode 100644 index 000000000..bd77be7ca --- /dev/null +++ b/src/qml/RoomSearchPage.qml @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// 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 +} diff --git a/src/qml/SearchPage.qml b/src/qml/SearchPage.qml index 9787448ee..24595fba7 100644 --- a/src/qml/SearchPage.qml +++ b/src/qml/SearchPage.qml @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2022 Tobias Fella -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2024 James Graham +// 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 } } } + diff --git a/src/qml/SelectParentDialog.qml b/src/qml/SelectParentDialog.qml index 64a453def..b6cfa8abe 100644 --- a/src/qml/SelectParentDialog.qml +++ b/src/qml/SelectParentDialog.qml @@ -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; diff --git a/src/qml/ServerComboBox.qml b/src/qml/ServerComboBox.qml new file mode 100644 index 000000000..54342ca85 --- /dev/null +++ b/src/qml/ServerComboBox.qml @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// 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(); + } + } + } + } + +}