matrixion/resources/qml/RoomList.qml
2024-11-17 19:42:22 +01:00

852 lines
32 KiB
QML

// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import "./components"
import "./dialogs"
import "./ui"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import im.nheko
Page {
//leftPadding: Nheko.paddingSmall
//rightPadding: Nheko.paddingSmall
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
property bool collapsed: false
background: Rectangle {
color: Nheko.theme.sidebarBackground
}
footer: ColumnLayout {
spacing: 0
Rectangle {
Layout.fillWidth: true
color: Nheko.theme.separator
Layout.preferredHeight: 1
}
Pane {
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
Layout.minimumHeight: 40
horizontalPadding: Nheko.paddingMedium
verticalPadding: 0
background: Rectangle {
color: palette.window
}
contentItem: RowLayout {
id: buttonRow
ImageButton {
id: startChatButton
Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Start a new chat")
ToolTip.visible: hovered
Layout.preferredHeight: 22
Layout.preferredWidth: 22
hoverEnabled: true
image: ":/icons/icons/ui/add-square-button.svg"
onClicked: roomJoinCreateMenu.popup(startChatButton)
Menu {
id: roomJoinCreateMenu
MenuItem {
text: qsTr("Join a room")
onTriggered: Nheko.openJoinRoomDialog()
}
MenuItem {
text: qsTr("Create a new room")
onTriggered: {
var createRoom = createRoomComponent.createObject(timelineRoot);
createRoom.show();
timelineRoot.destroyOnClose(createRoom);
}
}
MenuItem {
text: qsTr("Start a direct chat")
onTriggered: {
var createDirect = createDirectComponent.createObject(timelineRoot);
createDirect.show();
timelineRoot.destroyOnClose(createDirect);
}
}
MenuItem {
text: qsTr("Create a new community")
onTriggered: {
var createRoom = createRoomComponent.createObject(timelineRoot, {
"space": true
});
createRoom.show();
timelineRoot.destroyOnClose(createRoom);
}
}
}
}
ImageButton {
Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Room directory")
ToolTip.visible: hovered
Layout.preferredHeight: 22
Layout.preferredWidth: 22
hoverEnabled: true
image: ":/icons/icons/ui/room-directory.svg"
visible: !collapsed
onClicked: {
var win = roomDirectoryComponent.createObject(timelineRoot);
win.show();
timelineRoot.destroyOnClose(win);
}
}
ImageButton {
Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Search rooms (Ctrl+K)")
ToolTip.visible: hovered
Layout.preferredHeight: 22
Layout.preferredWidth: 22
hoverEnabled: true
image: ":/icons/icons/ui/search.svg"
ripple: false
visible: !collapsed
onClicked: {
var component = Qt.createComponent("qrc:/resources/qml/QuickSwitcher.qml");
if (component.status == Component.Ready) {
var quickSwitch = component.createObject(timelineRoot);
quickSwitch.open();
destroyOnClosed(quickSwitch);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
}
ImageButton {
Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("User settings")
ToolTip.visible: hovered
Layout.preferredHeight: 22
Layout.preferredWidth: 22
hoverEnabled: true
image: ":/icons/icons/ui/settings.svg"
ripple: false
visible: !collapsed
onClicked: mainWindow.push(userSettingsPage)
}
}
}
}
header: ColumnLayout {
spacing: 0
Pane {
id: userInfoPanel
function openUserProfile() {
Nheko.updateUserProfile();
var component = Qt.createComponent("qrc:/resources/qml/dialogs/UserProfile.qml");
if (component.status == Component.Ready) {
var userProfile = component.createObject(timelineRoot, {
"profile": Nheko.currentUser
});
userProfile.show();
timelineRoot.destroyOnClose(userProfile);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
Layout.minimumHeight: 40
//Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
padding: Nheko.paddingMedium
background: Rectangle {
color: palette.window
}
contentItem: RowLayout {
id: userInfoGrid
property var profile: Nheko.currentUser
spacing: Nheko.paddingMedium
Avatar {
id: headerAvatar
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: fontMetrics.lineSpacing * 2
Layout.preferredWidth: fontMetrics.lineSpacing * 2
displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
enabled: false
url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
}
ColumnLayout {
id: col
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.preferredWidth: parent.width - headerAvatar.width - logoutButton.width - Nheko.paddingMedium * 2
spacing: 0
visible: !collapsed
ElidedLabel {
Layout.alignment: Qt.AlignBottom
elideWidth: col.width
font.pointSize: fontMetrics.font.pointSize * 1.1
font.weight: Font.DemiBold
fullText: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
}
ElidedLabel {
Layout.alignment: Qt.AlignTop
color: palette.buttonText
elideWidth: col.width
font.pointSize: fontMetrics.font.pointSize * 0.9
fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
}
}
Item {
}
ImageButton {
id: logoutButton
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: fontMetrics.lineSpacing * 2
Layout.preferredWidth: fontMetrics.lineSpacing * 2
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Logout")
ToolTip.visible: hovered
image: ":/icons/icons/ui/power-off.svg"
visible: !collapsed
onClicked: Nheko.openLogoutDialog()
}
}
InputDialog {
id: statusDialog
prompt: qsTr("Enter your status message:")
title: qsTr("Status Message")
text: userInfoGrid.profile ? Presence.userStatus(userInfoGrid.profile.userid) : ""
onAccepted: function (text) {
Nheko.setStatusMessage(text);
}
}
Menu {
id: userInfoMenu
MenuItem {
text: qsTr("Profile settings")
onTriggered: userInfoPanel.openUserProfile()
}
MenuItem {
text: qsTr("Set status message")
onTriggered: statusDialog.show()
}
MenuSeparator {
}
ButtonGroup {
id: onlineStateGroup
}
MenuItem {
text: qsTr("Automatic online status")
ButtonGroup.group: onlineStateGroup
checkable: true
checked: Settings.presence == Settings.AutomaticPresence
onTriggered: if (checked) Settings.presence = Settings.AutomaticPresence
}
MenuItem {
text: qsTr("Online")
ButtonGroup.group: onlineStateGroup
checkable: true
checked: Settings.presence == Settings.Online
onTriggered: if (checked) Settings.presence = Settings.Online
}
MenuItem {
text: qsTr("Unavailable")
ButtonGroup.group: onlineStateGroup
checkable: true
checked: Settings.presence == Settings.Unavailable
onTriggered: if (checked) Settings.presence = Settings.Unavailable
}
MenuItem {
text: qsTr("Offline")
ButtonGroup.group: onlineStateGroup
checkable: true
checked: Settings.presence == Settings.Offline
onTriggered: if (checked) Settings.presence = Settings.Offline
}
}
TapHandler {
id: userTapHandler
acceptedButtons: Qt.LeftButton
gesturePolicy: TapHandler.ReleaseWithinBounds
margin: -Nheko.paddingSmall
onLongPressed: userInfoMenu.popup(userTapHandler)
onSingleTapped: userInfoPanel.openUserProfile()
}
TapHandler {
id: userTapHandler2
acceptedButtons: Qt.RightButton
gesturePolicy: TapHandler.ReleaseWithinBounds
margin: -Nheko.paddingSmall
onSingleTapped: userInfoMenu.popup(userTapHandler2)
}
}
Rectangle {
Layout.fillWidth: true
color: Nheko.theme.separator
Layout.preferredHeight: 2
}
Rectangle {
id: unverifiedStuffBubble
Layout.fillWidth: true
color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1)
implicitHeight: explanation.height + Nheko.paddingMedium * 2
visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified
RowLayout {
id: unverifiedStuffBubbleContainer
height: explanation.height + Nheko.paddingMedium * 2
spacing: 0
width: parent.width
Label {
id: explanation
Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
Layout.rightMargin: Nheko.paddingSmall
color: palette.buttonText
text: {
switch (SelfVerificationStatus.status) {
case SelfVerificationStatus.NoMasterKey:
//: Cross-signing setup has not run yet.
return qsTr("Encryption not set up");
case SelfVerificationStatus.UnverifiedMasterKey:
//: The user just signed in with this device and hasn't verified their master key.
return qsTr("Unverified login");
case SelfVerificationStatus.UnverifiedDevices:
//: There are unverified devices signed in to this account.
return qsTr("Please verify your other devices");
default:
return "";
}
}
textFormat: Text.PlainText
wrapMode: Text.Wrap
}
ImageButton {
id: closeUnverifiedBubble
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.rightMargin: Nheko.paddingMedium
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Close")
ToolTip.visible: closeUnverifiedBubble.hovered
Layout.preferredHeight: fontMetrics.font.pixelSize
hoverEnabled: true
image: ":/icons/icons/ui/dismiss.svg"
Layout.preferredWidth: fontMetrics.font.pixelSize
onClicked: unverifiedStuffBubble.visible = false
}
}
HoverHandler {
id: verifyButtonHovered
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
enabled: !closeUnverifiedBubble.hovered
}
TapHandler {
acceptedButtons: Qt.LeftButton
enabled: !closeUnverifiedBubble.hovered
onSingleTapped: {
if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices)
SelfVerificationStatus.verifyUnverifiedDevices();
else
SelfVerificationStatus.statusChanged();
}
}
}
Rectangle {
Layout.fillWidth: true
color: Nheko.theme.separator
Layout.preferredHeight: 1
visible: unverifiedStuffBubble.visible
}
}
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections {
function onHideMenu() {
userInfoMenu.close();
roomContextMenu.close();
}
target: MainWindow
}
Component {
id: roomDirectoryComponent
RoomDirectory {
}
}
Component {
id: createRoomComponent
CreateRoom {
}
}
Component {
id: createDirectComponent
CreateDirect {
}
}
ListView {
id: roomlist
anchors.left: parent.left
anchors.right: parent.right
height: parent.height
model: Rooms
boundsBehavior: Flickable.StopAtBounds
//reuseItems: true
ScrollBar.vertical: ScrollBar {
id: scrollbar
parent: !collapsed && Settings.scrollbarsInRoomlist ? roomlist : null
}
delegate: ItemDelegate {
id: roomItem
required property string avatarUrl
property color backgroundColor: palette.window
property color bubbleBackground: palette.highlight
property color bubbleText: palette.highlightedText
required property string directChatOtherUserId
required property bool hasLoudNotification
required property bool hasUnreadMessages
property color importantText: palette.text
required property bool isDirect
required property bool isInvite
required property bool isSpace
required property string lastMessage
required property int notificationCount
required property string roomId
required property string roomName
required property var tags
required property string time
property color unimportantText: palette.buttonText
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: roomName
ToolTip.visible: hovered && collapsed
height: avatarSize + 2 * Nheko.paddingMedium
state: "normal"
width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0)
topInset: 0
bottomInset: 0
leftInset: 0
rightInset: 0
background: Rectangle {
color: backgroundColor
}
states: [
State {
name: "highlight"
when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
PropertyChanges {
roomItem {
backgroundColor: palette.dark
bubbleBackground: palette.highlight
bubbleText: palette.highlightedText
importantText: palette.brightText
unimportantText: palette.brightText
}
}
},
State {
name: "selected"
when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId
PropertyChanges {
roomItem {
backgroundColor: palette.highlight
bubbleBackground: palette.highlightedText
bubbleText: palette.highlight
importantText: palette.highlightedText
unimportantText: palette.highlightedText
}
}
}
]
onClicked: {
console.log("tapped " + roomId);
if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
Rooms.setCurrentRoom(roomId);
else
Rooms.resetCurrentRoom();
}
onPressAndHold: {
if (!isInvite)
roomContextMenu.show(roomItem, roomId, tags);
}
Ripple {
color: Qt.rgba(palette.dark.r, palette.dark.g, palette.dark.b, 0.5)
}
// NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that...
Item {
anchors.fill: parent
anchors.margins: 1
TapHandler {
id: roomItemTh
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
gesturePolicy: TapHandler.ReleaseWithinBounds
onSingleTapped: {
if (!TimelineManager.isInvite)
roomContextMenu.show(roomItemTh, roomId, tags);
}
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
spacing: Nheko.paddingMedium
Avatar {
id: avatar
Layout.alignment: Qt.AlignVCenter
displayName: roomName
enabled: false
roomid: roomId
url: avatarUrl.replace("mxc://", "image://MxcImage/")
userid: isDirect ? directChatOtherUserId : ""
Layout.preferredWidth: avatarSize
Layout.preferredHeight: avatarSize
NotificationBubble {
id: collapsedNotificationBubble
anchors.bottom: parent.bottom
anchors.margins: -Nheko.paddingSmall
anchors.right: parent.right
bubbleBackgroundColor: roomItem.bubbleBackground
bubbleTextColor: roomItem.bubbleText
hasLoudNotification: roomItem.hasLoudNotification
mayBeVisible: collapsed && (isSpace ? Settings.spaceNotifications : true)
notificationCount: roomItem.notificationCount
}
}
ColumnLayout {
id: textContent
Layout.alignment: Qt.AlignLeft
Layout.minimumWidth: 100
Layout.preferredWidth: roomItem.width - avatar.width
Layout.preferredHeight: avatar.height
spacing: Nheko.paddingSmall
visible: !collapsed
Item {
id: titleRow
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
Layout.preferredHeight: subtitleText.implicitHeight
ElidedLabel {
id: titleText
anchors.left: parent.left
color: roomItem.importantText
elideWidth: parent.width - (timestamp.visible ? timestamp.implicitWidth : 0) - (spaceNotificationBubble.visible ? spaceNotificationBubble.implicitWidth : 0)
fullText: TimelineManager.htmlEscape(roomName)
textFormat: Text.RichText
}
Label {
id: timestamp
anchors.baseline: titleText.baseline
anchors.right: parent.right
color: roomItem.unimportantText
font.pixelSize: fontMetrics.font.pixelSize * 0.9
text: time
visible: !isInvite && !isSpace
}
NotificationBubble {
id: spaceNotificationBubble
anchors.right: parent.right
bubbleBackgroundColor: roomItem.bubbleBackground
bubbleTextColor: roomItem.bubbleText
hasLoudNotification: roomItem.hasLoudNotification
mayBeVisible: !collapsed && (isSpace ? Settings.spaceNotifications : false)
notificationCount: roomItem.notificationCount
parent: isSpace ? titleRow : subtextRow
}
}
Item {
id: subtextRow
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
Layout.preferredHeight: subtitleText.implicitHeight
visible: !isSpace
ElidedLabel {
id: subtitleText
anchors.left: parent.left
color: roomItem.unimportantText
elideWidth: subtextRow.width - (subtextNotificationBubble.visible ? subtextNotificationBubble.implicitWidth : 0)
font.pixelSize: fontMetrics.font.pixelSize * 0.9
fullText: TimelineManager.htmlEscape(lastMessage)
textFormat: Text.RichText
}
NotificationBubble {
id: subtextNotificationBubble
anchors.baseline: subtitleText.baseline
anchors.right: parent.right
bubbleBackgroundColor: roomItem.bubbleBackground
bubbleTextColor: roomItem.bubbleText
hasLoudNotification: roomItem.hasLoudNotification
mayBeVisible: !collapsed
notificationCount: roomItem.notificationCount
}
}
}
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: palette.highlight
height: parent.height - Nheko.paddingSmall * 2
visible: hasUnreadMessages
width: 3
}
}
Connections {
function onCurrentRoomChanged() {
if (Rooms.currentRoom)
roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
}
target: Rooms
}
Component {
id: roomWindowComponent
ApplicationWindow {
id: roomWindowW
property var room: null
property var roomPreview: null
color: palette.window
height: 650
minimumHeight: 150
minimumWidth: 150
title: room.plainRoomName
width: 420
Component.onCompleted: {
MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW);
Nheko.setTransientParent(roomWindowW, null);
}
Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW)
onActiveChanged: {
room.lastReadIdOnWindowFocus();
}
//flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Shortcut {
sequence: StandardKey.Cancel
onActivated: roomWindowW.close()
}
TimelineView {
id: timeline
anchors.fill: parent
privacyScreen: privacyScreen
room: roomWindowW.room
roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null
}
PrivacyScreen {
id: privacyScreen
anchors.fill: parent
screenTimeout: Settings.privacyScreenTimeout
timelineRoot: timeline
visible: Settings.privacyScreen
windowTarget: roomWindowW
}
}
}
Menu {
id: roomContextMenu
property string roomid
property var tags
function show(parent, roomid_, tags_) {
roomid = roomid_;
tags = tags_;
popup(parent);
}
Component.onCompleted: {
if (roomContextMenu.popupType != undefined) {
roomContextMenu.popupType = 2; // Popup.Native with fallback on older Qt (<6.8.0)
}
}
Component {
id: nestedSpaceMenuLevel
SpaceMenuLevel {
childMenu: rootSpaceMenu.childMenu
roomid: roomContextMenu.roomid
}
}
InputDialog {
id: newTag
prompt: qsTr("Enter the tag you want to use:")
title: qsTr("New tag")
onAccepted: function (text) {
Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true);
}
}
MenuItem {
text: qsTr("Open separately")
onTriggered: {
var roomWindow = roomWindowComponent.createObject(null, {
"room": Rooms.getRoomById(roomContextMenu.roomid),
"roomPreview": Rooms.getRoomPreviewById(roomContextMenu.roomid)
});
roomWindow.showNormal();
destroyOnClose(roomWindow);
}
}
MenuItem {
text: qsTr("Mark as read")
onTriggered: Rooms.getRoomById(roomContextMenu.roomid).markRoomAsRead()
}
MenuItem {
text: qsTr("Room settings")
onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid)
}
MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
}
MenuItem {
text: qsTr("Copy room link")
onTriggered: Rooms.copyLink(roomContextMenu.roomid)
}
Menu {
id: tagsMenu
title: qsTr("Tag room as:")
Instantiator {
model: Communities.tagsWithDefault
delegate: MenuItem {
property string t: modelData
checkable: true
checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t)
text: {
switch (t) {
case "m.favourite":
return qsTr("Favourite");
case "m.lowpriority":
return qsTr("Low priority");
case "m.server_notice":
return qsTr("Server notice");
default:
return t.substring(2);
}
}
onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked)
}
onObjectAdded: (index, object) => tagsMenu.insertItem(index, object)
onObjectRemoved: (index, object) => tagsMenu.removeItem(object)
}
MenuItem {
text: qsTr("Create new tag...")
onTriggered: newTag.show()
}
}
SpaceMenuLevel {
id: rootSpaceMenu
childMenu: nestedSpaceMenuLevel
position: -1
roomid: roomContextMenu.roomid
title: qsTr("Add or remove from community...")
}
}
}
}