diff --git a/TODO.md b/TODO.md index 00f00db6..a66bd10f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,11 @@ # TODO -- Image viewer: - - hflickable: support kinetic scrolling disabler +- fallback page +- SSO page +- sever list +- cursor shape in HBox/HTabbedBox pages over fields +- login with account already added → infinite spinner in room list +- verify onKeyboardAccept/Cancel things respect button enabled state - global presence control diff --git a/src/backend/backend.py b/src/backend/backend.py index c5a6e0b7..d37f29c4 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -6,14 +6,14 @@ import os import sys import traceback from pathlib import Path -from typing import Any, DefaultDict, Dict, List, Optional +from typing import Any, DefaultDict, Dict, List, Optional, Tuple from appdirs import AppDirs import nio from . import __app_name__ -from .errors import MatrixError +from .errors import MatrixError, MatrixForbidden, MatrixNotFound from .matrix_client import MatrixClient from .media_cache import MediaCache from .models import SyncId @@ -129,6 +129,30 @@ class Backend: # Clients management + async def server_info(self, homeserver: str) -> Tuple[str, List[str]]: + """Return server's real URL and supported login flows. + + Retrieving the real URL uses the `.well-known` API. + Possible login methods include `m.login.password` or `m.login.sso`. + """ + + client = MatrixClient(self, homeserver=homeserver) + + try: + homeserver = (await client.discovery_info()).homeserver_url + except (MatrixNotFound, MatrixForbidden): + # This is either already the real URL, or an invalid URL. + pass + else: + await client.close() + client = MatrixClient(self, homeserver=homeserver) + + try: + return (homeserver, (await client.login_info()).flows) + finally: + await client.close() + + async def login_client(self, user: str, password: str, diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index a84ffd04..11d7efbc 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -152,11 +152,13 @@ class MatrixClient(nio.AsyncClient): } - def __init__(self, - backend, - user: str, - homeserver: str = "https://matrix.org", - device_id: Optional[str] = None) -> None: + def __init__( + self, + backend, + user: str = "", + homeserver: str = "https://matrix.org", + device_id: Optional[str] = None, + ) -> None: if not urlparse(homeserver).scheme: raise ValueError( diff --git a/src/gui/Base/HBox.qml b/src/gui/Base/HBox.qml index 56a665ed..4fd87cbd 100644 --- a/src/gui/Base/HBox.qml +++ b/src/gui/Base/HBox.qml @@ -5,7 +5,6 @@ import QtQuick.Layouts 1.12 HFlickableColumnPage { implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth) - implicitHeight: Math.min(parent.height, flickable.contentHeight) background: Rectangle { color: theme.controls.box.background diff --git a/src/gui/Base/HFlickableColumnPage.qml b/src/gui/Base/HFlickableColumnPage.qml index 3730c02f..0913ba1a 100644 --- a/src/gui/Base/HFlickableColumnPage.qml +++ b/src/gui/Base/HFlickableColumnPage.qml @@ -18,6 +18,7 @@ HPage { implicitWidth: theme.controls.box.defaultWidth + implicitHeight: contentHeight + implicitHeaderHeight + implicitFooterHeight contentHeight: flickable.contentHeight + flickable.topMargin + flickable.bottomMargin diff --git a/src/gui/Pages/AddAccount/AddAccount.qml b/src/gui/Pages/AddAccount/AddAccount.qml index f8c51d2c..342398d4 100644 --- a/src/gui/Pages/AddAccount/AddAccount.qml +++ b/src/gui/Pages/AddAccount/AddAccount.qml @@ -4,22 +4,48 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" -HPage { - id: page +HSwipeView { + id: swipeView + clip: true + interactive: currentIndex !== 0 || signIn.serverUrl + onCurrentItemChanged: if (currentIndex === 0) serverBrowser.takeFocus() + Component.onCompleted: serverBrowser.takeFocus() - HTabbedBox { - anchors.centerIn: parent - width: Math.min(implicitWidth, page.availableWidth) - height: Math.min(implicitHeight, page.availableHeight) + HPage { + id: serverPage - header: HTabBar { - HTabButton { text: qsTr("Sign in") } - HTabButton { text: qsTr("Register") } - HTabButton { text: qsTr("Reset") } + ServerBrowser { + id: serverBrowser + anchors.centerIn: parent + width: Math.min(implicitWidth, serverPage.availableWidth) + height: Math.min(implicitHeight, serverPage.availableHeight) + onAccepted: swipeView.currentIndex = 1 } + } - SignIn {} - Register {} - Reset {} + HPage { + id: tabPage + + HTabbedBox { + anchors.centerIn: parent + width: Math.min(implicitWidth, tabPage.availableWidth) + height: Math.min(implicitHeight, tabPage.availableHeight) + + header: HTabBar { + HTabButton { text: qsTr("Sign in") } + HTabButton { text: qsTr("Register") } + HTabButton { text: qsTr("Reset") } + } + + SignIn { + id: signIn + serverUrl: serverBrowser.acceptedUrl + displayUrl: serverBrowser.acceptedUserUrl + onExitRequested: swipeView.currentIndex = 0 + } + + Register {} + Reset {} + } } } diff --git a/src/gui/Pages/AddAccount/ServerBrowser.qml b/src/gui/Pages/AddAccount/ServerBrowser.qml new file mode 100644 index 00000000..99d13af1 --- /dev/null +++ b/src/gui/Pages/AddAccount/ServerBrowser.qml @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../../Base" +import "../../Base/Buttons" +import "../../PythonBridge" + +HBox { + id: page + + property string acceptedUserUrl: "" + property string acceptedUrl: "" + property var loginFlows: ["m.login.password"] + + property string saveName: "serverBrowser" + property var saveProperties: ["acceptedUserUrl"] + property Future connectFuture: null + + signal accepted() + + function takeFocus() { serverField.item.forceActiveFocus() } + + function connect() { + if (connectFuture) connectFuture.cancel() + connectTimeout.restart() + + const args = [serverField.item.cleanText] + + connectFuture = py.callCoro("server_info", args, ([url, flows]) => { + connectTimeout.stop() + errorMessage.text = "" + + connectFuture = null + acceptedUrl = url + acceptedUserUrl = args[0] + loginFlows = flows + accepted() + + }, (type, args, error, traceback, uuid) => { + connectTimeout.stop() + connectFuture = null + + let text = qsTr("Unexpected error: %1 [%2]").arg(type).arg(args) + + type === "MatrixNotFound" ? + text = qsTr("Invalid homeserver address") : + + type.startsWith("Matrix") ? + text = qsTr("Error contacting server: %1").arg(type) : + + utils.showError(type, traceback, uuid) + + errorMessage.text = text + }) + } + + function cancel() { + if (page.connectFuture) return + + connectTimeout.stop() + connectFuture.cancel() + connectFuture = null + } + + + footer: AutoDirectionLayout { + ApplyButton { + id: applyButton + enabled: serverField.item.cleanText && ! serverField.item.error + text: qsTr("Connect") + icon.name: "server-connect" + loading: page.connectFuture !== null + disableWhileLoading: false + onClicked: page.connect() + } + + CancelButton { + id: cancelButton + enabled: page.connectFuture !== null + onClicked: page.cancel() + } + } + + onKeyboardAccept: if (applyButton.enabled) page.connect() + onKeyboardCancel: if (cancelButton.enabled) page.cancel() + onAccepted: window.saveState(this) + + Timer { + id: connectTimeout + interval: 30 * 1000 + onTriggered: { + errorMessage.text = + serverField.knownServerChosen ? + + qsTr("This homeserver seems unavailable. Verify your inter" + + "net connection or try again in a few minutes.") : + + qsTr("This homeserver seems unavailable. Verify the " + + "entered address, your internet connection or try " + + "again in a few minutes.") + } + } + + HLabeledItem { + id: serverField + + // 2019-11-11 https://www.hello-matrix.net/public_servers.php + readonly property var knownServers: [ + "https://matrix.org", + "https://chat.weho.st", + "https://tchncs.de", + "https://chat.privacytools.io", + "https://hackerspaces.be", + "https://matrix.allmende.io", + "https://feneas.org", + "https://junta.pl", + "https://perthchat.org", + "https://matrix.tedomum.net", + "https://converser.eu", + "https://ru-matrix.org", + "https://matrix.sibnsk.net", + "https://alternanet.fr", + ] + + readonly property bool knownServerChosen: + knownServers.includes(item.cleanText) + + label.text: qsTr("Homeserver:") + + Layout.fillWidth: true + + HTextField { + readonly property string cleanText: text.toLowerCase().trim() + + width: parent.width + error: ! /https?:\/\/.+/.test(cleanText) + defaultText: + window.getState(page, "acceptedUserUrl", "https://matrix.org") + } + } + + HLabel { + id: errorMessage + wrapMode: HLabel.Wrap + horizontalAlignment: Text.AlignHCenter + color: theme.colors.errorText + + visible: Layout.maximumHeight > 0 + Layout.maximumHeight: text ? implicitHeight : 0 + Behavior on Layout.maximumHeight { HNumberAnimation {} } + + Layout.fillWidth: true + } +} diff --git a/src/gui/Pages/AddAccount/SignIn.qml b/src/gui/Pages/AddAccount/SignIn.qml index e3746e56..1fface65 100644 --- a/src/gui/Pages/AddAccount/SignIn.qml +++ b/src/gui/Pages/AddAccount/SignIn.qml @@ -8,33 +8,42 @@ import "../../Base/Buttons" HFlickableColumnPage { id: page + enum Security { Insecure, LocalHttp, Secure } + + property string serverUrl + property string displayUrl: serverUrl property var loginFuture: null - property string signInWith: "username" + signal exitRequested() - readonly property bool canSignIn: - serverField.item.text.trim() && idField.item.text.trim() && - passwordField.item.text && ! serverField.item.error + readonly property int security: + serverUrl.startsWith("https://") ? + SignIn.Security.Secure : + + ["//localhost", "//127.0.0.1", "//:1"].includes( + serverUrl.split(":")[1], + ) ? + SignIn.Security.LocalHttp : + + SignIn.Security.Insecure function takeFocus() { idField.item.forceActiveFocus() } function signIn() { if (page.loginFuture) page.loginFuture.cancel() - signInTimeout.restart() - errorMessage.text = "" const args = [ idField.item.text.trim(), passwordField.item.text, - undefined, serverField.item.text.trim(), + undefined, page.serverUrl, ] page.loginFuture = py.callCoro("login_client", args, userId => { - signInTimeout.stop() errorMessage.text = "" page.loginFuture = null + print(rememberAccount.checked) py.callCoro( rememberAccount.checked ? "saved_accounts.add": "saved_accounts.delete", @@ -48,7 +57,6 @@ HFlickableColumnPage { }, (type, args, error, traceback, uuid) => { page.loginFuture = null - signInTimeout.stop() let txt = qsTr( "Invalid request, login type or unknown error: %1", @@ -67,17 +75,23 @@ HFlickableColumnPage { } function cancel() { - if (! page.loginFuture) return + if (! page.loginFuture) { + page.exitRequested() + return + } - signInTimeout.stop() page.loginFuture.cancel() page.loginFuture = null } + flickable.topMargin: theme.spacing * 1.5 + flickable.bottomMargin: flickable.topMargin + footer: AutoDirectionLayout { ApplyButton { - enabled: page.canSignIn + id: applyButton + enabled: idField.item.text.trim() && passwordField.item.text text: qsTr("Sign in") icon.name: "sign-in" loading: page.loginFuture !== null @@ -90,54 +104,39 @@ HFlickableColumnPage { } } - onKeyboardAccept: page.signIn() + onKeyboardAccept: if (applyButton.enabled) page.signIn() onKeyboardCancel: page.cancel() - Timer { - id: signInTimeout - interval: 30 * 1000 - onTriggered: { - errorMessage.text = - serverField.knownServerChosen ? + HButton { + icon.name: "sign-in-" + ( + page.security === SignIn.Security.Insecure ? "insecure" : + page.security === SignIn.Security.LocalHttp ? "local-http" : + "secure" + ) - qsTr("This server seems unavailable. Verify your inter" + - "net connection or try again in a few minutes.") : + icon.color: + page.security === SignIn.Security.Insecure ? + theme.colors.negativeBackground : - qsTr("This server seems unavailable. Verify the " + - "entered URL, your internet connection or try " + - "again in a few minutes.") - } - } + page.security === SignIn.Security.LocalHttp ? + theme.colors.middleBackground : - HRowLayout { - visible: false // TODO - spacing: theme.spacing * 1.25 - Layout.alignment: Qt.AlignHCenter + theme.colors.positiveBackground - Layout.topMargin: theme.spacing - Layout.bottomMargin: Layout.topMargin + text: + page.security === SignIn.Security.Insecure ? + page.serverUrl : + page.displayUrl.replace(/^(https?:\/\/)?(www\.)?/, "") - Repeater { - model: ["username", "email", "phone"] + onClicked: page.exitRequested() - HButton { - icon.name: modelData - circle: true - checked: page.signInWith === modelData - enabled: modelData === "username" - autoExclusive: true - onClicked: page.signInWith = modelData - } - } + Layout.alignment: Qt.AlignCenter + Layout.maximumWidth: parent.width } HLabeledItem { id: idField - label.text: qsTr( - page.signInWith === "email" ? "Email:" : - page.signInWith === "phone" ? "Phone:" : - "Username:" - ) + label.text: qsTr("Username:") Layout.fillWidth: true @@ -158,43 +157,6 @@ HFlickableColumnPage { } } - HLabeledItem { - id: serverField - - // 2019-11-11 https://www.hello-matrix.net/public_servers.php - readonly property var knownServers: [ - "https://matrix.org", - "https://chat.weho.st", - "https://tchncs.de", - "https://chat.privacytools.io", - "https://hackerspaces.be", - "https://matrix.allmende.io", - "https://feneas.org", - "https://junta.pl", - "https://perthchat.org", - "https://matrix.tedomum.net", - "https://converser.eu", - "https://ru-matrix.org", - "https://matrix.sibnsk.net", - "https://alternanet.fr", - ] - - readonly property bool knownServerChosen: - knownServers.includes(item.cleanText) - - label.text: qsTr("Homeserver:") - - Layout.fillWidth: true - - HTextField { - readonly property string cleanText: text.toLowerCase().trim() - - width: parent.width - text: "https://matrix.org" - error: ! /.+:\/\/.+/.test(cleanText) - } - } - HCheckBox { id: rememberAccount checked: true @@ -206,7 +168,6 @@ HFlickableColumnPage { Layout.fillWidth: true Layout.topMargin: theme.spacing / 2 - Layout.bottomMargin: Layout.topMargin } HLabel { diff --git a/src/gui/Utils.qml b/src/gui/Utils.qml index 7c5815cd..2fea6921 100644 --- a/src/gui/Utils.qml +++ b/src/gui/Utils.qml @@ -440,6 +440,7 @@ QtObject { flickable.maximumFlickVelocity = 5000 + flickable.flickDeceleration = Math.max( goFaster ? normalDecel : -Infinity, Math.abs(normalDecel * magicNumber * pages), diff --git a/src/icons/thin/server-connect.svg b/src/icons/thin/server-connect.svg new file mode 100644 index 00000000..c5d41de2 --- /dev/null +++ b/src/icons/thin/server-connect.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/sign-in-insecure.svg b/src/icons/thin/sign-in-insecure.svg new file mode 100644 index 00000000..e2f983ab --- /dev/null +++ b/src/icons/thin/sign-in-insecure.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/sign-in-local-http.svg b/src/icons/thin/sign-in-local-http.svg new file mode 100644 index 00000000..e2f983ab --- /dev/null +++ b/src/icons/thin/sign-in-local-http.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/thin/sign-in-secure.svg b/src/icons/thin/sign-in-secure.svg new file mode 100644 index 00000000..198a8504 --- /dev/null +++ b/src/icons/thin/sign-in-secure.svg @@ -0,0 +1,3 @@ + + +