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 @@
+