// SPDX-FileCopyrightText: 2018-2020 Black Hat // SPDX-FileCopyrightText: 2020 Carl Schwan // SPDX-License-Identifier: GPL-3.0-only import QtQuick 2.15 import QtQuick.Controls 2.15 as QQC2 import QtQuick.Layouts 1.15 import org.kde.kirigami 2.15 as Kirigami import org.kde.neochat 1.0 Kirigami.ApplicationWindow { id: root property int columnWidth: Kirigami.Units.gridUnit * 13 minimumWidth: Kirigami.Units.gridUnit * 15 minimumHeight: Kirigami.Units.gridUnit * 15 visible: false // Will be overridden in Component.onCompleted wideScreen: width > columnWidth * 5 pageStack.initialPage: LoadingPage {} pageStack.globalToolBar.canContainHandles: true property RoomListPage roomListPage property bool roomListLoaded: false property RoomPage roomPage Connections { target: root.quitAction function onTriggered() { Qt.quit() } } Loader { active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile source: Qt.resolvedUrl("qrc:/GlobalMenu.qml") } // This timer allows to batch update the window size change to reduce // the io load and also work around the fact that x/y/width/height are // changed when loading the page and overwrite the saved geometry from // the previous session. Timer { id: saveWindowGeometryTimer interval: 1000 onTriggered: Controller.saveWindowGeometry() } Connections { id: saveWindowGeometryConnections enabled: false // Disable on startup to avoid writing wrong values if the window is hidden target: root function onClosing() { Controller.saveWindowGeometry(); } function onWidthChanged() { saveWindowGeometryTimer.restart(); } function onHeightChanged() { saveWindowGeometryTimer.restart(); } function onXChanged() { saveWindowGeometryTimer.restart(); } function onYChanged() { saveWindowGeometryTimer.restart(); } } Loader { id: quickView active: !Kirigami.Settings.isMobile sourceComponent: QuickSwitcher { } } Connections { target: RoomManager function onPushRoom(room, event) { root.roomPage = pageStack.push("qrc:/RoomPage.qml"); root.roomPage.forceActiveFocus(); if (event.length > 0) { roomPage.goToEvent(event); } } function onReplaceRoom(room, event) { const roomItem = pageStack.get(pageStack.depth - 1); pageStack.currentIndex = pageStack.depth - 1; root.roomPage.forceActiveFocus(); if (event.length > 0) { roomItem.goToEvent(event); } } function goToEvent(event) { if (event.length > 0) { roomItem.goToEvent(event); } roomItem.forceActiveFocus(); } function onPushWelcomePage() { // TODO } function onOpenRoomInNewWindow(room) { const secondaryWindow = roomWindow.createObject(undefined, {currentRoom: room}); secondaryWindow.width = root.width - pageStack.get(0).width; secondaryWindow.show(); } function onShowUserDetail(user) { const roomItem = pageStack.get(pageStack.depth - 1); roomItem.showUserDetail(user); } function onAskDirectChatConfirmation(user) { askDirectChatConfirmationComponent.createObject(QQC2.ApplicationWindow.overlay, { user: user, }).open(); } function onWarning(title, message) { if (RoomManager.currentRoom) { const roomItem = pageStack.get(pageStack.depth - 1); roomItem.warning(title, message); } else { showPassiveNotification(i18n("Warning: %1", message)); } } } function pushReplaceLayer(page, args) { if (pageStack.layers.depth === 2) { pageStack.layers.replace(page, args); } else { pageStack.layers.push(page, args); } } contextDrawer: RoomDrawer { id: contextDrawer // This is a memory for all user initiated actions on the drawer, i.e. clicking the button // It is used to ensure that user choice is remembered when changing pages and expanding and contracting the window width property bool drawerUserState: Config.autoRoomInfoDrawer // Connect to the onClicked function of the RoomDrawer handle button Connections { target: contextDrawer.handle.children[0] function onClicked() { contextDrawer.drawerUserState = contextDrawer.drawerOpen } } modal: !root.wideScreen || !enabled onEnabledChanged: drawerOpen = enabled && !modal onModalChanged: { if (Config.autoRoomInfoDrawer) { drawerOpen = !modal && drawerUserState dim = false } } enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3 && (pageStack.visibleItems.length > 1 || pageStack.currentIndex > 0) handleVisible: enabled } readonly property int defaultPageWidth: Kirigami.Units.gridUnit * 17 readonly property int minPageWidth: Kirigami.Units.gridUnit * 10 readonly property int collapsedPageWidth: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3 + (roomListPage.contentItem.QQC2.ScrollBar.vertical.visible ? roomListPage.contentItem.QQC2.ScrollBar.vertical.width : 0) readonly property bool shouldUseSidebars: RoomManager.hasOpenRoom && (Config.roomListPageWidth > minPageWidth ? root.width >= Kirigami.Units.gridUnit * 35 : root.width > Kirigami.Units.gridUnit * 27) && roomListLoaded readonly property int pageWidth: { if (Config.roomListPageWidth === -1) { return defaultPageWidth; } else if (Config.roomListPageWidth < minPageWidth) { return collapsedPageWidth; } else { return Config.roomListPageWidth; } } pageStack.defaultColumnWidth: pageWidth pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar pageStack.globalToolBar.showNavigationButtons: pageStack.currentIndex > 0 ? Kirigami.ApplicationHeaderStyle.ShowBackButton : 0 pageStack.columnView.columnResizeMode: shouldUseSidebars ? Kirigami.ColumnView.FixedColumns : Kirigami.ColumnView.SingleColumn MouseArea { visible: root.pageStack.wideMode z: 500 anchors.top: parent.top anchors.bottom: parent.bottom x: root.pageStack.defaultColumnWidth - (width / 2) width: 2 property int _lastX: -1 enabled: !Kirigami.Settings.isMobile cursorShape: !Kirigami.Settings.isMobile ? Qt.SplitHCursor : undefined onPressed: _lastX = mouseX onReleased: Config.save(); onPositionChanged: { if (_lastX == -1) { return; } if (mouse.x > _lastX) { // we moved to the right if (Config.roomListPageWidth === root.collapsedPageWidth && root.pageWidth + (mouse.x - _lastX) >= root.minPageWidth) { // Here we get back directly to a more wide mode. Config.roomListPageWidth = root.minPageWidth; if (root.width < Kirigami.Units.gridUnit * 35) { root.width = Kirigami.Units.gridUnit * 35; } } else if (Config.roomListPageWidth !== root.collapsedPageWidth) { // Increase page width Config.roomListPageWidth = Math.min(root.defaultPageWidth, root.pageWidth + (mouse.x - _lastX)); } } else if (mouse.x < _lastX) { const tmpWidth = root.pageWidth - (_lastX - mouse.x); if (tmpWidth < root.minPageWidth) { Config.roomListPageWidth = root.collapsedPageWidth; } else { Config.roomListPageWidth = tmpWidth; } } } } globalDrawer: Kirigami.GlobalDrawer { property bool hasLayer contentItem.implicitWidth: columnWidth isMenu: true actions: [ Kirigami.Action { text: i18n("Explore rooms") icon.name: "compass" onTriggered: pushReplaceLayer("qrc:/JoinRoomPage.qml", {connection: Controller.activeConnection}) enabled: pageStack.layers.depth === 1 && Controller.accountCount > 0 }, Kirigami.Action { text: i18n("Start a Chat") icon.name: "irc-join-channel" onTriggered: pushReplaceLayer("qrc:/StartChatPage.qml", {connection: Controller.activeConnection}) enabled: pageStack.layers.depth === 1 && Controller.accountCount > 0 }, Kirigami.Action { text: i18n("Create a Room") icon.name: "irc-join-channel" onTriggered: { let dialog = createRoomDialog.createObject(root.overlay); dialog.open(); } shortcut: StandardKey.New enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0 }, Kirigami.Action { text: i18n("Configure NeoChat...") icon.name: "settings-configure" onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {}, { title: i18n("Configure") }) enabled: pageStack.layers.depth === 1 shortcut: StandardKey.Preferences }, Kirigami.Action { text: i18n("Logout") icon.name: "list-remove-user" enabled: Controller.accountCount > 0 onTriggered: confirmLogoutDialog.open() }, Kirigami.Action { text: i18n("Quit") icon.name: "gtk-quit" shortcut: StandardKey.Quit onTriggered: Qt.quit() } ] } ConfirmLogoutDialog { id: confirmLogoutDialog } Component.onCompleted: { Controller.setBlur(pageStack, Config.blur && !Config.compactLayout); if (Config.minimizeToSystemTrayOnStartup && !Kirigami.Settings.isMobile && Controller.supportSystemTray && Config.systemTray) { restoreWindowGeometryConnections.enabled = true; // To restore window size and position } else { visible = true; saveWindowGeometryConnections.enabled = true; } } Connections { target: Config function onBlurChanged() { Controller.setBlur(pageStack, Config.blur && !Config.compactLayout); } function onCompactLayoutChanged() { Controller.setBlur(pageStack, Config.blur && !Config.compactLayout); } } // blur effect color: Config.blur && !Config.compactLayout ? "transparent" : Kirigami.Theme.backgroundColor // we need to apply the translucency effect separately on top of the color background: Rectangle { color: Config.blur && !Config.compactLayout ? Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 1 - Config.transparency) : "transparent" } Component { id: roomListComponent RoomListPage { id: roomList Shortcut { sequences: ["Ctrl+PgUp", "Ctrl+Backtab"] onActivated: { roomList.goToPreviousRoom(); } } Shortcut { sequences: ["Ctrl+PgDown", "Ctrl+Tab"] onActivated: { roomList.goToNextRoom(); } } } } Connections { target: AccountRegistry function onRowsRemoved() { if (AccountRegistry.rowCount() === 0) { RoomManager.reset(); pageStack.clear(); roomListLoaded = false; pageStack.push("qrc:/WelcomePage.qml"); } } } Connections { target: Controller function onInitiated() { if (Controller.accountCount === 0) { pageStack.replace("qrc:/WelcomePage.qml", {}); } else if (!roomListLoaded) { pageStack.replace(roomListComponent, { activeConnection: Controller.activeConnection }); roomListLoaded = true; roomListPage = pageStack.currentItem RoomManager.loadInitialRoom(); } } function onGlobalErrorOccured(error, detail) { showPassiveNotification(i18n("%1: %2", error, detail)); } function onUserConsentRequired(url) { consentSheet.url = url consentSheet.open() } } Connections { id: restoreWindowGeometryConnections enabled: false target: root function onVisibleChanged() { if (!visible) { return; } Controller.restoreWindowGeometry(root); restoreWindowGeometryConnections.enabled = false; // Only restore window geometry for the first time saveWindowGeometryConnections.enabled = true; } } Component { id: keyVerificationDialogComponent KeyVerificationDialog { } } Connections { target: Controller.activeConnection //TODO Remove this when the E2EE flag in libQuotient goes away ignoreUnknownSignals: true function onDirectChatAvailable(directChat) { RoomManager.enterRoom(Controller.activeConnection.room(directChat.id)); } function onNewKeyVerificationSession(session) { applicationWindow().pageStack.pushDialogLayer(keyVerificationDialogComponent, { session: session, }, { title: i18nc("@title:window", "Session Verification") }); } } Kirigami.OverlaySheet { id: consentSheet property string url: "" title: i18n("User consent") QQC2.Label { id: label text: i18n("Your homeserver requires you to agree to its terms and conditions before being able to use it. Please click the button below to read them.") wrapMode: Text.WordWrap width: parent.width } footer: QQC2.Button { text: i18n("Open") onClicked: UrlHelper.openUrl(consentSheet.url) } } Component { id: createRoomDialog CreateRoomDialog {} } Component { id: roomWindow RoomWindow {} } Component { id: userDialog UserDetailDialog {} } Component { id: askDirectChatConfirmationComponent Kirigami.OverlaySheet { id: askDirectChatConfirmation required property var user; parent: QQC2.ApplicationWindow.overlay title: i18n("Start a chat") contentItem: QQC2.Label { text: i18n("Do you want to start a chat with %1?", user.displayName) wrapMode: Text.WordWrap } footer: QQC2.DialogButtonBox { standardButtons: QQC2.DialogButtonBox.Ok | QQC2.DialogButtonBox.Cancel onAccepted: { user.requestDirectChat(); askDirectChatConfirmation.close(); } onRejected: askDirectChatConfirmation.close(); } } } property Item hoverLinkIndicator: QQC2.Control { parent: overlay.parent property string text opacity: linkText.text.length > 0 ? 1 : 0 z: 20 x: 0 y: parent.height - implicitHeight contentItem: QQC2.Label { id: linkText text: parent.text.startsWith("https://matrix.to/") ? "" : parent.text } Kirigami.Theme.colorSet: Kirigami.Theme.View background: Rectangle { color: Kirigami.Theme.backgroundColor } } }