Spectral/imports/Spectral/Panel/RoomListPanel.qml

959 lines
28 KiB
QML

import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
import SortFilterProxyModel 0.2
Item {
property var controller: null
readonly property var user: controller.connection ? controller.connection.localUser : null
property int filter: 0
property var enteredRoom: null
property alias errorControl: errorControl
signal enterRoom(var room)
signal leaveRoom(var room)
id: root
RoomListModel {
id: roomListModel
connection: controller.connection
onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon)
}
SortFilterProxyModel {
id: sortedRoomListModel
sourceModel: roomListModel
proxyRoles: ExpressionRole {
name: "display"
expression: {
switch (category) {
case 1: return "Invited"
case 2: return "Favorites"
case 3: return "Rooms"
case 4: return "People"
case 5: return "Low Priority"
}
}
}
sorters: [
RoleSorter { roleName: "category" },
RoleSorter {
roleName: "lastActiveTime"
sortOrder: Qt.DescendingOrder
}
]
filters: [
RegExpFilter {
roleName: "name"
pattern: searchField.text
caseSensitivity: Qt.CaseInsensitive
},
ExpressionFilter {
enabled: filter === 1
expression: unreadCount > 0
},
ExpressionFilter {
enabled: filter === 2
expression: category === 1 || category === 2 || category === 4
},
ExpressionFilter {
enabled: filter === 3
expression: category === 3 || category === 5
}
]
}
Drawer {
width: Math.max(root.width, 400)
height: root.height
id: drawer
edge: Qt.LeftEdge
Component {
id: mainPage
ColumnLayout {
readonly property string title: "Main"
id: mainColumn
spacing: 0
Control {
Layout.fillWidth: true
Layout.preferredHeight: 330
padding: 24
contentItem: ColumnLayout {
spacing: 4
Avatar {
Layout.preferredWidth: 200
Layout.preferredHeight: 200
Layout.margins: 12
Layout.alignment: Qt.AlignHCenter
source: root.user ? root.user.avatarMediaId : null
hint: root.user ? root.user.displayName : "?"
}
Label {
Layout.alignment: Qt.AlignHCenter
text: root.user ? root.user.displayName : "No Name"
color: "white"
font.pixelSize: 22
}
Label {
Layout.alignment: Qt.AlignHCenter
text: root.user ? root.user.id : "@example:matrix.org"
color: "white"
opacity: 0.7
font.pixelSize: 13
}
}
background: Rectangle { color: Material.primary }
RippleEffect {
anchors.fill: parent
onClicked: stackView.push(userPage)
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ColumnLayout {
width: mainColumn.width
spacing: 0
Repeater {
model: AccountListModel {
controller: spectralController
}
delegate: ItemDelegate {
Layout.fillWidth: true
text: user.displayName
onClicked: {
controller.connection = connection
drawer.close()
}
}
}
ItemDelegate {
Layout.fillWidth: true
text: "Add Account"
onClicked: loginDialog.open()
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: MSettings.darkTheme ? "#424242" : "#e7ebeb"
}
ItemDelegate {
Layout.fillWidth: true
text: "Settings"
onClicked: stackView.push(settingsPage)
}
ItemDelegate {
Layout.fillWidth: true
text: "Logout"
onClicked: controller.logout(controller.connection)
}
ItemDelegate {
Layout.fillWidth: true
text: "Exit"
onClicked: Qt.quit()
}
}
}
}
}
Component {
id: userPage
ScrollView {
readonly property string title: "User Info"
id: main
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ColumnLayout {
width: main.width
spacing: 0
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Matrix ID"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.user.id
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Name"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.user.name
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Avatar"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.user.avatarMediaId
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Server"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.controller.connection.accessToken
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Device"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.controller.connection.deviceId
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Token"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.controller.connection.accessToken
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
}
}
Component {
id: settingsPage
ScrollView {
readonly property string title: "Settings"
id: main
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
padding: 32
ColumnLayout {
width: main.width - 64
spacing: 0
Switch {
text: "Dark theme"
checked: MSettings.darkTheme
onCheckedChanged: MSettings.darkTheme = checked
}
Switch {
text: "Show notifications"
checked: MSettings.showNotification
onCheckedChanged: MSettings.showNotification = checked
}
Switch {
text: "Use press and hold instead of right click"
checked: MSettings.pressAndHold
onCheckedChanged: MSettings.pressAndHold = checked
}
Switch {
text: "Show tray icon"
checked: MSettings.showTray
onCheckedChanged: MSettings.showTray = checked
}
Switch {
text: "Enable timeline background"
checked: MSettings.enableTimelineBackground
onCheckedChanged: MSettings.enableTimelineBackground = checked
}
RowLayout {
Layout.fillWidth: true
Label {
text: "DPI"
}
Slider {
Layout.fillWidth: true
value: controller.dpi()
from: 100
to: 300
stepSize: 25
snapMode: Slider.SnapAlways
ToolTip.visible: pressed
ToolTip.text: value
onMoved: controller.setDpi(value)
}
}
}
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 64
visible: stackView.depth > 1
color: Material.primary
RowLayout {
anchors.fill: parent
anchors.margins: 4
ToolButton {
Layout.preferredWidth: height
Layout.fillHeight: true
contentItem: MaterialIcon {
icon: "\ue5c4"
color: "white"
}
onClicked: stackView.pop()
}
Label {
Layout.fillWidth: true
text: stackView.currentItem.title
color: "white"
font.pixelSize: 18
elide: Label.ElideRight
}
}
}
StackView {
Layout.fillWidth: true
Layout.fillHeight: true
id: stackView
clip: true
initialItem: mainPage
}
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Control {
Layout.fillWidth: true
Layout.preferredHeight: 64
id: roomListHeader
topPadding: 12
bottomPadding: 12
leftPadding: 12
rightPadding: 18
contentItem: RowLayout {
ItemDelegate {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: !searchField.active
contentItem: MaterialIcon {
icon: {
switch (filter) {
case 0: return "\ue8b6"
case 1: return "\ue7f5"
case 2: return "\ue7ff"
case 3: return "\ue7fc"
}
}
}
Menu {
id: filterMenu
MenuItem {
text: "All"
onClicked: filter = 0
}
MenuSeparator {}
MenuItem {
text: "New"
onClicked: filter = 1
}
MenuItem {
text: "People"
onClicked: filter = 2
}
MenuItem {
text: "Group"
onClicked: filter = 3
}
}
onClicked: filterMenu.popup()
}
ItemDelegate {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: searchField.active
contentItem: MaterialIcon { icon: "\ue5cd" }
onClicked: searchField.clear()
}
AutoTextField {
readonly property bool active: text
readonly property bool isRoom: text.match(/#.*:.*\..*/g) || text.match(/!.*:.*\..*/g)
readonly property bool isUser: text.match(/@.*:.*\..*/g)
Layout.fillWidth: true
Layout.fillHeight: true
id: searchField
topPadding: 0
bottomPadding: 0
placeholderText: "Search..."
color: MPalette.lighter
background: Item {}
}
ItemDelegate {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: searchField.isRoom || searchField.isUser
contentItem: MaterialIcon { icon: "\ue145" }
onClicked: {
if (searchField.isRoom) {
controller.joinRoom(controller.connection, searchField.text)
return
}
if (searchField.isUser) {
controller.createDirectChat(controller.connection, searchField.text)
return
}
}
}
Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
Layout.alignment: Qt.AlignRight
visible: !searchField.active
source: root.user ? root.user.avatarMediaId : null
hint: root.user ? root.user.displayName : "?"
MouseArea {
anchors.fill: parent
onClicked: drawer.open()
}
}
}
background: Rectangle {
color: Material.background
opacity: listView.atYBeginning ? 0 : 1
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
}
}
Control {
property string error: ""
property string detail: ""
Layout.fillWidth: true
id: errorControl
visible: false
topPadding: 16
bottomPadding: 16
leftPadding: 24
rightPadding: 24
contentItem: ColumnLayout {
Label {
Layout.fillWidth: true
text: errorControl.error
font.pixelSize: 16
color: "white"
wrapMode: Text.Wrap
}
Label {
Layout.fillWidth: true
text: errorControl.detail
font.pixelSize: 14
color: "white"
opacity: 0.6
wrapMode: Text.Wrap
}
}
background: Rectangle {
color: "#273338"
}
RippleEffect {
anchors.fill: parent
onClicked: errorControl.visible = false
}
}
AutoListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: listView
spacing: 0
clip: true
model: sortedRoomListModel
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: ScrollBar {}
delegate: Item {
width: listView.width
height: 64
Rectangle {
anchors.fill: parent
visible: currentRoom === enteredRoom
color: Material.accent
opacity: 0.1
}
Rectangle {
width: unreadCount > 0 ? 4 : 0
height: parent.height
color: Material.accent
Behavior on width {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
source: avatar
hint: name || "No Name"
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: name || "No Name"
color: MPalette.foreground
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm,"")
color: MPalette.lighter
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
Label {
visible: notificationCount > 0 && highlightCount == 0
color: "white"
text: notificationCount
leftPadding: 12
rightPadding: 12
topPadding: 4
bottomPadding: 4
font.bold: true
background: Rectangle {
radius: height / 2
color: MPalette.lighter
}
}
Label {
visible: highlightCount > 0
color: "white"
text: highlightCount
leftPadding: 12
rightPadding: 12
topPadding: 4
bottomPadding: 4
font.bold: true
background: Rectangle {
radius: height / 2
color: MPalette.accent
}
}
}
RippleEffect {
anchors.fill: parent
onSecondaryClicked: roomContextMenu.popup()
onPrimaryClicked: {
if (category === RoomType.Invited) {
inviteDialog.currentRoom = currentRoom
inviteDialog.open()
} else {
if (enteredRoom) {
enteredRoom.displayed = false
leaveRoom(enteredRoom)
}
currentRoom.displayed = true
enterRoom(currentRoom)
enteredRoom = currentRoom
}
}
}
Menu {
id: roomContextMenu
MenuItem {
text: "Favourite"
checkable: true
checked: category === RoomType.Favorite
onTriggered: category === RoomType.Favorite ? currentRoom.removeTag("m.favourite") : currentRoom.addTag("m.favourite", 1.0)
}
MenuItem {
text: "Deprioritize"
checkable: true
checked: category === RoomType.Deprioritized
onTriggered: category === RoomType.Deprioritized ? currentRoom.removeTag("m.lowpriority") : currentRoom.addTag("m.lowpriority", 1.0)
}
MenuSeparator {}
MenuItem {
text: "Mark as Read"
onTriggered: currentRoom.markAllMessagesAsRead()
}
MenuItem {
text: "Leave Room"
onTriggered: currentRoom.forget()
}
}
}
section.property: "display"
section.criteria: ViewSection.FullString
section.delegate: Label {
width: parent.width
height: 24
text: section
color: MPalette.lighter
leftPadding: 16
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
Dialog {
property var currentRoom
id: inviteDialog
parent: ApplicationWindow.overlay
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 360
title: "Action Required"
modal: true
contentItem: Label { text: "Accept this invitation?" }
footer: DialogButtonBox {
Button {
text: "Accept"
flat: true
onClicked: currentRoom.acceptInvitation()
}
Button {
text: "Reject"
flat: true
onClicked: currentRoom.forget()
}
Button {
text: "Cancel"
flat: true
onClicked: inviteDialog.close()
}
}
}
}