1// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org> 2// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu> 3// SPDX-License-Identifier: GPL-3.0-only 4 5import QtQuick 2.15 6import QtQuick.Controls 2.15 as QQC2 7import QtQuick.Layouts 1.15 8 9import org.kde.kirigami 2.15 as Kirigami 10 11import org.kde.neochat 1.0 12import NeoChat.Component 1.0 13import NeoChat.Dialog 1.0 14import NeoChat.Page 1.0 15import NeoChat.Panel 1.0 16 17Kirigami.ApplicationWindow { 18 id: root 19 20 property int columnWidth: Kirigami.Units.gridUnit * 13 21 22 minimumWidth: Kirigami.Units.gridUnit * 15 23 minimumHeight: Kirigami.Units.gridUnit * 20 24 25 wideScreen: width > columnWidth * 5 26 27 onClosing: Controller.saveWindowGeometry(root) 28 29 pageStack.initialPage: LoadingPage {} 30 31 property bool roomListLoaded: false 32 33 Connections { 34 target: root.quitAction 35 function onTriggered() { 36 Qt.quit() 37 } 38 } 39 40 // This timer allows to batch update the window size change to reduce 41 // the io load and also work around the fact that x/y/width/height are 42 // changed when loading the page and overwrite the saved geometry from 43 // the previous session. 44 Timer { 45 id: saveWindowGeometryTimer 46 interval: 1000 47 onTriggered: Controller.saveWindowGeometry(root) 48 } 49 50 onWidthChanged: saveWindowGeometryTimer.restart() 51 onHeightChanged: saveWindowGeometryTimer.restart() 52 onXChanged: saveWindowGeometryTimer.restart() 53 onYChanged: saveWindowGeometryTimer.restart() 54 55 56 /// Setup keyboard navigation to the room page. 57 function connectRoomToSignal(item) { 58 if (!roomListLoaded) { 59 console.log("Should not happen: no room list page but room page"); 60 } 61 const roomList = pageStack.get(0); 62 item.switchRoomUp.connect(function() { 63 roomList.goToNextRoom(); 64 }); 65 66 item.switchRoomDown.connect(function() { 67 roomList.goToPreviousRoom(); 68 }); 69 item.forceActiveFocus(); 70 item.KeyNavigation.left = pageStack.get(0); 71 } 72 73 Connections { 74 target: RoomManager 75 76 function onPushRoom(room, event) { 77 const roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml"); 78 connectRoomToSignal(roomItem); 79 if (event.length > 0) { 80 roomItem.goToEvent(event); 81 } 82 } 83 84 function onReplaceRoom(room, event) { 85 const roomItem = pageStack.get(pageStack.depth - 1); 86 pageStack.currentIndex = pageStack.depth - 1; 87 connectRoomToSignal(roomItem); 88 if (event.length > 0) { 89 roomItem.goToEvent(event); 90 } 91 } 92 93 function goToEvent(event) { 94 if (event.length > 0) { 95 roomItem.goToEvent(event); 96 } 97 roomItem.forceActiveFocus(); 98 } 99 100 function onPushWelcomePage() { 101 // TODO 102 } 103 104 function onOpenRoomInNewWindow(room) { 105 const secondayWindow = roomWindow.createObject(applicationWindow(), {currentRoom: room}); 106 secondayWindow.width = root.width - pageStack.get(0).width; 107 secondayWindow.show(); 108 } 109 110 function onShowUserDetail(user) { 111 const roomItem = pageStack.get(pageStack.depth - 1); 112 roomItem.showUserDetail(user); 113 } 114 115 function onAskDirectChatConfirmation(user) { 116 askDirectChatConfirmationComponent.createObject(QQC2.ApplicationWindow.overlay, { 117 user: user, 118 }).open(); 119 } 120 121 function onWarning(title, message) { 122 if (RoomManager.currentRoom) { 123 const roomItem = pageStack.get(pageStack.depth - 1); 124 roomItem.warning(title, message); 125 } else { 126 showPassiveNotification(i18n("Warning: %1", message)); 127 } 128 } 129 } 130 131 function pushReplaceLayer(page, args) { 132 if (pageStack.layers.depth === 2) { 133 pageStack.layers.replace(page, args); 134 } else { 135 pageStack.layers.push(page, args); 136 } 137 } 138 139 function showWindow() { 140 root.show() 141 root.raise() 142 root.requestActivate() 143 } 144 145 contextDrawer: RoomDrawer { 146 id: contextDrawer 147 contentItem.implicitWidth: columnWidth 148 edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge 149 modal: !root.wideScreen || !enabled 150 onEnabledChanged: drawerOpen = enabled && !modal 151 onModalChanged: drawerOpen = !modal 152 enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3 153 handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3 154 } 155 156 pageStack.columnView.columnWidth: Kirigami.Units.gridUnit * 17 157 158 globalDrawer: Kirigami.GlobalDrawer { 159 property bool hasLayer 160 contentItem.implicitWidth: columnWidth 161 isMenu: true 162 actions: [ 163 Kirigami.Action { 164 text: i18n("Explore rooms") 165 icon.name: "compass" 166 onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/JoinRoomPage.qml", {"connection": Controller.activeConnection}) 167 enabled: pageStack.layers.currentItem.title !== i18n("Explore Rooms") && Controller.accountCount > 0 168 }, 169 Kirigami.Action { 170 text: i18n("Start a Chat") 171 icon.name: "irc-join-channel" 172 onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/StartChatPage.qml", {"connection": Controller.activeConnection}) 173 enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0 174 }, 175 Kirigami.Action { 176 text: i18n("Create a Room") 177 icon.name: "irc-join-channel" 178 onTriggered: { 179 let dialog = createRoomDialog.createObject(root.overlay); 180 dialog.open(); 181 } 182 shortcut: StandardKey.New 183 enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0 184 }, 185 Kirigami.Action { 186 text: i18n("Accounts") 187 icon.name: "im-user" 188 onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/AccountsPage.qml") 189 enabled: pageStack.layers.currentItem.title !== i18n("Accounts") && Controller.accountCount > 0 190 191 }, 192 Kirigami.Action { 193 text: i18n("Devices") 194 iconName: "network-connect" 195 onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/DevicesPage.qml") 196 enabled: pageStack.layers.currentItem.title !== i18n("Devices") && Controller.accountCount > 0 197 }, 198 Kirigami.Action { 199 text: i18n("Settings") 200 icon.name: "settings-configure" 201 onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml") 202 enabled: pageStack.layers.currentItem.title !== i18n("Settings") 203 shortcut: StandardKey.Preferences 204 }, 205 Kirigami.Action { 206 text: i18n("About NeoChat") 207 icon.name: "help-about" 208 onTriggered: pushReplaceLayer(aboutPage) 209 enabled: pageStack.layers.currentItem.title !== i18n("About") 210 }, 211 Kirigami.Action { 212 text: i18n("Logout") 213 icon.name: "list-remove-user" 214 enabled: Controller.accountCount > 0 215 onTriggered: Controller.logout(Controller.activeConnection, true) 216 }, 217 Kirigami.Action { 218 text: i18n("Quit") 219 icon.name: "gtk-quit" 220 shortcut: StandardKey.Quit 221 onTriggered: Qt.quit() 222 } 223 ] 224 } 225 226 Component { 227 id: aboutPage 228 Kirigami.AboutPage { 229 aboutData: Controller.aboutData 230 } 231 } 232 233 Component { 234 id: roomListComponent 235 RoomListPage { 236 id: roomList 237 } 238 } 239 Connections { 240 target: LoginHelper 241 function onInitialSyncFinished() { 242 pageStack.replace(roomListComponent, { 243 activeConnection: Controller.activeConnection 244 }); 245 roomListLoaded = true; 246 RoomManager.loadInitialRoom(); 247 } 248 } 249 250 Connections { 251 target: Controller 252 253 function onInitiated() { 254 if (RoomManager.hasOpenRoom) { 255 return; 256 } 257 if (Controller.accountCount === 0) { 258 pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {}); 259 } else { 260 pageStack.replace(roomListComponent, { 261 activeConnection: Controller.activeConnection 262 }); 263 roomListLoaded = true; 264 RoomManager.loadInitialRoom(); 265 } 266 } 267 268 function onBusyChanged() { 269 if(!Controller.busy && roomListLoaded === false) { 270 pageStack.replace(roomListComponent); 271 roomListLoaded = true; 272 } 273 } 274 275 function onConnectionDropped() { 276 if (Controller.accountCount === 0) { 277 RoomManager.reset(); 278 pageStack.clear(); 279 roomListLoaded = false; 280 pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml"); 281 } 282 } 283 284 function onGlobalErrorOccured(error, detail) { 285 showPassiveNotification(i18nc("%1: %2", error, detail)); 286 } 287 288 function onShowWindow() { 289 root.showWindow() 290 } 291 292 function onUserConsentRequired(url) { 293 consentSheet.url = url 294 consentSheet.open() 295 } 296 } 297 298 Connections { 299 target: Controller.activeConnection 300 onDirectChatAvailable: { 301 RoomManager.enterRoom(Controller.activeConnection.room(directChat.id)); 302 } 303 } 304 305 Kirigami.OverlaySheet { 306 id: consentSheet 307 308 property string url: "" 309 310 header: Kirigami.Heading { 311 text: i18n("User consent") 312 } 313 314 QQC2.Label { 315 id: label 316 317 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.") 318 wrapMode: Text.WordWrap 319 width: parent.width 320 } 321 footer: QQC2.Button { 322 text: i18n("Open") 323 onClicked: Qt.openUrlExternally(consentSheet.url) 324 } 325 } 326 327 Component { 328 id: createRoomDialog 329 330 CreateRoomDialog {} 331 } 332 333 Component { 334 id: roomWindow 335 RoomWindow {} 336 } 337 338 Component { 339 id: userDialog 340 UserDetailDialog {} 341 } 342 343 Component { 344 id: askDirectChatConfirmationComponent 345 346 Kirigami.OverlaySheet { 347 id: askDirectChatConfirmation 348 required property var user; 349 350 parent: QQC2.ApplicationWindow.overlay 351 header: Kirigami.Heading { 352 text: i18n("Start a chat") 353 } 354 contentItem: QQC2.Label { 355 text: i18n("Do you want to start a chat with %1?", user.displayName) 356 wrapMode: Text.WordWrap 357 } 358 footer: QQC2.DialogButtonBox { 359 standardButtons: QQC2.DialogButtonBox.Ok | QQC2.DialogButtonBox.Cancel 360 onAccepted: { 361 user.requestDirectChat(); 362 askDirectChatConfirmation.close(); 363 } 364 onRejected: askDirectChatConfirmation.close(); 365 } 366 } 367 } 368 369 property Item hoverLinkIndicator: QQC2.Control { 370 parent: overlay.parent 371 property alias text: linkText.text 372 opacity: text.length > 0 ? 1 : 0 373 374 z: 20 375 x: 0 376 y: parent.height - implicitHeight 377 contentItem: QQC2.Label { 378 id: linkText 379 } 380 Kirigami.Theme.colorSet: Kirigami.Theme.View 381 background: Rectangle { 382 color: Kirigami.Theme.backgroundColor 383 } 384 } 385} 386