Create new rooms module
This commit is contained in:
43
src/rooms/CMakeLists.txt
Normal file
43
src/rooms/CMakeLists.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
qt_add_library(Rooms STATIC)
|
||||
ecm_add_qml_module(Rooms GENERATE_PLUGIN_SOURCE
|
||||
URI org.kde.neochat.rooms
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/rooms
|
||||
QML_FILES
|
||||
RoomListPage.qml
|
||||
SpaceDrawer.qml
|
||||
RoomDelegate.qml
|
||||
RoomTreeSection.qml
|
||||
ExploreComponent.qml
|
||||
ExploreComponentMobile.qml
|
||||
UserInfo.qml
|
||||
UserInfoDesktop.qml
|
||||
RoomContextMenu.qml
|
||||
SpaceListContextMenu.qml
|
||||
SOURCES
|
||||
models/publicroomlistmodel.cpp
|
||||
models/roomtreeitem.cpp
|
||||
models/roomtreemodel.cpp
|
||||
models/sortfilterroomlistmodel.cpp
|
||||
models/sortfilterroomtreemodel.cpp
|
||||
models/sortfilterspacelistmodel.cpp
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(Rooms
|
||||
HEADER "publicroomlist_logging.h"
|
||||
IDENTIFIER "PublicRoomList"
|
||||
CATEGORY_NAME "org.kde.neochat.publicroomlistmodel"
|
||||
DESCRIPTION "Neochat: publicroomlistmodel"
|
||||
DEFAULT_SEVERITY Info
|
||||
EXPORT NEOCHAT
|
||||
)
|
||||
|
||||
target_include_directories(Rooms PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||
target_link_libraries(Rooms PRIVATE
|
||||
Qt::Core
|
||||
Qt::Quick
|
||||
KF6::Kirigami
|
||||
LibNeoChat
|
||||
)
|
||||
131
src/rooms/ExploreComponent.qml
Normal file
131
src/rooms/ExploreComponent.qml
Normal file
@@ -0,0 +1,131 @@
|
||||
// SPDX-FileCopyrightText: 2023 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
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property var desiredWidth
|
||||
property bool collapsed: false
|
||||
required property NeoChatConnection connection
|
||||
|
||||
signal search
|
||||
|
||||
/**
|
||||
* @brief Emitted when the text is changed in the search field.
|
||||
*/
|
||||
signal textChanged(string newText)
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.collapsed
|
||||
text: i18nc("@title", "Rooms")
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
visible: root.collapsed
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: searchButton
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: root.search();
|
||||
icon.name: "search"
|
||||
text: i18nc("@action", "Search Rooms")
|
||||
Shortcut {
|
||||
sequence: "Ctrl+F"
|
||||
onActivated: searchButton.clicked()
|
||||
}
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: menuButton
|
||||
Accessible.role: Accessible.ButtonMenu
|
||||
Accessible.onPressAction: menuButton.action.trigger()
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
action: QQC2.Action {
|
||||
text: i18nc("@action:button", "Show Menu")
|
||||
icon.name: "application-menu-symbolic"
|
||||
onTriggered: {
|
||||
const item = menu.createObject(menuButton);
|
||||
item.closed.connect(menuButton.toggle);
|
||||
item.open();
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Component {
|
||||
id: menu
|
||||
QQC2.Menu {
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Find your friends")
|
||||
icon.name: "list-add-user"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
})
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Create a Room")
|
||||
icon.name: "system-users-symbolic"
|
||||
action: QQC2.Action {
|
||||
shortcut: StandardKey.New
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Room")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Create a Space")
|
||||
icon.name: "list-add"
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
connection: root.connection,
|
||||
isSpace: true,
|
||||
title: i18nc("@title", "Create a Space")
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Space")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Scan a QR Code")
|
||||
icon.name: "view-barcode-qr"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Scan a QR Code")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
176
src/rooms/ExploreComponentMobile.qml
Normal file
176
src/rooms/ExploreComponentMobile.qml
Normal file
@@ -0,0 +1,176 @@
|
||||
// SPDX-FileCopyrightText: 2023 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
|
||||
|
||||
Kirigami.NavigationTabBar {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The connection for the current user.
|
||||
*/
|
||||
required property NeoChatConnection connection
|
||||
|
||||
/**
|
||||
* @brief Emitted when the text is changed in the search field.
|
||||
*/
|
||||
signal textChanged(string newText)
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
id: infoAction
|
||||
text: i18n("Search")
|
||||
icon.name: "search"
|
||||
onTriggered: {
|
||||
if (explorePopup.visible && explorePopupLoader.sourceComponent == search) {
|
||||
explorePopup.close();
|
||||
root.currentIndex = -1;
|
||||
} else if (explorePopup.visible && explorePopupLoader.sourceComponent != search) {
|
||||
explorePopup.close();
|
||||
explorePopup.open();
|
||||
} else {
|
||||
explorePopup.open();
|
||||
}
|
||||
explorePopupLoader.switchComponent(search);
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Explore rooms")
|
||||
icon.name: "compass"
|
||||
onTriggered: {
|
||||
explorePopup.close();
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||
});
|
||||
root.currentIndex = -1;
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Find your friends")
|
||||
icon.name: "list-add-user"
|
||||
onTriggered: {
|
||||
explorePopup.close();
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
});
|
||||
root.currentIndex = -1;
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Create New")
|
||||
icon.name: "list-add"
|
||||
onTriggered: {
|
||||
if (explorePopup.visible && explorePopupLoader.sourceComponent == create) {
|
||||
explorePopup.close();
|
||||
root.currentIndex = -1;
|
||||
} else if (explorePopup.visible && explorePopupLoader.sourceComponent != create) {
|
||||
explorePopup.close();
|
||||
explorePopup.open();
|
||||
} else {
|
||||
explorePopup.open();
|
||||
}
|
||||
explorePopupLoader.switchComponent(create);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
QQC2.Popup {
|
||||
id: explorePopup
|
||||
parent: root
|
||||
|
||||
y: -height + 1
|
||||
width: root.width
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
closePolicy: QQC2.Popup.CloseOnEscape
|
||||
|
||||
contentItem: Loader {
|
||||
id: explorePopupLoader
|
||||
sourceComponent: search
|
||||
|
||||
function switchComponent(newComponent) {
|
||||
if (sourceComponent == search) {
|
||||
root.textChanged("");
|
||||
}
|
||||
sourceComponent = newComponent;
|
||||
}
|
||||
}
|
||||
|
||||
background: ColumnLayout {
|
||||
spacing: 0
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: search
|
||||
Kirigami.SearchField {
|
||||
onTextChanged: root.textChanged(text)
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: create
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
Delegates.RoundedItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Create a Room")
|
||||
icon.name: "system-users-symbolic"
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Room")
|
||||
});
|
||||
explorePopup.close();
|
||||
}
|
||||
shortcut: StandardKey.New
|
||||
}
|
||||
}
|
||||
Delegates.RoundedItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Create a Space")
|
||||
icon.name: "list-add"
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
connection: root.connection,
|
||||
isSpace: true,
|
||||
title: i18nc("@title", "Create a Space")
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Space")
|
||||
});
|
||||
explorePopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/rooms/RoomContextMenu.qml
Normal file
158
src/rooms/RoomContextMenu.qml
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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 org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
|
||||
/**
|
||||
* Context menu when clicking on a room in the room list
|
||||
*/
|
||||
KirigamiComponents.ConvergentContextMenu {
|
||||
id: root
|
||||
|
||||
property NeoChatRoom room
|
||||
required property NeoChatConnection connection
|
||||
|
||||
headerContentItem: RowLayout {
|
||||
id: headerLayout
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
KirigamiComponents.Avatar {
|
||||
id: avatar
|
||||
source: room.avatarMediaUrl
|
||||
name: room.displayName
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
Kirigami.Heading {
|
||||
level: 5
|
||||
Layout.fillWidth: true
|
||||
text: room.displayName
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Mark as Read")
|
||||
icon.name: "checkmark"
|
||||
enabled: room.notificationCount > 0
|
||||
onTriggered: room.markAllMessagesAsRead()
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
separator: true
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Notifications")
|
||||
icon.name: "notifications"
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18n("Follow Global Setting")
|
||||
icon.name: "globe"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
checked: room.pushNotificationState === PushNotificationState.Default
|
||||
enabled: room.pushNotificationState != PushNotificationState.Unknown
|
||||
onTriggered: {
|
||||
room.pushNotificationState = PushNotificationState.Default;
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("As in 'notify for all messages'", "All")
|
||||
icon.name: "notifications"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
checked: room.pushNotificationState === PushNotificationState.All
|
||||
enabled: room.pushNotificationState != PushNotificationState.Unknown
|
||||
onTriggered: {
|
||||
room.pushNotificationState = PushNotificationState.All;
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("As in 'notify when the user is mentioned or the message contains a set keyword'", "@Mentions and Keywords")
|
||||
icon.name: "im-user"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
|
||||
enabled: room.pushNotificationState != PushNotificationState.Unknown
|
||||
onTriggered: {
|
||||
room.pushNotificationState = PushNotificationState.MentionKeyword;
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("As in 'do not notify for any messages'", "Off")
|
||||
icon.name: "notifications-disabled"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
checked: room.pushNotificationState === PushNotificationState.Mute
|
||||
enabled: room.pushNotificationState != PushNotificationState.Unknown
|
||||
onTriggered: {
|
||||
room.pushNotificationState = PushNotificationState.Mute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: room.isFavourite ? i18n("Remove from Favorites") : i18n("Add to Favorites")
|
||||
icon.name: room.isFavourite ? "rating" : "rating-unrated"
|
||||
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
|
||||
icon.name: room.isLowPriority ? "arrow-up-symbolic" : "arrow-down-symbolic"
|
||||
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
separator: true
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: room.isDirectChat() ? i18nc("@action:inmenu", "Copy user's Matrix ID to Clipboard") : i18nc("@action:inmenu", "Copy Address to Clipboard")
|
||||
icon.name: "edit-copy"
|
||||
onTriggered: if (room.isDirectChat()) {
|
||||
Clipboard.saveText(room.directChatRemoteMember.id);
|
||||
} else if (room.canonicalAlias.length === 0) {
|
||||
Clipboard.saveText(room.id);
|
||||
} else {
|
||||
Clipboard.saveText(room.canonicalAlias);
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("@action:inmenu", "Room Settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
onTriggered: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
separator: true
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Leave Room")
|
||||
icon.name: "go-previous"
|
||||
onTriggered: {
|
||||
Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
|
||||
room: root.room
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
}
|
||||
180
src/rooms/RoomDelegate.qml
Normal file
180
src/rooms/RoomDelegate.qml
Normal file
@@ -0,0 +1,180 @@
|
||||
// SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQml.Models
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: root
|
||||
|
||||
required property int index
|
||||
required property int contextNotificationCount
|
||||
required property bool hasHighlightNotifications
|
||||
required property NeoChatRoom currentRoom
|
||||
required property NeoChatConnection connection
|
||||
required property url avatar
|
||||
required property string subtitleText
|
||||
required property string displayName
|
||||
|
||||
property bool openOnClick: true
|
||||
property bool showConfigure: true
|
||||
|
||||
property bool collapsed: false
|
||||
|
||||
readonly property bool hasNotifications: contextNotificationCount > 0
|
||||
|
||||
Accessible.name: root.displayName
|
||||
Accessible.onPressAction: clicked()
|
||||
|
||||
onClicked: {
|
||||
if (root.openOnClick) {
|
||||
RoomManager.resolveResource(currentRoom.id);
|
||||
pageStack.currentIndex = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onSpacePressed: clicked()
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
|
||||
onTapped: (eventPoint, button) => root.createRoomListContextMenu()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: root.createRoomListContextMenu()
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
AvatarNotification {
|
||||
source: root.avatar
|
||||
name: root.displayName
|
||||
visible: NeoChatConfig.showAvatarInRoomDrawer
|
||||
implicitHeight: Kirigami.Units.gridUnit + (NeoChatConfig.compactRoomList ? 0 : Kirigami.Units.largeSpacing * 2)
|
||||
implicitWidth: visible ? implicitHeight : 0
|
||||
|
||||
notificationCount: root.contextNotificationCount
|
||||
notificationHighlight: root.hasHighlightNotifications
|
||||
showNotificationLabel: root.hasNotifications && root.collapsed
|
||||
asynchronous: true
|
||||
|
||||
Layout.preferredWidth: height
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !root.collapsed
|
||||
|
||||
QQC2.Label {
|
||||
id: label
|
||||
|
||||
text: root.displayName
|
||||
elide: Text.ElideRight
|
||||
font.weight: root.hasNotifications ? Font.Bold : Font.Normal
|
||||
textFormat: Text.PlainText
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: subtitle.visible ? Qt.AlignLeft | Qt.AlignBottom : Qt.AlignLeft | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: subtitle
|
||||
|
||||
text: root.subtitleText
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: root.hasNotifications ? 0.9 : 0.7
|
||||
visible: !NeoChatConfig.compactRoomList && text.length > 0
|
||||
textFormat: Text.PlainText
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: visible ? Qt.AlignLeft | Qt.AlignTop : Qt.AlignLeft | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "notifications-disabled"
|
||||
enabled: false
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible && !root.collapsed
|
||||
Accessible.name: i18n("Muted room")
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: notificationCountLabel
|
||||
|
||||
text: root.contextNotificationCount
|
||||
visible: root.hasNotifications && !root.collapsed
|
||||
color: Kirigami.Theme.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
background: Rectangle {
|
||||
visible: root.hasNotifications
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
Kirigami.Theme.inherit: false
|
||||
color: root.hasHighlightNotifications > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.minimumWidth: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
|
||||
|
||||
TextMetrics {
|
||||
id: notificationCountTextMetrics
|
||||
text: notificationCountLabel.text
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: configButton
|
||||
visible: root.hovered && !Kirigami.Settings.isMobile && !NeoChatConfig.compactRoomList && !root.collapsed && root.showConfigure
|
||||
text: i18n("Configure room")
|
||||
display: QQC2.Button.IconOnly
|
||||
|
||||
icon.name: "overflow-menu-symbolic"
|
||||
onClicked: createRoomListContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
function createRoomListContextMenu(): void {
|
||||
const component = Qt.createComponent('org.kde.neochat', 'RoomContextMenu');
|
||||
if (component.status === Component.Error) {
|
||||
console.error(component.errorString());
|
||||
}
|
||||
const menu = component.createObject(root.ListView.view ?? root.treeView, {
|
||||
room: root.currentRoom,
|
||||
connection: root.connection
|
||||
});
|
||||
if (!Kirigami.Settings.isMobile && !NeoChatConfig.compactRoomList) {
|
||||
configButton.visible = true;
|
||||
configButton.down = true;
|
||||
}
|
||||
menu.closed.connect(function () {
|
||||
configButton.down = undefined;
|
||||
configButton.visible = Qt.binding(() => {
|
||||
return root.hovered && !Kirigami.Settings.isMobile && !NeoChatConfig.compactRoomList;
|
||||
});
|
||||
});
|
||||
menu.popup();
|
||||
}
|
||||
}
|
||||
332
src/rooms/RoomListPage.qml
Normal file
332
src/rooms/RoomListPage.qml
Normal file
@@ -0,0 +1,332 @@
|
||||
// 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 QtQml.Models
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current width of the room list.
|
||||
*
|
||||
* @note Other objects can access the value but the private function makes sure
|
||||
* that only the internal members can modify it.
|
||||
*/
|
||||
readonly property int currentWidth: _private.currentWidth + spaceDrawer.width + 1
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
readonly property bool collapsed: NeoChatConfig.collapsed
|
||||
|
||||
signal search
|
||||
|
||||
onCurrentWidthChanged: pageStack.defaultColumnWidth = root.currentWidth
|
||||
Component.onCompleted: pageStack.defaultColumnWidth = root.currentWidth
|
||||
|
||||
|
||||
onCollapsedChanged: {
|
||||
if (collapsed) {
|
||||
RoomManager.sortFilterRoomTreeModel.filterText = "";
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextRoomFiltered(condition) {
|
||||
let index = treeView.rowAtIndex(RoomManager.sortFilterRoomTreeModel.currentRoomIndex());
|
||||
while (index++ < treeView.rows) {
|
||||
let item = treeView.itemAtIndex(treeView.index(index, 0))
|
||||
if (condition(item)) {
|
||||
RoomManager.resolveResource(item.currentRoom.id)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goToPreviousRoomFiltered(condition) {
|
||||
let index = treeView.rowAtIndex(RoomManager.sortFilterRoomTreeModel.currentRoomIndex());
|
||||
while (index-- > 0) {
|
||||
let item = treeView.itemAtIndex(treeView.index(index, 0))
|
||||
if (condition(item)) {
|
||||
RoomManager.resolveResource(item.currentRoom.id)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextRoom() {
|
||||
goToNextRoomFiltered(item => (item && item instanceof RoomDelegate));
|
||||
}
|
||||
|
||||
function goToPreviousRoom() {
|
||||
goToPreviousRoomFiltered(item => (item && item instanceof RoomDelegate));
|
||||
}
|
||||
|
||||
function goToNextUnreadRoom() {
|
||||
goToNextRoomFiltered(item => (item && item instanceof RoomDelegate && item.hasUnread));
|
||||
}
|
||||
|
||||
function goToPreviousUnreadRoom() {
|
||||
goToPreviousRoomFiltered(item => (item && item instanceof RoomDelegate && item.hasUnread));
|
||||
}
|
||||
|
||||
titleDelegate: Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Kirigami.Settings.isMobile ? userInfo : exploreComponent
|
||||
}
|
||||
|
||||
padding: 0
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentSpaceChanged() {
|
||||
treeView.expandRecursively();
|
||||
}
|
||||
|
||||
function onCurrentRoomChanged() {
|
||||
treeView.positionViewAtIndex(RoomManager.sortFilterRoomTreeModel.currentRoomIndex(), TableView.AlignVCenter)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
SpaceDrawer {
|
||||
id: spaceDrawer
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
|
||||
Layout.fillHeight: true
|
||||
|
||||
connection: root.connection
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 1
|
||||
}
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: scrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
}
|
||||
|
||||
Keys.onDownPressed: ; // Do not delete 🫠
|
||||
Keys.onUpPressed: ; // These make sure the scrollview doesn't also scroll while going through the roomlist using the arrow keys
|
||||
|
||||
contentItem: TreeView {
|
||||
id: treeView
|
||||
topMargin: Math.round(Kirigami.Units.smallSpacing / 2)
|
||||
|
||||
clip: true
|
||||
reuseItems: false
|
||||
|
||||
model: RoomManager.sortFilterRoomTreeModel
|
||||
|
||||
selectionModel: ItemSelectionModel {}
|
||||
|
||||
delegate: DelegateChooser {
|
||||
role: "delegateType"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "section"
|
||||
delegate: RoomTreeSection {
|
||||
collapsed: root.collapsed
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "normal"
|
||||
delegate: RoomDelegate {
|
||||
id: roomDelegate
|
||||
required property int row
|
||||
required property TreeView treeView
|
||||
required property bool current
|
||||
onCurrentChanged: if (current) {
|
||||
forceActiveFocus(Qt.TabFocusReason);
|
||||
}
|
||||
|
||||
implicitWidth: treeView.width
|
||||
connection: root.connection
|
||||
collapsed: root.collapsed
|
||||
highlighted: RoomManager.currentRoom === currentRoom
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "addDirect"
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
text: i18n("Find your friends")
|
||||
icon.name: "list-add-user"
|
||||
icon.width: Kirigami.Units.gridUnit * 2
|
||||
icon.height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
onClicked: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: (spaceDrawer.width + 1) / 2
|
||||
width: scrollView.width - Kirigami.Units.largeSpacing * 4
|
||||
visible: treeView.rows == 0
|
||||
text: if (RoomManager.sortFilterRoomTreeModel.filterText.length > 0) {
|
||||
return spaceDrawer.showDirectChats ? i18n("No friends found") : i18n("No rooms found");
|
||||
} else {
|
||||
return spaceDrawer.showDirectChats ? i18n("You haven't added any of your friends yet, click below to search for them.") : i18n("Join some rooms to get started");
|
||||
}
|
||||
helpfulAction: spaceDrawer.showDirectChats ? userSearchAction : exploreRoomAction
|
||||
|
||||
Kirigami.Action {
|
||||
id: exploreRoomAction
|
||||
icon.name: RoomManager.sortFilterRoomTreeModel.filterText.length > 0 ? "search" : "list-add"
|
||||
text: RoomManager.sortFilterRoomTreeModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
|
||||
onTriggered: {
|
||||
let dialog = pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection,
|
||||
keyword: RoomManager.sortFilterRoomTreeModel.filterText
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
id: userSearchAction
|
||||
icon.name: RoomManager.sortFilterRoomTreeModel.filterText.length > 0 ? "search" : "list-add"
|
||||
text: RoomManager.sortFilterRoomTreeModel.filterText.length > 0 ? i18n("Search in friend directory") : i18n("Find your friends")
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
footer: Loader {
|
||||
width: parent.width
|
||||
sourceComponent: Kirigami.Settings.isMobile ? exploreComponentMobile : userInfoDesktop
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
parent: applicationWindow().overlay.parent
|
||||
|
||||
x: root.currentWidth - width / 2
|
||||
width: Kirigami.Units.smallSpacing * 2
|
||||
z: root.z + 1
|
||||
enabled: RoomManager.hasOpenRoom && applicationWindow().width >= Kirigami.Units.gridUnit * 35
|
||||
visible: enabled
|
||||
cursorShape: Qt.SplitHCursor
|
||||
|
||||
property int _lastX
|
||||
|
||||
onPressed: mouse => {
|
||||
_lastX = mouse.x;
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (_lastX == -1) {
|
||||
return;
|
||||
}
|
||||
if (mouse.x > _lastX) {
|
||||
// we moved to the right
|
||||
if (_private.currentWidth < _private.collapseWidth && _private.currentWidth + (mouse.x - _lastX) >= _private.collapseWidth) {
|
||||
// Here we get back directly to a more wide mode.
|
||||
_private.currentWidth = _private.defaultWidth;
|
||||
NeoChatConfig.collapsed = false;
|
||||
} else if (_private.currentWidth >= _private.collapseWidth) {
|
||||
// Increase page width
|
||||
_private.currentWidth = Math.min(_private.defaultWidth, _private.currentWidth + (mouse.x - _lastX));
|
||||
}
|
||||
} else if (mouse.x < _lastX) {
|
||||
const tmpWidth = _private.currentWidth - (_lastX - mouse.x);
|
||||
if (tmpWidth < _private.collapseWidth) {
|
||||
_private.currentWidth = Qt.binding(() => _private.collapsedSize);
|
||||
NeoChatConfig.collapsed = true;
|
||||
} else {
|
||||
_private.currentWidth = tmpWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userInfo
|
||||
UserInfo {
|
||||
bottomEdge: false
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userInfoDesktop
|
||||
UserInfoDesktop {
|
||||
connection: root.connection
|
||||
collapsed: root.collapsed
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: exploreComponent
|
||||
ExploreComponent {
|
||||
desiredWidth: root.width - Kirigami.Units.largeSpacing
|
||||
collapsed: root.collapsed
|
||||
connection: root.connection
|
||||
|
||||
onSearch: root.search()
|
||||
|
||||
onTextChanged: newText => {
|
||||
RoomManager.sortFilterRoomTreeModel.filterText = newText;
|
||||
treeView.expandRecursively();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: exploreComponentMobile
|
||||
ExploreComponentMobile {
|
||||
connection: root.connection
|
||||
|
||||
onTextChanged: newText => {
|
||||
RoomManager.sortFilterRoomTreeModel.filterText = newText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Hold the modifiable currentWidth in a private object so that only internal
|
||||
* members can modify it.
|
||||
*/
|
||||
QtObject {
|
||||
id: _private
|
||||
property int currentWidth: NeoChatConfig.collapsed ? collapsedSize : defaultWidth
|
||||
readonly property int defaultWidth: Kirigami.Units.gridUnit * 15
|
||||
readonly property int collapseWidth: Kirigami.Units.gridUnit * 10
|
||||
readonly property int collapsedSize: Kirigami.Units.gridUnit + (NeoChatConfig.compactRoomList ? 0 : Kirigami.Units.largeSpacing * 2) + Kirigami.Units.largeSpacing * 2 + (scrollView.QQC2.ScrollBar.vertical.visible ? scrollView.QQC2.ScrollBar.vertical.width : 0)
|
||||
}
|
||||
}
|
||||
83
src/rooms/RoomTreeSection.qml
Normal file
83
src/rooms/RoomTreeSection.qml
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
id: root
|
||||
|
||||
required property TreeView treeView
|
||||
required property bool isTreeNode
|
||||
required property bool expanded
|
||||
required property bool hasChildren
|
||||
required property int depth
|
||||
required property string displayName
|
||||
required property int row
|
||||
required property bool current
|
||||
onCurrentChanged: if (current) {
|
||||
collapseButton.forceActiveFocus(Qt.TabFocusReason);
|
||||
}
|
||||
required property bool selected
|
||||
|
||||
property bool collapsed: false
|
||||
|
||||
implicitWidth: treeView.width
|
||||
|
||||
hoverEnabled: false
|
||||
activeFocusOnTab: false
|
||||
background: null
|
||||
|
||||
onClicked: root.treeView.toggleExpanded(row)
|
||||
|
||||
Keys.onEnterPressed: root.treeView.toggleExpanded(row)
|
||||
Keys.onReturnPressed: root.treeView.toggleExpanded(row)
|
||||
Keys.onSpacePressed: root.treeView.toggleExpanded(row)
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !root.collapsed
|
||||
|
||||
opacity: 0.7
|
||||
level: 5
|
||||
type: Kirigami.Heading.Primary
|
||||
text: root.collapsed ? "" : model.displayName
|
||||
elide: Text.ElideRight
|
||||
|
||||
// we override the Primary type's font weight (DemiBold) for Bold for contrast with small text
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.collapsed
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: collapseButton
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
icon {
|
||||
name: root.expanded ? "go-up" : "go-down"
|
||||
width: Kirigami.Units.iconSizes.small
|
||||
height: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
text: root.expanded ? i18nc("Collapse <section name>", "Collapse %1", root.displayName) : i18nc("Expand <section name", "Expand %1", root.displayName)
|
||||
display: QQC2.Button.IconOnly
|
||||
|
||||
activeFocusOnTab: false
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onClicked: root.treeView.toggleExpanded(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
320
src/rooms/SpaceDrawer.qml
Normal file
320
src/rooms/SpaceDrawer.qml
Normal file
@@ -0,0 +1,320 @@
|
||||
// SPDX-FileCopyrightText: 2020-2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
|
||||
// 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.neochat
|
||||
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
readonly property real pinnedWidth: Kirigami.Units.gridUnit * 6
|
||||
required property NeoChatConnection connection
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
onActiveFocusChanged: if (activeFocus) {
|
||||
notificationsButton.forceActiveFocus();
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: scrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
QQC2.ScrollBar.vertical.policy: QQC2.ScrollBar.AlwaysOff
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
contentWidth: -1 // disable horizontal scroll
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
width: scrollView.width
|
||||
spacing: 0
|
||||
|
||||
AvatarTabButton {
|
||||
id: notificationsButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing / 2
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing / 2
|
||||
text: i18n("View notifications")
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "notifications"
|
||||
}
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
onSelected: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'NotificationsView'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Notifications"),
|
||||
modality: Qt.NonModal
|
||||
})
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
AvatarTabButton {
|
||||
id: allRoomButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing / 2
|
||||
|
||||
text: i18n("Home")
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "user-home-symbolic"
|
||||
|
||||
QQC2.Label {
|
||||
id: homeNotificationCountLabel
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: -Kirigami.Units.smallSpacing
|
||||
anchors.rightMargin: -Kirigami.Units.smallSpacing
|
||||
z: 1
|
||||
width: Math.max(homeNotificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
|
||||
height: Kirigami.Units.iconSizes.smallMedium
|
||||
|
||||
text: root.connection.homeNotifications > 0 ? root.connection.homeNotifications : ""
|
||||
visible: root.connection.homeNotifications > 0 && (RoomManager.currentSpace.length > 0 || root.showDirectChats === true)
|
||||
color: Kirigami.Theme.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
background: Rectangle {
|
||||
visible: true
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
Kirigami.Theme.inherit: false
|
||||
color: root.connection.homeHaveHighlightNotifications ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: homeNotificationCountTextMetrics
|
||||
text: homeNotificationCountLabel.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
checked: RoomManager.currentSpace.length === 0
|
||||
onSelected: RoomManager.currentSpace = ""
|
||||
}
|
||||
AvatarTabButton {
|
||||
id: directChatButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing / 2
|
||||
|
||||
text: i18nc("@button View all one-on-one chats with your friends.", "Friends")
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "system-users-symbolic"
|
||||
|
||||
QQC2.Label {
|
||||
id: directChatNotificationCountLabel
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: -Kirigami.Units.smallSpacing
|
||||
anchors.rightMargin: -Kirigami.Units.smallSpacing
|
||||
z: 1
|
||||
width: Math.max(directChatNotificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
|
||||
height: Kirigami.Units.iconSizes.smallMedium
|
||||
|
||||
text: root.connection.directChatNotifications > 0 ? root.connection.directChatNotifications : ""
|
||||
visible: (root.connection.directChatNotifications > 0 || root.connection.directChatInvites) && RoomManager.currentSpace !== "DM"
|
||||
color: Kirigami.Theme.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
background: Rectangle {
|
||||
visible: true
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
Kirigami.Theme.inherit: false
|
||||
color: root.connection.directChatsHaveHighlightNotifications || root.connection.directChatInvites ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: directChatNotificationCountTextMetrics
|
||||
text: directChatNotificationCountLabel.text
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.fill: parent
|
||||
|
||||
source: "list-add-symbolic"
|
||||
visible: root.connection.directChatInvites && root.connection.directChatNotifications === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
checked: RoomManager.currentSpace === "DM"
|
||||
onSelected: RoomManager.currentSpace = "DM"
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: RoomManager.sortFilterSpaceListModel
|
||||
|
||||
delegate: AvatarTabButton {
|
||||
id: spaceDelegate
|
||||
|
||||
required property string displayName
|
||||
required property url avatar
|
||||
required property string roomId
|
||||
required property var currentRoom
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
|
||||
text: displayName
|
||||
source: avatar
|
||||
|
||||
notificationCount: spaceDelegate.currentRoom.childrenNotificationCount
|
||||
notificationHighlight: spaceDelegate.currentRoom.childrenHaveHighlightNotifications
|
||||
showNotificationLabel: spaceDelegate.currentRoom.childrenNotificationCount > 0 && RoomManager.currentSpace != spaceDelegate.roomId
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
onSelected: {
|
||||
RoomManager.currentSpace = spaceDelegate.roomId;
|
||||
}
|
||||
checked: RoomManager.currentSpace === roomId
|
||||
onContextMenuRequested: root.createContextMenu(currentRoom)
|
||||
}
|
||||
}
|
||||
|
||||
AvatarTabButton {
|
||||
id: recommendedSpaceButton
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
visible: SpaceHierarchyCache.recommendedSpaceId.length > 0 && !root.connection.room(SpaceHierarchyCache.recommendedSpaceId) && !SpaceHierarchyCache.recommendedSpaceHidden
|
||||
|
||||
text: i18nc("Join <name of a space>", "Join %1", SpaceHierarchyCache.recommendedSpaceDisplayName)
|
||||
source: SpaceHierarchyCache.recommendedSpaceAvatar.toString().length > 0 ? root.connection.makeMediaUrl(SpaceHierarchyCache.recommendedSpaceAvatar) : ""
|
||||
onSelected: {
|
||||
recommendedSpaceDialogComponent.createObject(QQC2.Overlay.overlay, {
|
||||
connection: root.connection
|
||||
}).open();
|
||||
}
|
||||
Component {
|
||||
id: recommendedSpaceDialogComponent
|
||||
RecommendedSpaceDialog {}
|
||||
}
|
||||
Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
width: Kirigami.Units.gridUnit * 1.5
|
||||
height: width
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Kirigami.Units.smallSpacing
|
||||
anchors.rightMargin: Kirigami.Units.smallSpacing * 2
|
||||
anchors.right: parent.right
|
||||
radius: width / 2
|
||||
z: parent.z + 1
|
||||
Kirigami.Icon {
|
||||
anchors.fill: parent
|
||||
z: parent + 1
|
||||
source: "list-add"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing / 2
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing / 2
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
AvatarTabButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
|
||||
text: i18n("Create a space")
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "list-add"
|
||||
}
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
onSelected: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
connection: root.connection,
|
||||
isSpace: true,
|
||||
title: i18nc("@title", "Create a Space")
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Space")
|
||||
})
|
||||
}
|
||||
|
||||
AvatarTabButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
|
||||
text: i18nc("@action:button", "Explore rooms")
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "compass"
|
||||
}
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
onSelected: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection,
|
||||
keyword: RoomManager.sortFilterRoomTreeModel.filterText
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createContextMenu(room: NeoChatRoom): void {
|
||||
let context = spaceListContextMenu.createObject(root, {
|
||||
room: room,
|
||||
connection: root.connection
|
||||
});
|
||||
context.popup();
|
||||
}
|
||||
Component {
|
||||
id: spaceListContextMenu
|
||||
SpaceListContextMenu {
|
||||
window: root.QQC2.ApplicationWindow.window
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/rooms/SpaceListContextMenu.qml
Normal file
77
src/rooms/SpaceListContextMenu.qml
Normal file
@@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
|
||||
/**
|
||||
* Context menu when clicking on a room in the room list
|
||||
*/
|
||||
KirigamiComponents.ConvergentContextMenu {
|
||||
id: root
|
||||
|
||||
property NeoChatRoom room
|
||||
required property NeoChatConnection connection
|
||||
required property Kirigami.ApplicationWindow window
|
||||
|
||||
headerContentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: avatar
|
||||
source: room.avatarMediaUrl
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
Layout.fillWidth: true
|
||||
text: room.displayName
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("'Space' is a matrix space", "View Space")
|
||||
icon.name: "view-list-details"
|
||||
onTriggered: RoomManager.resolveResource(room.id)
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("@action:inmenu", "Copy Address to Clipboard")
|
||||
icon.name: "edit-copy"
|
||||
onTriggered: if (room.canonicalAlias.length === 0) {
|
||||
Clipboard.saveText(room.id);
|
||||
} else {
|
||||
Clipboard.saveText(room.canonicalAlias);
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("'Space' is a matrix space", "Space Settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
onTriggered: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Space);
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
separator: true
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("'Space' is a matrix space", "Leave Space")
|
||||
icon.name: "go-previous"
|
||||
onTriggered: RoomManager.leaveRoom(room)
|
||||
}
|
||||
}
|
||||
129
src/rooms/UserInfo.qml
Normal file
129
src/rooms/UserInfo.qml
Normal file
@@ -0,0 +1,129 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
property bool collapsed: false
|
||||
|
||||
property bool bottomEdge: true
|
||||
|
||||
property var addAccount
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.minimumHeight: bottomEdge ? Kirigami.Units.gridUnit * 3 : -1
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
accountsPopup.close();
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: accountButton
|
||||
|
||||
down: accountMenu.opened || pressed
|
||||
|
||||
onClicked: accountMenu.popup()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: i18nc("@info:tooltip", "Manage Account")
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
readonly property url avatarUrl: root.connection.localUser.avatarUrl
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
|
||||
source: avatarUrl.toString().length > 0 ? root.connection.makeMediaUrl(avatarUrl) : ""
|
||||
name: root.connection.localUser.displayName
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Math.round(root.width * 0.55)
|
||||
visible: !root.collapsed
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
id: displayNameLabel
|
||||
Layout.fillWidth: true
|
||||
text: root.connection.localUser.displayName
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
id: idLabel
|
||||
Layout.fillWidth: true
|
||||
text: (root.connection.label.length > 0 ? (root.connection.label + " ") : "") + root.connection.localUser.id
|
||||
font.pointSize: displayNameLabel.font.pointSize * 0.8
|
||||
opacity: 0.7
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccountMenu {
|
||||
id: accountMenu
|
||||
connection: root.connection
|
||||
window: QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.ActionToolBar {
|
||||
alignment: Qt.AlignRight
|
||||
display: QQC2.Button.IconOnly
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: maximumContentWidth
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18n("Switch User")
|
||||
icon.name: "system-switch-user"
|
||||
shortcut: "Ctrl+U"
|
||||
onTriggered: accountSwitchDialog.createObject(QQC2.Overlay.overlay, {
|
||||
connection: root.connection
|
||||
}).open();
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Open Settings")
|
||||
icon.name: "settings-configure-symbolic"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Component {
|
||||
id: accountSwitchDialog
|
||||
AccountSwitchDialog {}
|
||||
}
|
||||
}
|
||||
41
src/rooms/UserInfoDesktop.qml
Normal file
41
src/rooms/UserInfoDesktop.qml
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2023 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.neochat
|
||||
|
||||
QQC2.ToolBar {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The connection for the current user.
|
||||
*/
|
||||
required property NeoChatConnection connection
|
||||
|
||||
property bool collapsed: false
|
||||
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
UserInfo {
|
||||
collapsed: root.collapsed
|
||||
bottomEdge: true
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
}
|
||||
332
src/rooms/models/publicroomlistmodel.cpp
Normal file
332
src/rooms/models/publicroomlistmodel.cpp
Normal file
@@ -0,0 +1,332 @@
|
||||
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "publicroomlistmodel.h"
|
||||
|
||||
#include "neochatconnection.h"
|
||||
#include "publicroomlist_logging.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class NeoChatQueryPublicRoomsJob : public QueryPublicRoomsJob
|
||||
{
|
||||
public:
|
||||
explicit NeoChatQueryPublicRoomsJob(const QString &server = {},
|
||||
std::optional<int> limit = std::nullopt,
|
||||
const QString &since = {},
|
||||
const std::optional<Filter> &filter = std::nullopt,
|
||||
std::optional<bool> includeAllNetworks = std::nullopt,
|
||||
const QString &thirdPartyInstanceId = {})
|
||||
: QueryPublicRoomsJob(server, limit, since, filter, includeAllNetworks, thirdPartyInstanceId)
|
||||
{
|
||||
// TODO Remove once we can use libQuotient's job directly
|
||||
// This is to make libQuotient happy about results not having the "chunk" field
|
||||
setExpectedKeys({});
|
||||
}
|
||||
};
|
||||
|
||||
PublicRoomListModel::PublicRoomListModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
NeoChatConnection *PublicRoomListModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection == connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
|
||||
nextBatch = QString();
|
||||
attempted = false;
|
||||
rooms.clear();
|
||||
m_server.clear();
|
||||
|
||||
if (m_connection) {
|
||||
m_connection->disconnect(this);
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
|
||||
m_connection = connection;
|
||||
|
||||
if (job) {
|
||||
job->abandon();
|
||||
job = nullptr;
|
||||
Q_EMIT searchingChanged();
|
||||
}
|
||||
|
||||
if (m_connection) {
|
||||
next();
|
||||
}
|
||||
|
||||
Q_EMIT connectionChanged();
|
||||
Q_EMIT serverChanged();
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::server() const
|
||||
{
|
||||
return m_server;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setServer(const QString &value)
|
||||
{
|
||||
if (m_server == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_server = value;
|
||||
|
||||
beginResetModel();
|
||||
|
||||
nextBatch = QString();
|
||||
attempted = false;
|
||||
rooms.clear();
|
||||
|
||||
endResetModel();
|
||||
|
||||
if (job) {
|
||||
job->abandon();
|
||||
job = nullptr;
|
||||
Q_EMIT searchingChanged();
|
||||
}
|
||||
|
||||
if (m_connection) {
|
||||
next();
|
||||
}
|
||||
|
||||
Q_EMIT serverChanged();
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::searchText() const
|
||||
{
|
||||
return m_searchText;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setSearchText(const QString &value)
|
||||
{
|
||||
if (m_searchText == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_searchText = value;
|
||||
Q_EMIT searchTextChanged();
|
||||
|
||||
nextBatch = QString();
|
||||
attempted = false;
|
||||
|
||||
if (job) {
|
||||
job->abandon();
|
||||
job = nullptr;
|
||||
Q_EMIT searchingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool PublicRoomListModel::showOnlySpaces() const
|
||||
{
|
||||
return m_showOnlySpaces;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setShowOnlySpaces(bool showOnlySpaces)
|
||||
{
|
||||
if (showOnlySpaces == m_showOnlySpaces) {
|
||||
return;
|
||||
}
|
||||
m_showOnlySpaces = showOnlySpaces;
|
||||
Q_EMIT showOnlySpacesChanged();
|
||||
|
||||
nextBatch = QString();
|
||||
attempted = false;
|
||||
|
||||
if (job) {
|
||||
job->abandon();
|
||||
job = nullptr;
|
||||
Q_EMIT searchingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void PublicRoomListModel::search(int limit)
|
||||
{
|
||||
if (limit < 1 || attempted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (job) {
|
||||
qCDebug(PublicRoomList) << "Other job running, ignore";
|
||||
return;
|
||||
}
|
||||
|
||||
next(limit);
|
||||
}
|
||||
|
||||
void PublicRoomListModel::next(int limit)
|
||||
{
|
||||
if (m_connection == nullptr || limit < 1) {
|
||||
return;
|
||||
}
|
||||
m_redirectedText.clear();
|
||||
Q_EMIT redirectedChanged();
|
||||
|
||||
if (job) {
|
||||
qCDebug(PublicRoomList) << "Other job running, ignore";
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList roomTypes;
|
||||
if (m_showOnlySpaces) {
|
||||
roomTypes += u"m.space"_s;
|
||||
}
|
||||
job = m_connection->callApi<NeoChatQueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
|
||||
Q_EMIT searchingChanged();
|
||||
|
||||
connect(job, &BaseJob::finished, this, [this] {
|
||||
if (!attempted) {
|
||||
beginResetModel();
|
||||
rooms.clear();
|
||||
endResetModel();
|
||||
|
||||
attempted = true;
|
||||
}
|
||||
|
||||
if (job->status() == BaseJob::Success) {
|
||||
nextBatch = job->nextBatch();
|
||||
|
||||
this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
|
||||
rooms.append(job->chunk());
|
||||
this->endInsertRows();
|
||||
} else if (job->error() == BaseJob::ContentAccessError) {
|
||||
m_redirectedText = job->jsonData()[u"error"_s].toString();
|
||||
Q_EMIT redirectedChanged();
|
||||
}
|
||||
|
||||
this->job = nullptr;
|
||||
Q_EMIT searchingChanged();
|
||||
});
|
||||
}
|
||||
|
||||
QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (index.row() >= rooms.count()) {
|
||||
qCDebug(PublicRoomList) << "something's wrong: index.row() >= rooms.count()";
|
||||
return {};
|
||||
}
|
||||
auto room = rooms.at(index.row());
|
||||
if (role == DisplayNameRole) {
|
||||
auto displayName = room.name;
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
displayName = room.canonicalAlias;
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return room.roomId;
|
||||
}
|
||||
if (role == AvatarUrlRole) {
|
||||
auto avatarUrl = room.avatarUrl;
|
||||
if (avatarUrl.isEmpty() || !m_connection) {
|
||||
return QUrl();
|
||||
}
|
||||
return m_connection->makeMediaUrl(avatarUrl);
|
||||
}
|
||||
if (role == TopicRole) {
|
||||
return room.topic;
|
||||
}
|
||||
if (role == RoomIdRole) {
|
||||
return room.roomId;
|
||||
}
|
||||
if (role == AliasRole) {
|
||||
if (!room.canonicalAlias.isEmpty()) {
|
||||
return room.canonicalAlias;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (role == MemberCountRole) {
|
||||
return room.numJoinedMembers;
|
||||
}
|
||||
if (role == AllowGuestsRole) {
|
||||
return room.guestCanJoin;
|
||||
}
|
||||
if (role == WorldReadableRole) {
|
||||
return room.worldReadable;
|
||||
}
|
||||
if (role == IsJoinedRole) {
|
||||
if (!m_connection) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return m_connection->room(room.roomId, JoinState::Join) != nullptr;
|
||||
}
|
||||
if (role == IsSpaceRole) {
|
||||
return room.roomType == u"m.space"_s;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> PublicRoomListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[DisplayNameRole] = "displayName";
|
||||
roles[AvatarUrlRole] = "avatarUrl";
|
||||
roles[TopicRole] = "topic";
|
||||
roles[RoomIdRole] = "roomId";
|
||||
roles[MemberCountRole] = "memberCount";
|
||||
roles[AllowGuestsRole] = "allowGuests";
|
||||
roles[WorldReadableRole] = "worldReadable";
|
||||
roles[IsJoinedRole] = "isJoined";
|
||||
roles[IsSpaceRole] = "isSpace";
|
||||
roles[AliasRole] = "alias";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
int PublicRoomListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return rooms.count();
|
||||
}
|
||||
|
||||
bool PublicRoomListModel::canFetchMore(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return !nextBatch.isEmpty();
|
||||
}
|
||||
|
||||
void PublicRoomListModel::fetchMore(const QModelIndex &parent)
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
next();
|
||||
}
|
||||
|
||||
bool PublicRoomListModel::searching() const
|
||||
{
|
||||
return job != nullptr;
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::redirectedText() const
|
||||
{
|
||||
return m_redirectedText;
|
||||
}
|
||||
|
||||
#include "moc_publicroomlistmodel.cpp"
|
||||
154
src/rooms/models/publicroomlistmodel.h
Normal file
154
src/rooms/models/publicroomlistmodel.h
Normal file
@@ -0,0 +1,154 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <Quotient/csapi/list_public_rooms.h>
|
||||
|
||||
class NeoChatConnection;
|
||||
|
||||
/**
|
||||
* @class PublicRoomListModel
|
||||
*
|
||||
* This class defines the model for visualising a list of public rooms.
|
||||
*
|
||||
* The model finds the public rooms visible to the given server (which doesn't have
|
||||
* to be the user's home server) and can also apply a filter if desired.
|
||||
*
|
||||
* Due to the fact that the public room list could be huge the model is lazily loaded
|
||||
* and requires that the next batch of rooms be manually called.
|
||||
*/
|
||||
class PublicRoomListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is getting its rooms from.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The server to get the public room list from.
|
||||
*/
|
||||
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
|
||||
|
||||
/**
|
||||
* @brief The text to search the public room list for.
|
||||
*/
|
||||
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether only space rooms should be shown.
|
||||
*/
|
||||
Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the model is searching.
|
||||
*/
|
||||
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
|
||||
|
||||
/**
|
||||
* @brief The text returned by the server after redirection
|
||||
*/
|
||||
Q_PROPERTY(QString redirectedText READ redirectedText NOTIFY redirectedChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
DisplayNameRole = Qt::DisplayRole + 1, /**< The name of the room. */
|
||||
AvatarUrlRole, /**< The source URL for the room's avatar. */
|
||||
TopicRole, /**< The room topic. */
|
||||
RoomIdRole, /**< The room matrix ID. */
|
||||
AliasRole, /**< The room canonical alias. */
|
||||
MemberCountRole, /**< The number of members in the room. */
|
||||
AllowGuestsRole, /**< Whether the room allows guest users. */
|
||||
WorldReadableRole, /**< Whether the room events can be seen by non-members. */
|
||||
IsJoinedRole, /**< Whether the local user has joined the room. */
|
||||
IsSpaceRole, /**< Whether the room is a space. */
|
||||
};
|
||||
|
||||
explicit PublicRoomListModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = DisplayNameRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
[[nodiscard]] NeoChatConnection *connection() const;
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
|
||||
[[nodiscard]] QString server() const;
|
||||
void setServer(const QString &value);
|
||||
|
||||
[[nodiscard]] QString searchText() const;
|
||||
void setSearchText(const QString &searchText);
|
||||
|
||||
[[nodiscard]] bool showOnlySpaces() const;
|
||||
void setShowOnlySpaces(bool showOnlySpaces);
|
||||
|
||||
[[nodiscard]] bool searching() const;
|
||||
|
||||
/**
|
||||
* @brief Search the room directory.
|
||||
*
|
||||
* @param limit the maximum number of rooms to load.
|
||||
*/
|
||||
Q_INVOKABLE void search(int limit = 50);
|
||||
|
||||
QString redirectedText() const;
|
||||
|
||||
private:
|
||||
QPointer<NeoChatConnection> m_connection = nullptr;
|
||||
QString m_server;
|
||||
QString m_searchText;
|
||||
bool m_showOnlySpaces = false;
|
||||
|
||||
/**
|
||||
* @brief Load the next set of rooms.
|
||||
*
|
||||
* @param limit the maximum number of rooms to load.
|
||||
*/
|
||||
void next(int limit = 50);
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
|
||||
bool attempted = false;
|
||||
bool m_searching = false;
|
||||
QString nextBatch;
|
||||
|
||||
QList<Quotient::PublicRoomsChunk> rooms;
|
||||
|
||||
Quotient::QueryPublicRoomsJob *job = nullptr;
|
||||
QString m_redirectedText;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void serverChanged();
|
||||
void searchTextChanged();
|
||||
void showOnlySpacesChanged();
|
||||
void searchingChanged();
|
||||
void redirectedChanged();
|
||||
};
|
||||
100
src/rooms/models/roomtreeitem.cpp
Normal file
100
src/rooms/models/roomtreeitem.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// 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
|
||||
|
||||
#include "roomtreeitem.h"
|
||||
|
||||
RoomTreeItem::RoomTreeItem(TreeData data, RoomTreeItem *parent)
|
||||
: m_parentItem(parent)
|
||||
, m_data(data)
|
||||
{
|
||||
}
|
||||
|
||||
bool RoomTreeItem::operator==(const RoomTreeItem &other) const
|
||||
{
|
||||
if (std::holds_alternative<NeoChatRoomType::Types>(m_data) && std::holds_alternative<NeoChatRoomType::Types>(other.data())) {
|
||||
return std::get<NeoChatRoomType::Types>(m_data) == std::get<NeoChatRoomType::Types>(m_data);
|
||||
}
|
||||
if (std::holds_alternative<NeoChatRoom *>(m_data) && std::holds_alternative<NeoChatRoom *>(other.data())) {
|
||||
return std::get<NeoChatRoom *>(m_data)->id() == std::get<NeoChatRoom *>(m_data)->id();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RoomTreeItem *RoomTreeItem::child(int row)
|
||||
{
|
||||
return row >= 0 && row < childCount() ? m_children.at(row).get() : nullptr;
|
||||
}
|
||||
|
||||
int RoomTreeItem::childCount() const
|
||||
{
|
||||
return int(m_children.size());
|
||||
}
|
||||
|
||||
bool RoomTreeItem::insertChild(std::unique_ptr<RoomTreeItem> newChild)
|
||||
{
|
||||
if (newChild == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto it = m_children.begin(), end = m_children.end(); it != end; ++it) {
|
||||
if (*it == newChild) {
|
||||
*it = std::move(newChild);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
m_children.push_back(std::move(newChild));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RoomTreeItem::removeChild(int row)
|
||||
{
|
||||
if (row < 0 || row >= childCount()) {
|
||||
return false;
|
||||
}
|
||||
m_children.erase(m_children.begin() + row);
|
||||
return true;
|
||||
}
|
||||
|
||||
int RoomTreeItem::row() const
|
||||
{
|
||||
if (m_parentItem == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto it = std::find_if(m_parentItem->m_children.cbegin(), m_parentItem->m_children.cend(), [this](const std::unique_ptr<RoomTreeItem> &treeItem) {
|
||||
return treeItem.get() == this;
|
||||
});
|
||||
|
||||
if (it != m_parentItem->m_children.cend()) {
|
||||
return std::distance(m_parentItem->m_children.cbegin(), it);
|
||||
}
|
||||
Q_ASSERT(false); // should not happen
|
||||
return -1;
|
||||
}
|
||||
|
||||
RoomTreeItem *RoomTreeItem::parentItem() const
|
||||
{
|
||||
return m_parentItem;
|
||||
}
|
||||
|
||||
RoomTreeItem::TreeData RoomTreeItem::data() const
|
||||
{
|
||||
return m_data;
|
||||
}
|
||||
|
||||
std::optional<int> RoomTreeItem::rowForRoom(Quotient::Room *room) const
|
||||
{
|
||||
Q_ASSERT_X(std::holds_alternative<NeoChatRoomType::Types>(m_data), __FUNCTION__, "rowForRoom only works items for rooms not categories");
|
||||
|
||||
int i = 0;
|
||||
for (const auto &child : m_children) {
|
||||
if (std::get<NeoChatRoom *>(child->data()) == room) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
78
src/rooms/models/roomtreeitem.h
Normal file
78
src/rooms/models/roomtreeitem.h
Normal file
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// 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
|
||||
|
||||
#include "enums/neochatroomtype.h"
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
* @class RoomTreeItem
|
||||
*
|
||||
* This class defines an item in the space tree hierarchy model.
|
||||
*
|
||||
* @note This is separate from Quotient::Room and NeoChatRoom because we don't have
|
||||
* full room information for any room/space the user hasn't joined and we
|
||||
* don't want to create one for ever possible child in a space as that would
|
||||
* be expensive.
|
||||
*
|
||||
* @sa Quotient::Room, NeoChatRoom
|
||||
*/
|
||||
class RoomTreeItem
|
||||
{
|
||||
public:
|
||||
using TreeData = std::variant<NeoChatRoom *, NeoChatRoomType::Types>;
|
||||
|
||||
explicit RoomTreeItem(TreeData data, RoomTreeItem *parent = nullptr);
|
||||
|
||||
bool operator==(const RoomTreeItem &other) const;
|
||||
|
||||
/**
|
||||
* @brief Return the child at the given row number.
|
||||
*
|
||||
* Nullptr is returned if there is no child at the given row number.
|
||||
*/
|
||||
RoomTreeItem *child(int row);
|
||||
|
||||
/**
|
||||
* @brief The number of children this item has.
|
||||
*/
|
||||
int childCount() const;
|
||||
|
||||
/**
|
||||
* @brief Insert the given child.
|
||||
*/
|
||||
bool insertChild(std::unique_ptr<RoomTreeItem> newChild);
|
||||
|
||||
/**
|
||||
* @brief Remove the child at the given row number.
|
||||
*
|
||||
* @return True if a child was removed, false if the given row isn't valid.
|
||||
*/
|
||||
bool removeChild(int row);
|
||||
|
||||
/**
|
||||
* @brief Return this item's parent.
|
||||
*/
|
||||
RoomTreeItem *parentItem() const;
|
||||
|
||||
/**
|
||||
* @brief Return the row number for this child relative to the parent.
|
||||
*
|
||||
* @return The row value if the child has a parent, 0 otherwise.
|
||||
*/
|
||||
int row() const;
|
||||
|
||||
/**
|
||||
* @brief Return this item's data.
|
||||
*/
|
||||
TreeData data() const;
|
||||
|
||||
std::optional<int> rowForRoom(Quotient::Room *room) const;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<RoomTreeItem>> m_children;
|
||||
RoomTreeItem *m_parentItem;
|
||||
|
||||
TreeData m_data;
|
||||
};
|
||||
428
src/rooms/models/roomtreemodel.cpp
Normal file
428
src/rooms/models/roomtreemodel.cpp
Normal file
@@ -0,0 +1,428 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "roomtreemodel.h"
|
||||
|
||||
#include <Quotient/events/roommemberevent.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include "enums/neochatroomtype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
std::function<bool(const Quotient::RoomEvent *)> RoomTreeModel::m_hiddenFilter = [](const Quotient::RoomEvent *) -> bool {
|
||||
return false;
|
||||
};
|
||||
|
||||
RoomTreeModel::RoomTreeModel(QObject *parent)
|
||||
: QAbstractItemModel(parent)
|
||||
, m_rootItem(new RoomTreeItem(nullptr))
|
||||
{
|
||||
}
|
||||
|
||||
RoomTreeItem *RoomTreeModel::getItem(const QModelIndex &index) const
|
||||
{
|
||||
if (index.isValid()) {
|
||||
RoomTreeItem *item = static_cast<RoomTreeItem *>(index.internalPointer());
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return m_rootItem.get();
|
||||
}
|
||||
|
||||
void RoomTreeModel::resetModel()
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
beginResetModel();
|
||||
m_rootItem.reset();
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_rootItem.reset(new RoomTreeItem(nullptr));
|
||||
|
||||
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
|
||||
m_rootItem->insertChild(std::make_unique<RoomTreeItem>(NeoChatRoomType::Types(i), m_rootItem.get()));
|
||||
}
|
||||
|
||||
for (const auto &r : m_connection->allRooms()) {
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(r);
|
||||
const auto type = NeoChatRoomType::typeForRoom(room);
|
||||
const auto categoryItem = m_rootItem->child(type);
|
||||
if (categoryItem->insertChild(std::make_unique<RoomTreeItem>(room, categoryItem))) {
|
||||
connectRoomSignals(room);
|
||||
}
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void RoomTreeModel::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection == connection) {
|
||||
return;
|
||||
}
|
||||
if (m_connection) {
|
||||
disconnect(m_connection.get(), nullptr, this, nullptr);
|
||||
}
|
||||
m_connection = connection;
|
||||
|
||||
resetModel();
|
||||
|
||||
connect(connection, &Connection::newRoom, this, &RoomTreeModel::newRoom);
|
||||
connect(connection, &Connection::leftRoom, this, &RoomTreeModel::leftRoom);
|
||||
connect(connection, &Connection::aboutToDeleteRoom, this, &RoomTreeModel::leftRoom);
|
||||
|
||||
Q_EMIT connectionChanged();
|
||||
}
|
||||
|
||||
void RoomTreeModel::newRoom(Room *r)
|
||||
{
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(r);
|
||||
const auto type = NeoChatRoomType::typeForRoom(room);
|
||||
// Check if the room is already in the model.
|
||||
const auto checkRoomIndex = indexForRoom(room);
|
||||
if (checkRoomIndex.isValid()) {
|
||||
// If the room is in the wrong type category for whatever reason, move it.
|
||||
if (checkRoomIndex.parent().row() != type) {
|
||||
moveRoom(room);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto parentItem = m_rootItem->child(type);
|
||||
beginInsertRows(index(parentItem->row(), 0), parentItem->childCount(), parentItem->childCount());
|
||||
parentItem->insertChild(std::make_unique<RoomTreeItem>(room, parentItem));
|
||||
connectRoomSignals(room);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void RoomTreeModel::leftRoom(Room *r)
|
||||
{
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(r);
|
||||
auto index = indexForRoom(room);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto parentItem = getItem(index.parent());
|
||||
Q_ASSERT(parentItem);
|
||||
|
||||
beginRemoveRows(index.parent(), index.row(), index.row());
|
||||
parentItem->removeChild(index.row());
|
||||
room->disconnect(this);
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
void RoomTreeModel::moveRoom(Quotient::Room *room)
|
||||
{
|
||||
// We can't assume the type as it has changed so currently the return of
|
||||
// NeoChatRoomType::typeForRoom doesn't match it's current location. So find the room.
|
||||
NeoChatRoomType::Types oldType;
|
||||
int oldRow = -1;
|
||||
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
|
||||
const auto categoryItem = m_rootItem->child(i);
|
||||
const auto row = categoryItem->rowForRoom(room);
|
||||
if (row) {
|
||||
oldType = static_cast<NeoChatRoomType::Types>(i);
|
||||
oldRow = *row;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldRow == -1) {
|
||||
return;
|
||||
}
|
||||
auto neochatRoom = dynamic_cast<NeoChatRoom *>(room);
|
||||
const auto newType = NeoChatRoomType::typeForRoom(neochatRoom);
|
||||
if (newType == oldType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto oldParent = index(oldType, 0, {});
|
||||
auto oldParentItem = getItem(oldParent);
|
||||
Q_ASSERT(oldParentItem);
|
||||
|
||||
const auto newParent = index(newType, 0, {});
|
||||
auto newParentItem = getItem(newParent);
|
||||
Q_ASSERT(newParentItem);
|
||||
|
||||
// HACK: We're doing this as a remove then insert because moving doesn't work
|
||||
// properly with DelegateChooser for whatever reason.
|
||||
Q_ASSERT(checkIndex(index(oldRow, 0, oldParent), QAbstractItemModel::CheckIndexOption::IndexIsValid));
|
||||
beginRemoveRows(oldParent, oldRow, oldRow);
|
||||
const bool success = oldParentItem->removeChild(oldRow);
|
||||
Q_ASSERT(success);
|
||||
endRemoveRows();
|
||||
beginInsertRows(newParent, newParentItem->childCount(), newParentItem->childCount());
|
||||
newParentItem->insertChild(std::make_unique<RoomTreeItem>(neochatRoom, newParentItem));
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
|
||||
{
|
||||
connect(room, &Room::displaynameChanged, this, [this, room] {
|
||||
refreshRoomRoles(room, {DisplayNameRole});
|
||||
});
|
||||
connect(room, &Room::unreadStatsChanged, this, [this, room] {
|
||||
refreshRoomRoles(room, {ContextNotificationCountRole, HasHighlightNotificationsRole});
|
||||
});
|
||||
connect(room, &Room::avatarChanged, this, [this, room] {
|
||||
refreshRoomRoles(room, {AvatarRole});
|
||||
});
|
||||
connect(room, &Room::tagsChanged, this, [this, room] {
|
||||
moveRoom(room);
|
||||
});
|
||||
connect(room, &Room::joinStateChanged, this, [this, room] {
|
||||
refreshRoomRoles(room);
|
||||
});
|
||||
connect(room, &Room::addedMessages, this, [this, room] {
|
||||
refreshRoomRoles(room, {SubtitleTextRole});
|
||||
});
|
||||
connect(room, &Room::pendingEventMerged, this, [this, room] {
|
||||
refreshRoomRoles(room, {SubtitleTextRole});
|
||||
});
|
||||
connect(room, &NeoChatRoom::pushNotificationStateChanged, this, [this, room] {
|
||||
refreshRoomRoles(room, {ContextNotificationCountRole, HasHighlightNotificationsRole});
|
||||
});
|
||||
}
|
||||
|
||||
void RoomTreeModel::refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles)
|
||||
{
|
||||
const auto index = indexForRoom(room);
|
||||
if (!index.isValid()) {
|
||||
qCritical() << "Room" << room->id() << "not found in the room list";
|
||||
return;
|
||||
}
|
||||
Q_EMIT dataChanged(index, index, roles);
|
||||
}
|
||||
|
||||
NeoChatConnection *RoomTreeModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
int RoomTreeModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return 1;
|
||||
}
|
||||
|
||||
int RoomTreeModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
RoomTreeItem *parentItem;
|
||||
if (parent.column() > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!parent.isValid()) {
|
||||
parentItem = m_rootItem.get();
|
||||
} else {
|
||||
parentItem = static_cast<RoomTreeItem *>(parent.internalPointer());
|
||||
}
|
||||
|
||||
if (!parentItem) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parentItem->childCount();
|
||||
}
|
||||
|
||||
QModelIndex RoomTreeModel::parent(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
RoomTreeItem *childItem = static_cast<RoomTreeItem *>(index.internalPointer());
|
||||
if (!childItem) {
|
||||
return QModelIndex();
|
||||
}
|
||||
RoomTreeItem *parentItem = childItem->parentItem();
|
||||
|
||||
if (parentItem == m_rootItem.get()) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
return createIndex(parentItem->row(), 0, parentItem);
|
||||
}
|
||||
|
||||
QModelIndex RoomTreeModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (!hasIndex(row, column, parent)) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
RoomTreeItem *parentItem = getItem(parent);
|
||||
if (!parentItem) {
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
RoomTreeItem *childItem = parentItem->child(row);
|
||||
if (childItem) {
|
||||
return createIndex(row, column, childItem);
|
||||
}
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> RoomTreeModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[DisplayNameRole] = "displayName";
|
||||
roles[AvatarRole] = "avatar";
|
||||
roles[CanonicalAliasRole] = "canonicalAlias";
|
||||
roles[TopicRole] = "topic";
|
||||
roles[CategoryRole] = "category";
|
||||
roles[ContextNotificationCountRole] = "contextNotificationCount";
|
||||
roles[HasHighlightNotificationsRole] = "hasHighlightNotifications";
|
||||
roles[JoinStateRole] = "joinState";
|
||||
roles[CurrentRoomRole] = "currentRoom";
|
||||
roles[SubtitleTextRole] = "subtitleText";
|
||||
roles[IsSpaceRole] = "isSpace";
|
||||
roles[RoomIdRole] = "roomId";
|
||||
roles[IsChildSpaceRole] = "isChildSpace";
|
||||
roles[IsDirectChat] = "isDirectChat";
|
||||
roles[DelegateTypeRole] = "delegateType";
|
||||
roles[IconRole] = "icon";
|
||||
roles[RoomTypeRole] = "roomType";
|
||||
return roles;
|
||||
}
|
||||
|
||||
// TODO room type changes
|
||||
QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
RoomTreeItem *child = getItem(index);
|
||||
if (std::holds_alternative<NeoChatRoomType::Types>(child->data())) {
|
||||
if (role == DisplayNameRole) {
|
||||
return NeoChatRoomType::typeName(index.row());
|
||||
}
|
||||
if (role == DelegateTypeRole) {
|
||||
if (index.row() == NeoChatRoomType::AddDirect) {
|
||||
return u"addDirect"_s;
|
||||
}
|
||||
return u"section"_s;
|
||||
}
|
||||
if (role == IconRole) {
|
||||
return NeoChatRoomType::typeIconName(index.row());
|
||||
}
|
||||
if (role == CategoryRole) {
|
||||
return index.row();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto room = std::get<NeoChatRoom *>(child->data());
|
||||
Q_ASSERT(room);
|
||||
|
||||
if (role == DisplayNameRole) {
|
||||
return room->displayName();
|
||||
}
|
||||
if (role == AvatarRole) {
|
||||
return room->avatarMediaUrl();
|
||||
}
|
||||
if (role == CanonicalAliasRole) {
|
||||
return room->canonicalAlias();
|
||||
}
|
||||
if (role == TopicRole) {
|
||||
return room->topic();
|
||||
}
|
||||
if (role == CategoryRole) {
|
||||
return NeoChatRoomType::typeForRoom(room);
|
||||
}
|
||||
if (role == ContextNotificationCountRole) {
|
||||
return int(room->contextAwareNotificationCount());
|
||||
}
|
||||
if (role == HasHighlightNotificationsRole) {
|
||||
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
|
||||
}
|
||||
if (role == JoinStateRole) {
|
||||
if (!room->successorId().isEmpty()) {
|
||||
return u"upgraded"_s;
|
||||
}
|
||||
return QVariant::fromValue(room->joinState());
|
||||
}
|
||||
if (role == CurrentRoomRole) {
|
||||
return QVariant::fromValue(room);
|
||||
}
|
||||
if (role == SubtitleTextRole) {
|
||||
if (room->isInvite()) {
|
||||
if (room->isDirectChat()) {
|
||||
return i18nc("@info:label", "Invited you to chat");
|
||||
}
|
||||
return i18nc("@info:label", "%1 invited you", room->member(room->invitingUserId()).displayName());
|
||||
}
|
||||
const auto lastEvent = room->lastEvent(m_hiddenFilter);
|
||||
if (lastEvent == nullptr || room->lastEventIsSpoiler()) {
|
||||
return QString();
|
||||
}
|
||||
return EventHandler::subtitleText(room, lastEvent);
|
||||
}
|
||||
if (role == AvatarImageRole) {
|
||||
return room->avatar(128);
|
||||
}
|
||||
if (role == RoomIdRole) {
|
||||
return room->id();
|
||||
}
|
||||
if (role == IsSpaceRole) {
|
||||
return room->isSpace();
|
||||
}
|
||||
if (role == IsChildSpaceRole) {
|
||||
return SpaceHierarchyCache::instance().isChild(room->id());
|
||||
}
|
||||
if (role == ReplacementIdRole) {
|
||||
return room->successorId();
|
||||
}
|
||||
if (role == IsDirectChat) {
|
||||
return room->isDirectChat();
|
||||
}
|
||||
if (role == DelegateTypeRole) {
|
||||
return u"normal"_s;
|
||||
}
|
||||
if (role == RoomTypeRole) {
|
||||
if (room->creation()) {
|
||||
return room->creation()->contentPart<QString>("type"_L1);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const
|
||||
{
|
||||
if (room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Try and find by checking type.
|
||||
const auto type = NeoChatRoomType::typeForRoom(room);
|
||||
const auto parentItem = m_rootItem->child(type);
|
||||
const auto row = parentItem->rowForRoom(room);
|
||||
if (row) {
|
||||
return index(*row, 0, index(type, 0));
|
||||
}
|
||||
// Double check that the room isn't in the wrong category.
|
||||
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
|
||||
const auto parentItem = m_rootItem->child(i);
|
||||
const auto row = parentItem->rowForRoom(room);
|
||||
if (row) {
|
||||
return index(*row, 0, index(i, 0));
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void RoomTreeModel::setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter)
|
||||
{
|
||||
RoomTreeModel::m_hiddenFilter = hiddenFilter;
|
||||
}
|
||||
|
||||
#include "moc_roomtreemodel.cpp"
|
||||
100
src/rooms/models/roomtreemodel.h
Normal file
100
src/rooms/models/roomtreemodel.h
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QPointer>
|
||||
|
||||
#include "roomtreeitem.h"
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Room;
|
||||
}
|
||||
|
||||
class NeoChatConnection;
|
||||
class NeoChatRoom;
|
||||
|
||||
class RoomTreeModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
DisplayNameRole = Qt::DisplayRole, /**< The display name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
CanonicalAliasRole, /**< The room canonical alias. */
|
||||
TopicRole, /**< The room topic. */
|
||||
CategoryRole, /**< The room category, e.g favourite. */
|
||||
ContextNotificationCountRole, /**< The context aware notification count for the room. */
|
||||
HasHighlightNotificationsRole, /**< Whether there are any highlight notifications. */
|
||||
JoinStateRole, /**< The local user's join state in the room. */
|
||||
CurrentRoomRole, /**< The room object for the room. */
|
||||
SubtitleTextRole, /**< The text to show as the room subtitle. */
|
||||
AvatarImageRole, /**< The room avatar as an image. */
|
||||
RoomIdRole, /**< The room matrix ID. */
|
||||
IsSpaceRole, /**< Whether the room is a space. */
|
||||
IsChildSpaceRole, /**< Whether this space is a child of a different space. */
|
||||
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
|
||||
IsDirectChat, /**< Whether this room is a direct chat. */
|
||||
DelegateTypeRole,
|
||||
IconRole,
|
||||
RoomTypeRole, /**< The room's type. */
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
explicit RoomTreeModel(QObject *parent = nullptr);
|
||||
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
NeoChatConnection *connection() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
QModelIndex parent(const QModelIndex &index) const override;
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
Q_INVOKABLE QModelIndex indexForRoom(NeoChatRoom *room) const;
|
||||
|
||||
static void setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter);
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
|
||||
private:
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
std::unique_ptr<RoomTreeItem> m_rootItem;
|
||||
|
||||
RoomTreeItem *getItem(const QModelIndex &index) const;
|
||||
|
||||
void resetModel();
|
||||
void connectRoomSignals(NeoChatRoom *room);
|
||||
|
||||
void newRoom(Quotient::Room *room);
|
||||
void leftRoom(Quotient::Room *room);
|
||||
void moveRoom(Quotient::Room *room);
|
||||
|
||||
void refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles = {});
|
||||
|
||||
static std::function<bool(const Quotient::RoomEvent *)> m_hiddenFilter;
|
||||
};
|
||||
53
src/rooms/models/sortfilterroomlistmodel.cpp
Normal file
53
src/rooms/models/sortfilterroomlistmodel.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "sortfilterroomlistmodel.h"
|
||||
|
||||
#include "models/roomlistmodel.h"
|
||||
|
||||
#include "neochatconnection.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
SortFilterRoomListModel::SortFilterRoomListModel(RoomListModel *sourceModel, QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
Q_ASSERT(sourceModel);
|
||||
setSourceModel(sourceModel);
|
||||
|
||||
sort(0);
|
||||
invalidateFilter();
|
||||
connect(this, &SortFilterRoomListModel::filterTextChanged, this, [this]() {
|
||||
invalidateFilter();
|
||||
});
|
||||
connect(this, &SortFilterRoomListModel::sourceModelChanged, this, [this]() {
|
||||
connect(this->sourceModel(), &QAbstractListModel::rowsInserted, this, &SortFilterRoomListModel::invalidateRowsFilter);
|
||||
connect(this->sourceModel(), &QAbstractListModel::rowsRemoved, this, &SortFilterRoomListModel::invalidateRowsFilter);
|
||||
});
|
||||
}
|
||||
|
||||
void SortFilterRoomListModel::setFilterText(const QString &text)
|
||||
{
|
||||
m_filterText = text;
|
||||
Q_EMIT filterTextChanged();
|
||||
}
|
||||
|
||||
QString SortFilterRoomListModel::filterText() const
|
||||
{
|
||||
return m_filterText;
|
||||
}
|
||||
|
||||
bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
|
||||
|
||||
if (sourceModel()->data(index, RoomListModel::JoinStateRole).toString() == u"upgraded"_s
|
||||
&& dynamic_cast<RoomListModel *>(sourceModel())->connection()->room(sourceModel()->data(index, RoomListModel::ReplacementIdRole).toString())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return sourceModel()->data(index, RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|
||||
&& sourceModel()->data(index, RoomListModel::IsSpaceRole).toBool() == false;
|
||||
}
|
||||
|
||||
#include "moc_sortfilterroomlistmodel.cpp"
|
||||
63
src/rooms/models/sortfilterroomlistmodel.h
Normal file
63
src/rooms/models/sortfilterroomlistmodel.h
Normal file
@@ -0,0 +1,63 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "models/roomlistmodel.h"
|
||||
|
||||
/**
|
||||
* @class SortFilterRoomListModel
|
||||
*
|
||||
* This model sorts and filters the room list.
|
||||
*
|
||||
* There are numerous room sort orders available:
|
||||
* - Categories - sort rooms by their NeoChatRoomType and then by last activty within
|
||||
* each category.
|
||||
* - LastActivity - sort rooms by the last active time in the room.
|
||||
* - Alphabetical - sort the rooms alphabetically by room name.
|
||||
*
|
||||
* The model can be given a filter string that will only show rooms who's name includes
|
||||
* the text.
|
||||
*
|
||||
* The model can also be given an active space ID and will only show rooms within
|
||||
* that space.
|
||||
*
|
||||
* All space rooms and upgraded rooms will also be filtered out.
|
||||
*/
|
||||
class SortFilterRoomListModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief The text to use to filter room names.
|
||||
*/
|
||||
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
|
||||
|
||||
// This is a lazy hack to make this model compatible with SearchPage. TODO: rename the property entirely
|
||||
Q_PROPERTY(QString searchText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
|
||||
|
||||
public:
|
||||
explicit SortFilterRoomListModel(RoomListModel *sourceModel, QObject *parent = nullptr);
|
||||
|
||||
void setFilterText(const QString &text);
|
||||
[[nodiscard]] QString filterText() const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Whether a row should be shown out or not.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::filterAcceptsRow
|
||||
*/
|
||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void filterTextChanged();
|
||||
|
||||
private:
|
||||
QString m_filterText;
|
||||
};
|
||||
170
src/rooms/models/sortfilterroomtreemodel.cpp
Normal file
170
src/rooms/models/sortfilterroomtreemodel.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "sortfilterroomtreemodel.h"
|
||||
|
||||
#include "enums/neochatroomtype.h"
|
||||
#include "enums/roomsortparameter.h"
|
||||
#include "models/roomtreemodel.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroom.h"
|
||||
#include "spacehierarchycache.h"
|
||||
|
||||
bool SortFilterRoomTreeModel::m_showAllRoomsInHome = false;
|
||||
|
||||
SortFilterRoomTreeModel::SortFilterRoomTreeModel(RoomTreeModel *sourceModel, QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
Q_ASSERT(sourceModel);
|
||||
setSourceModel(sourceModel);
|
||||
|
||||
setRecursiveFilteringEnabled(true);
|
||||
sort(0);
|
||||
connect(this, &SortFilterRoomTreeModel::filterTextChanged, this, &SortFilterRoomTreeModel::invalidateFilter);
|
||||
connect(this, &SortFilterRoomTreeModel::sourceModelChanged, this, [this]() {
|
||||
this->sourceModel()->disconnect(this);
|
||||
connect(this->sourceModel(), &QAbstractItemModel::rowsInserted, this, &SortFilterRoomTreeModel::invalidateFilter);
|
||||
connect(this->sourceModel(), &QAbstractItemModel::rowsRemoved, this, &SortFilterRoomTreeModel::invalidateFilter);
|
||||
});
|
||||
}
|
||||
|
||||
bool SortFilterRoomTreeModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
|
||||
{
|
||||
// Don't sort the top level categories.
|
||||
if (!source_left.parent().isValid() || !source_right.parent().isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto treeModel = dynamic_cast<RoomTreeModel *>(sourceModel());
|
||||
if (treeModel == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto leftRoom = dynamic_cast<NeoChatRoom *>(treeModel->connection()->room(source_left.data(RoomTreeModel::RoomIdRole).toString()));
|
||||
const auto rightRoom = dynamic_cast<NeoChatRoom *>(treeModel->connection()->room(source_right.data(RoomTreeModel::RoomIdRole).toString()));
|
||||
if (leftRoom == nullptr || rightRoom == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto sortRole : RoomSortParameter::currentParameterList()) {
|
||||
auto result = RoomSortParameter::compareParameter(sortRole, leftRoom, rightRoom);
|
||||
|
||||
if (result != 0) {
|
||||
return result > 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SortFilterRoomTreeModel::setFilterText(const QString &text)
|
||||
{
|
||||
m_filterText = text;
|
||||
Q_EMIT filterTextChanged();
|
||||
}
|
||||
|
||||
QString SortFilterRoomTreeModel::filterText() const
|
||||
{
|
||||
return m_filterText;
|
||||
}
|
||||
|
||||
bool SortFilterRoomTreeModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
if (!source_parent.isValid()) {
|
||||
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::AddDirect
|
||||
&& m_mode == DirectChats) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
|
||||
|
||||
bool acceptRoom = sourceModel()->data(index, RoomTreeModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|
||||
&& sourceModel()->data(index, RoomTreeModel::IsSpaceRole).toBool() == false;
|
||||
|
||||
bool isDirectChat = sourceModel()->data(index, RoomTreeModel::IsDirectChat).toBool();
|
||||
// In `show direct chats` mode we only care about whether or not it's a direct chat or if the filter string matches.'
|
||||
if (m_mode == DirectChats) {
|
||||
return isDirectChat && acceptRoom;
|
||||
}
|
||||
|
||||
// When not in `show direct chats` mode, filter them out.
|
||||
if (isDirectChat && m_mode == Rooms) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceModel()->data(index, RoomTreeModel::JoinStateRole).toString() == u"upgraded"_s
|
||||
&& dynamic_cast<RoomTreeModel *>(sourceModel())->connection()->room(sourceModel()->data(index, RoomTreeModel::ReplacementIdRole).toString())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide rooms with defined types, assuming that data-holding rooms have a defined type
|
||||
if (!sourceModel()->data(index, RoomTreeModel::RoomTypeRole).toString().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_showAllRoomsInHome && m_activeSpaceId.isEmpty()) {
|
||||
return acceptRoom;
|
||||
}
|
||||
|
||||
if (m_activeSpaceId.isEmpty()) {
|
||||
if (!SpaceHierarchyCache::instance().isChild(sourceModel()->data(index, RoomTreeModel::RoomIdRole).toString())) {
|
||||
return acceptRoom;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
const auto &rooms = SpaceHierarchyCache::instance().getRoomListForSpace(m_activeSpaceId, false);
|
||||
return std::find(rooms.begin(), rooms.end(), sourceModel()->data(index, RoomTreeModel::RoomIdRole).toString()) != rooms.end() && acceptRoom;
|
||||
}
|
||||
}
|
||||
|
||||
QString SortFilterRoomTreeModel::activeSpaceId() const
|
||||
{
|
||||
return m_activeSpaceId;
|
||||
}
|
||||
|
||||
void SortFilterRoomTreeModel::setActiveSpaceId(const QString &spaceId)
|
||||
{
|
||||
m_activeSpaceId = spaceId;
|
||||
Q_EMIT activeSpaceIdChanged();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void SortFilterRoomTreeModel::setCurrentRoom(NeoChatRoom *room)
|
||||
{
|
||||
m_currentRoom = room;
|
||||
}
|
||||
|
||||
SortFilterRoomTreeModel::Mode SortFilterRoomTreeModel::mode() const
|
||||
{
|
||||
return m_mode;
|
||||
}
|
||||
|
||||
void SortFilterRoomTreeModel::setMode(SortFilterRoomTreeModel::Mode mode)
|
||||
{
|
||||
if (m_mode == mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_mode = mode;
|
||||
Q_EMIT modeChanged();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
QModelIndex SortFilterRoomTreeModel::currentRoomIndex() const
|
||||
{
|
||||
const auto roomModel = dynamic_cast<RoomTreeModel *>(sourceModel());
|
||||
if (roomModel == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return mapFromSource(roomModel->indexForRoom(m_currentRoom));
|
||||
}
|
||||
|
||||
void SortFilterRoomTreeModel::setShowAllRoomsInHome(bool enabled)
|
||||
{
|
||||
SortFilterRoomTreeModel::m_showAllRoomsInHome = enabled;
|
||||
}
|
||||
|
||||
#include "moc_sortfilterroomtreemodel.cpp"
|
||||
116
src/rooms/models/sortfilterroomtreemodel.h
Normal file
116
src/rooms/models/sortfilterroomtreemodel.h
Normal file
@@ -0,0 +1,116 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class RoomTreeModel;
|
||||
|
||||
/**
|
||||
* @class SortFilterRoomTreeModel
|
||||
*
|
||||
* This model sorts and filters the room list.
|
||||
*
|
||||
* There are numerous room sort orders available:
|
||||
* - Categories - sort rooms by their NeoChatRoomType and then by last activty within
|
||||
* each category.
|
||||
* - LastActivity - sort rooms by the last active time in the room.
|
||||
* - Alphabetical - sort the rooms alphabetically by room name.
|
||||
*
|
||||
* The model can be given a filter string that will only show rooms who's name includes
|
||||
* the text.
|
||||
*
|
||||
* The model can also be given an active space ID and will only show rooms within
|
||||
* that space.
|
||||
*
|
||||
* All space rooms and upgraded rooms will also be filtered out.
|
||||
*/
|
||||
class SortFilterRoomTreeModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief The text to use to filter room names.
|
||||
*/
|
||||
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
|
||||
|
||||
/**
|
||||
* @brief Set the ID of the space to show rooms for.
|
||||
*/
|
||||
Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether only direct chats should be shown.
|
||||
*/
|
||||
Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
|
||||
|
||||
public:
|
||||
enum RoomSortOrder {
|
||||
Alphabetical,
|
||||
Activity,
|
||||
LastMessage,
|
||||
};
|
||||
Q_ENUM(RoomSortOrder)
|
||||
|
||||
enum Mode {
|
||||
Rooms,
|
||||
DirectChats,
|
||||
All,
|
||||
};
|
||||
Q_ENUM(Mode)
|
||||
|
||||
explicit SortFilterRoomTreeModel(RoomTreeModel *sourceModel, QObject *parent = nullptr);
|
||||
|
||||
void setFilterText(const QString &text);
|
||||
[[nodiscard]] QString filterText() const;
|
||||
|
||||
QString activeSpaceId() const;
|
||||
void setActiveSpaceId(const QString &spaceId);
|
||||
|
||||
/**
|
||||
* @brief Set the current active room.
|
||||
*/
|
||||
void setCurrentRoom(NeoChatRoom *room);
|
||||
|
||||
Mode mode() const;
|
||||
void setMode(Mode mode);
|
||||
|
||||
Q_INVOKABLE QModelIndex currentRoomIndex() const;
|
||||
|
||||
static void setShowAllRoomsInHome(bool enabled);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Returns true if the value of source_left is less than source_right.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::lessThan
|
||||
*/
|
||||
[[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
|
||||
|
||||
/**
|
||||
* @brief Whether a row should be shown out or not.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::filterAcceptsRow
|
||||
*/
|
||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void filterTextChanged();
|
||||
void activeSpaceIdChanged();
|
||||
void modeChanged();
|
||||
|
||||
private:
|
||||
Mode m_mode = All;
|
||||
QString m_filterText;
|
||||
QString m_activeSpaceId;
|
||||
|
||||
QPointer<NeoChatRoom> m_currentRoom;
|
||||
|
||||
static bool m_showAllRoomsInHome;
|
||||
};
|
||||
42
src/rooms/models/sortfilterspacelistmodel.cpp
Normal file
42
src/rooms/models/sortfilterspacelistmodel.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2022 Snehit Sah <hi@snehit.dev>
|
||||
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
#include "sortfilterspacelistmodel.h"
|
||||
|
||||
#include "models/roomlistmodel.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
SortFilterSpaceListModel::SortFilterSpaceListModel(RoomListModel *sourceModel, QObject *parent)
|
||||
: QSortFilterProxyModel{parent}
|
||||
{
|
||||
Q_ASSERT(sourceModel);
|
||||
setSourceModel(sourceModel);
|
||||
|
||||
connect(this->sourceModel(), &QAbstractListModel::dataChanged, this, [this](const QModelIndex &, const QModelIndex &, QList<int> roles) {
|
||||
if (roles.contains(RoomListModel::IsChildSpaceRole)) {
|
||||
invalidate();
|
||||
}
|
||||
Q_EMIT countChanged();
|
||||
});
|
||||
|
||||
setSortRole(RoomListModel::RoomIdRole);
|
||||
sort(0);
|
||||
}
|
||||
|
||||
bool SortFilterSpaceListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
Q_UNUSED(source_parent);
|
||||
return sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool()
|
||||
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != u"upgraded"_s
|
||||
&& !sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsChildSpaceRole).toBool();
|
||||
}
|
||||
|
||||
bool SortFilterSpaceListModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
|
||||
{
|
||||
const auto idLeft = sourceModel()->data(source_left, RoomListModel::RoomIdRole).toString();
|
||||
const auto idRight = sourceModel()->data(source_right, RoomListModel::RoomIdRole).toString();
|
||||
return idLeft < idRight;
|
||||
}
|
||||
|
||||
#include "moc_sortfilterspacelistmodel.cpp"
|
||||
50
src/rooms/models/sortfilterspacelistmodel.h
Normal file
50
src/rooms/models/sortfilterspacelistmodel.h
Normal file
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2022 Snehit Sah <hi@snehit.dev>
|
||||
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "models/roomlistmodel.h"
|
||||
|
||||
/**
|
||||
* @class SortFilterSpaceListModel
|
||||
*
|
||||
* This model sorts and filters the space list.
|
||||
*
|
||||
* The spaces are sorted by their matrix ID. The filter only shows space rooms,
|
||||
* but filters out upgraded spaces.
|
||||
*/
|
||||
class SortFilterSpaceListModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief The number of spaces in the model.
|
||||
*/
|
||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||
|
||||
public:
|
||||
explicit SortFilterSpaceListModel(RoomListModel *sourceModel, QObject *parent = nullptr);
|
||||
|
||||
Q_SIGNALS:
|
||||
void countChanged();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Returns true if the value of source_left is less than source_right.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::lessThan
|
||||
*/
|
||||
[[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
|
||||
|
||||
/**
|
||||
* @brief Whether a row should be shown out or not.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::filterAcceptsRow
|
||||
*/
|
||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
};
|
||||
Reference in New Issue
Block a user