// SPDX-FileCopyrightText: 2021 Nheko Contributors // SPDX-FileCopyrightText: 2022 Nheko Contributors // // SPDX-License-Identifier: GPL-3.0-or-later import "./dialogs" import "./ui" import Qt.labs.platform 1.1 as Platform import QtQml 2.12 import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 import im.nheko 1.0 Page { //leftPadding: Nheko.paddingSmall //rightPadding: Nheko.paddingSmall property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) property bool collapsed: false 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 reuseItems: true ScrollHelper { flickable: parent anchors.fill: parent } 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 Component.onCompleted: MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW) Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW) height: 650 width: 420 minimumWidth: 150 minimumHeight: 150 palette: Nheko.colors color: Nheko.colors.window title: room.plainRoomName modality: Qt.NonModal flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint Shortcut { sequence: StandardKey.Cancel onActivated: roomWindowW.close() } TimelineView { id: timelineView anchors.fill: parent room: roomWindowW.room roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null } PrivacyScreen { anchors.fill: parent visible: Settings.privacyScreen screenTimeout: Settings.privacyScreenTimeout timelineRoot: timelineView windowTarget: roomWindowW } } } delegate: ItemDelegate { id: roomItem property color backgroundColor: Nheko.colors.window property color importantText: Nheko.colors.text property color unimportantText: Nheko.colors.buttonText property color bubbleBackground: Nheko.colors.highlight property color bubbleText: Nheko.colors.highlightedText required property string roomName required property string roomId required property string avatarUrl required property string time required property string lastMessage required property var tags required property bool isInvite required property bool isSpace required property int notificationCount required property bool hasLoudNotification required property bool hasUnreadMessages required property bool isDirect required property string directChatOtherUserId Ripple { color: Qt.rgba(Nheko.colors.dark.r, Nheko.colors.dark.g, Nheko.colors.dark.b, 0.5) } height: avatarSize + 2 * Nheko.paddingMedium width: ListView.view.width state: "normal" ToolTip.visible: hovered && collapsed ToolTip.delay: Nheko.tooltipDelay ToolTip.text: roomName onClicked: { console.log("tapped " + roomId); if (isSpace && Communities.currentTagId != "space:"+roomId) Communities.currentTagId = "space:"+roomId; if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId) Rooms.setCurrentRoom(roomId); else Rooms.resetCurrentRoom(); } onPressAndHold: { if (!isInvite) roomContextMenu.open(); } states: [ State { name: "highlight" when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId) PropertyChanges { target: roomItem backgroundColor: Nheko.colors.dark importantText: Nheko.colors.brightText unimportantText: Nheko.colors.brightText bubbleBackground: Nheko.colors.highlight bubbleText: Nheko.colors.highlightedText } }, State { name: "selected" when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId PropertyChanges { target: roomItem backgroundColor: Nheko.colors.highlight importantText: Nheko.colors.highlightedText unimportantText: Nheko.colors.highlightedText bubbleBackground: Nheko.colors.highlightedText bubbleText: Nheko.colors.highlight } } ] Platform.Menu { id: roomContextMenu InputDialog { id: newTag title: qsTr("New tag") prompt: qsTr("Enter the tag you want to use:") onAccepted: function(text) { Rooms.toggleTag(roomId, "u." + text, true); } } Platform.MenuItem { text: qsTr("Open separately") onTriggered: { var roomWindow = roomWindowComponent.createObject(null, { "room": Rooms.getRoomById(roomId), "roomPreview": Rooms.getRoomPreviewById(roomId) }); roomWindow.showNormal(); destroyOnClose(roomWindow); } } Platform.MenuItem { text: qsTr("Leave room") onTriggered: TimelineManager.openLeaveRoomDialog(roomId) } Platform.MenuSeparator { text: qsTr("Tag room as:") } Instantiator { model: Communities.tagsWithDefault onObjectAdded: roomContextMenu.insertItem(index + 3, object) onObjectRemoved: roomContextMenu.removeItem(object) delegate: Platform.MenuItem { property string t: modelData 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); } } checkable: true checked: tags !== undefined && tags.includes(t) onTriggered: Rooms.toggleTag(roomId, t, checked) } } Platform.MenuItem { text: qsTr("Create new tag...") onTriggered: newTag.show() } } // 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 { acceptedButtons: Qt.RightButton onSingleTapped: { if (!TimelineManager.isInvite) roomContextMenu.open(); } gesturePolicy: TapHandler.ReleaseWithinBounds acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad } } RowLayout { spacing: Nheko.paddingMedium anchors.fill: parent anchors.margins: Nheko.paddingMedium Avatar { // In the future we could show an online indicator by setting the userid for the avatar //userid: Nheko.currentUser.userid id: avatar enabled: false Layout.alignment: Qt.AlignVCenter height: avatarSize width: avatarSize url: avatarUrl.replace("mxc://", "image://MxcImage/") displayName: roomName userid: isDirect ? directChatOtherUserId : "" roomid: roomId Rectangle { id: collapsedNotificationBubble anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: -Nheko.paddingSmall visible: collapsed && notificationCount > 0 enabled: false Layout.alignment: Qt.AlignRight height: fontMetrics.averageCharacterWidth * 3 width: Math.max(collapsedBubbleText.width, height) radius: height / 2 color: hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground Label { id: collapsedBubbleText anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter width: Math.max(implicitWidth + Nheko.paddingMedium, parent.height) font.bold: true font.pixelSize: fontMetrics.font.pixelSize * 0.8 color: hasLoudNotification ? "white" : roomItem.bubbleText text: notificationCount > 9999 ? "9999+" : notificationCount } } } ColumnLayout { id: textContent visible: !collapsed Layout.alignment: Qt.AlignLeft Layout.fillWidth: true Layout.minimumWidth: 100 width: parent.width - avatar.width Layout.preferredWidth: parent.width - avatar.width height: avatar.height spacing: Nheko.paddingSmall RowLayout { Layout.alignment: Qt.AlignTop Layout.fillWidth: true spacing: Nheko.paddingSmall ElidedLabel { id: rN Layout.alignment: Qt.AlignBaseline color: roomItem.importantText elideWidth: width fullText: roomName textFormat: Text.RichText Layout.fillWidth: true } Label { id: timestamp visible: !isInvite && !isSpace width: visible ? 0 : undefined Layout.alignment: Qt.AlignRight | Qt.AlignBaseline font.pixelSize: fontMetrics.font.pixelSize * 0.9 color: roomItem.unimportantText text: time } } RowLayout { Layout.fillWidth: true spacing: 0 visible: !isSpace height: visible ? 0 : undefined Layout.alignment: Qt.AlignBottom ElidedLabel { color: roomItem.unimportantText font.pixelSize: fontMetrics.font.pixelSize * 0.9 elideWidth: width fullText: lastMessage textFormat: Text.RichText Layout.fillWidth: true } Rectangle { id: notificationBubble visible: notificationCount > 0 Layout.alignment: Qt.AlignRight Layout.leftMargin: Nheko.paddingSmall height: notificationBubbleText.height + Nheko.paddingMedium Layout.preferredWidth: Math.max(notificationBubbleText.width, height) radius: height / 2 color: hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground ToolTip.text: notificationCount ToolTip.delay: Nheko.tooltipDelay ToolTip.visible: notificationBubbleHover.hovered && (notificationCount > 9999) Label { id: notificationBubbleText anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter width: Math.max(implicitWidth + Nheko.paddingMedium, parent.height) font.bold: true font.pixelSize: fontMetrics.font.pixelSize * 0.8 color: hasLoudNotification ? "white" : roomItem.bubbleText text: notificationCount > 9999 ? "9999+" : notificationCount HoverHandler { id: notificationBubbleHover } } } } } } Rectangle { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter height: parent.height - Nheko.paddingSmall * 2 width: 3 color: Nheko.colors.highlight visible: hasUnreadMessages } background: Rectangle { color: backgroundColor } } } background: Rectangle { color: Nheko.theme.sidebarBackground } header: ColumnLayout { spacing: 0 Pane { id: userInfoPanel function openUserProfile() { Nheko.updateUserProfile(); var userProfile = userProfileComponent.createObject(timelineRoot, { "profile": Nheko.currentUser }); userProfile.show(); timelineRoot.destroyOnClose(userProfile); } Layout.fillWidth: true Layout.alignment: Qt.AlignBottom //Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium padding: Nheko.paddingMedium Layout.minimumHeight: 40 background: Rectangle {color: Nheko.colors.window} InputDialog { id: statusDialog title: qsTr("Status Message") prompt: qsTr("Enter your status message:") onAccepted: function(text) { Nheko.setStatusMessage(text); } } Platform.Menu { id: userInfoMenu Platform.MenuItem { text: qsTr("Profile settings") onTriggered: userInfoPanel.openUserProfile() } Platform.MenuItem { text: qsTr("Set status message") onTriggered: statusDialog.show() } } TapHandler { margin: -Nheko.paddingSmall acceptedButtons: Qt.LeftButton onSingleTapped: userInfoPanel.openUserProfile() onLongPressed: userInfoMenu.open() gesturePolicy: TapHandler.ReleaseWithinBounds } TapHandler { margin: -Nheko.paddingSmall acceptedButtons: Qt.RightButton onSingleTapped: userInfoMenu.open() gesturePolicy: TapHandler.ReleaseWithinBounds } contentItem: RowLayout { id: userInfoGrid property var profile: Nheko.currentUser spacing: Nheko.paddingMedium Avatar { id: avatar Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: fontMetrics.lineSpacing * 2 Layout.preferredHeight: fontMetrics.lineSpacing * 2 url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/") displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" userid: userInfoGrid.profile ? userInfoGrid.profile.userid : "" enabled: false } ColumnLayout { id: col visible: !collapsed Layout.alignment: Qt.AlignLeft Layout.fillWidth: true width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 Layout.preferredWidth: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 spacing: 0 ElidedLabel { Layout.alignment: Qt.AlignBottom font.pointSize: fontMetrics.font.pointSize * 1.1 font.weight: Font.DemiBold fullText: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" elideWidth: col.width } ElidedLabel { Layout.alignment: Qt.AlignTop color: Nheko.colors.buttonText font.pointSize: fontMetrics.font.pointSize * 0.9 elideWidth: col.width fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : "" } } Item { } ImageButton { id: logoutButton visible: !collapsed Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: fontMetrics.lineSpacing * 2 Layout.preferredHeight: fontMetrics.lineSpacing * 2 image: ":/icons/icons/ui/power-off.svg" ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Logout") onClicked: Nheko.openLogoutDialog() } } } Rectangle { color: Nheko.theme.separator height: 2 Layout.fillWidth: true } Rectangle { id: unverifiedStuffBubble color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1) Layout.fillWidth: true implicitHeight: explanation.height + Nheko.paddingMedium * 2 visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified RowLayout { id: unverifiedStuffBubbleContainer width: parent.width height: explanation.height + Nheko.paddingMedium * 2 spacing: 0 Label { id: explanation Layout.margins: Nheko.paddingMedium Layout.rightMargin: Nheko.paddingSmall color: Nheko.colors.buttonText Layout.fillWidth: true 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.rightMargin: Nheko.paddingMedium Layout.alignment: Qt.AlignRight | Qt.AlignVCenter hoverEnabled: true width: fontMetrics.font.pixelSize height: fontMetrics.font.pixelSize image: ":/icons/icons/ui/dismiss.svg" ToolTip.visible: closeUnverifiedBubble.hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Close") onClicked: unverifiedStuffBubble.visible = false } } HoverHandler { id: verifyButtonHovered enabled: !closeUnverifiedBubble.hovered acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad } TapHandler { enabled: !closeUnverifiedBubble.hovered acceptedButtons: Qt.LeftButton onSingleTapped: { if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices) SelfVerificationStatus.verifyUnverifiedDevices(); else SelfVerificationStatus.statusChanged(); } } } Rectangle { color: Nheko.theme.separator height: 1 Layout.fillWidth: true visible: unverifiedStuffBubble.visible } } footer: ColumnLayout { spacing: 0 Rectangle { color: Nheko.theme.separator height: 1 Layout.fillWidth: true } Pane { Layout.fillWidth: true Layout.alignment: Qt.AlignBottom Layout.minimumHeight: 40 horizontalPadding: Nheko.paddingMedium verticalPadding: 0 background: Rectangle {color: Nheko.colors.window} contentItem: RowLayout { id: buttonRow ImageButton { Layout.fillWidth: true hoverEnabled: true width: 22 height: 22 image: ":/icons/icons/ui/add-square-button.svg" ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Start a new chat") Layout.margins: Nheko.paddingMedium onClicked: roomJoinCreateMenu.open(parent) Platform.Menu { id: roomJoinCreateMenu Platform.MenuItem { text: qsTr("Join a room") onTriggered: Nheko.openJoinRoomDialog() } Platform.MenuItem { text: qsTr("Create a new room") onTriggered: { var createRoom = createRoomComponent.createObject(timelineRoot); createRoom.show(); timelineRoot.destroyOnClose(createRoom); } } Platform.MenuItem { text: qsTr("Start a direct chat") onTriggered: { var createDirect = createDirectComponent.createObject(timelineRoot); createDirect.show(); timelineRoot.destroyOnClose(createDirect); } } } } ImageButton { visible: !collapsed Layout.fillWidth: true hoverEnabled: true width: 22 height: 22 image: ":/icons/icons/ui/speech-bubbles.svg" ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Room directory") Layout.margins: Nheko.paddingMedium onClicked: { var win = roomDirectoryComponent.createObject(timelineRoot); win.show(); timelineRoot.destroyOnClose(win); } } ImageButton { visible: !collapsed Layout.fillWidth: true hoverEnabled: true ripple: false width: 22 height: 22 image: ":/icons/icons/ui/search.svg" ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Search rooms (Ctrl+K)") Layout.margins: Nheko.paddingMedium onClicked: { var quickSwitch = quickSwitcherComponent.createObject(timelineRoot); quickSwitch.open(); timelineRoot.destroyOnClose(quickSwitch); } } ImageButton { visible: !collapsed Layout.fillWidth: true hoverEnabled: true ripple: false width: 22 height: 22 image: ":/icons/icons/ui/settings.svg" ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("User settings") Layout.margins: Nheko.paddingMedium onClicked: mainWindow.push(userSettingsPage); } } } } }