[fractal/fractal-next] login: Add auto-discovery of homeserver



commit f4611d73bbbc91b5af04ee6fe12ad64ee1acd101
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Tue Feb 1 14:24:39 2022 +0100

    login: Add auto-discovery of homeserver
    
    Also check if the url provided is a valid homeserver.
    
    Closes #769

 data/resources/assets/homeserver.svg       |  62 ++++++
 data/resources/resources.gresource.xml     |   2 +
 data/resources/style.css                   |   6 +-
 data/resources/ui/login-advanced-dialog.ui |  32 +++
 data/resources/ui/login.ui                 | 190 +++++++++++-----
 po/POTFILES.in                             |   2 +
 src/login.rs                               | 339 ++++++++++++++++++++++++++---
 src/login_advanced_dialog.rs               | 118 ++++++++++
 src/main.rs                                |   1 +
 src/session/mod.rs                         |  14 +-
 10 files changed, 677 insertions(+), 89 deletions(-)
---
diff --git a/data/resources/assets/homeserver.svg b/data/resources/assets/homeserver.svg
new file mode 100644
index 000000000..4d0940cf7
--- /dev/null
+++ b/data/resources/assets/homeserver.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="340" height="200" version="1.1" viewBox="0 0 340 200" xmlns="http://www.w3.org/2000/svg"; 
xmlns:cc="http://creativecommons.org/ns#"; xmlns:dc="http://purl.org/dc/elements/1.1/"; 
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"; xmlns:xlink="http://www.w3.org/1999/xlink";>
+ <defs>
+  <linearGradient id="linearGradient7724">
+   <stop stop-color="#bbd5f5" offset="0"/>
+   <stop stop-color="#dfecfb" offset="1"/>
+  </linearGradient>
+  <linearGradient id="linearGradient7358" x1="-2987.1" x2="-2872.9" y1="521.67" y2="521.67" 
gradientUnits="userSpaceOnUse">
+   <stop stop-color="#a51d2d" offset="0"/>
+   <stop stop-color="#ed333b" offset=".061917"/>
+   <stop stop-color="#c01c28" offset=".11006"/>
+   <stop stop-color="#c01c28" offset=".89423"/>
+   <stop stop-color="#ed333b" offset=".94682"/>
+   <stop stop-color="#a51d2d" offset="1"/>
+  </linearGradient>
+  <linearGradient id="linearGradient7718" x1="-2960" x2="-2960" y1="613" y2="571.13" 
gradientTransform="matrix(1.0132 0 0 1.0132 3866.4 -1038.4)" gradientUnits="userSpaceOnUse" 
xlink:href="#linearGradient7724"/>
+  <linearGradient id="linearGradient7739" x1="-2845" x2="-2845" y1="548" y2="513" 
gradientTransform="matrix(1.0132 0 0 1.0132 3866.4 -1038.4)" gradientUnits="userSpaceOnUse" 
xlink:href="#linearGradient7724"/>
+  <linearGradient id="linearGradient7752" x1="-3020" x2="-3020" y1="538" y2="493" 
gradientTransform="matrix(1.0132 0 0 1.0132 3866.4 -1038.4)" gradientUnits="userSpaceOnUse" 
xlink:href="#linearGradient7724"/>
+  <linearGradient id="linearGradient7765" x1="-2885" x2="-2885" y1="483" y2="453" 
gradientTransform="matrix(1.0132 0 0 1.0132 3866.4 -1038.4)" gradientUnits="userSpaceOnUse" 
xlink:href="#linearGradient7724"/>
+ </defs>
+ <metadata>
+  <rdf:RDF>
+   <cc:Work rdf:about="">
+    <dc:format>image/svg+xml</dc:format>
+    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+   </cc:Work>
+  </rdf:RDF>
+ </metadata>
+ <g transform="translate(-726.5 598.35)">
+  <g>
+   <path d="m806.58-554.07a25.329 25.329 0 0 0-25.329 25.329 25.329 25.329 0 0 0 0.0233 0.67479 20.263 
20.263 0 0 0-5.0896-0.67479 20.263 20.263 0 0 0-20.263 20.263 20.263 20.263 0 0 0 20.263 
20.263h151.98v-20.263h-63.058a24.063 24.063 0 0 0 2.2678-10.132 24.063 24.063 0 0 0-24.063-24.063 24.063 
24.063 0 0 0-13.409 4.1061 25.329 25.329 0 0 0-23.319-15.504z" fill="url(#linearGradient7752)"/>
+   <path d="m987.94-523.68a22.796 22.796 0 0 0-20.384 12.609 17.73 17.73 0 0 0-8.9998-2.4775 17.73 17.73 0 0 
0-17.533 15.198h-28.06v20.263h106.38a17.73 17.73 0 0 0 17.73-17.73 17.73 17.73 0 0 0-17.73-17.73 17.73 17.73 
0 0 0-10.547 3.5045 22.796 22.796 0 0 0-20.859-13.636z" fill="url(#linearGradient7739)"/>
+   <path d="m902.83-478.08a29.762 29.762 0 0 0-29.192 24.011 22.163 22.163 0 0 0-17.667-8.8138 22.163 22.163 
0 0 0-22.068 20.263h-17.192a15.198 15.198 0 0 0-15.198 15.198 15.198 15.198 0 0 0 15.198 15.198h136.78a27.862 
27.862 0 0 0 27.862-27.862 27.862 27.862 0 0 0-27.862-27.862 27.862 27.862 0 0 0-22.183 11.06 29.762 29.762 0 
0 0-28.476-21.192z" fill="url(#linearGradient7718)"/>
+   <path d="m953.49-584.47a20.263 20.263 0 0 0-20.263 20.263h-10.132a10.132 10.132 0 0 0-10.132 10.132 
10.132 10.132 0 0 0 10.132 10.132h55.724a15.198 15.198 0 0 0 15.198-15.198 15.198 15.198 0 0 0-15.198-15.198 
15.198 15.198 0 0 0-6.9319 1.688 20.263 20.263 0 0 0-18.397-11.82z" fill="url(#linearGradient7765)"/>
+   <path d="m1036.9-497.88a17.73 17.73 0 0 1-17.598 15.74h-106.38v4.0527h106.38a17.73 17.73 0 0 0 
17.73-17.73 17.73 17.73 0 0 0-0.1326-2.062z" fill="#98c1f1"/>
+   <path d="m756.04-510.43c-0.0689 0.64688-0.10649 1.2967-0.11277 1.9472 0 11.191 9.0722 20.263 20.263 
20.263h151.98v-4.0527h-151.98c-10.375-2e-3 -19.073-7.8392-20.151-18.158z" fill="#98c1f1"/>
+  </g>
+  <g transform="matrix(1.0132 0 0 1.0132 3866.4 -100.7)">
+   <g>
+    <path transform="translate(0 -925.48)" d="m-2930 493.29-47.457 38.617h-9.543v10.18a7.0007 7.0007 0 0 0 
11.418 6.3438l6.6718-5.4297h77.82l6.6718 5.4297a7.0007 7.0007 0 0 0 11.418-6.375v-10.148h-9.543zm0 18.049 
25.277 20.568h-50.555z" color="#000000" color-rendering="auto" dominant-baseline="auto" 
fill="url(#linearGradient7358)" image-rendering="auto" shape-rendering="auto" solid-color="#000000" 
style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/>
+    <rect x="-2965" y="-437.48" width="15" height="30" fill="#77767b"/>
+    <path d="m-2970-397.48 40-30 40 30v60h-80z" fill="#f6f5f4"/>
+    <path d="m-2935.8-433.48-34.219 27.996v18.09l40-32.729 40 32.729v-18.09l-34.219-27.996z" color="#000000" 
color-rendering="auto" dominant-baseline="auto" fill="#c01c28" image-rendering="auto" shape-rendering="auto" 
solid-color="#000000" 
style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/>
+   </g>
+   <path d="m-2980-392.48 50-40.687 50 40.687" fill="none" stroke="#ed333b" stroke-linecap="round" 
stroke-width="14"/>
+   <circle transform="scale(1,-1)" cx="-2929.5" cy="391.48" r="10" fill="#62a0ea"/>
+   <g transform="translate(15)">
+    <circle cx="-2945" cy="-357.48" r="10" fill="#77767b"/>
+    <rect x="-2955" y="-357.48" width="20" height="20" fill="#77767b"/>
+    <path transform="translate(0 -925.48)" d="m-2945 558a10 10 0 0 0-10 10v4a10 10 0 0 1 10-10 10 10 0 0 1 
10 10v-4a10 10 0 0 0-10-10z" fill="#5e5c64"/>
+   </g>
+   <g>
+    <rect x="-2970" y="-342.48" width="80" height="5" fill="#241f31" opacity=".2"/>
+    <path d="m-2939.4-390.45a10 10 0 0 1-0.057-1.0352 10 10 0 0 1 10-10 10 10 0 0 1 10 10 10 10 0 0 1-0.057 
0.96484 10 10 0 0 0-9.9434-8.9648 10 10 0 0 0-9.9434 9.0352z" fill="#3584e4"/>
+    <rect x="-2965" y="-437.48" width="15" height="5" fill="#3d3846"/>
+   </g>
+  </g>
+  <path d="m981.26-441.97a27.862 27.862 0 0 1-27.769 25.691h-136.78a15.198 15.198 0 0 1-15.045-13.165 15.198 
15.198 0 0 0-0.15238 2.0204 15.198 15.198 0 0 0 15.198 15.198h136.78a27.862 27.862 0 0 0 27.862-27.862 27.862 
27.862 0 0 0-0.0932-1.8819z" fill="#98c1f1"/>
+  <path d="m993.87-561.17a15.198 15.198 0 0 1-15.045 13.177h-55.724a10.132 10.132 0 0 1-9.922-8.0856 10.132 
10.132 0 0 0-0.20973 2.0066 10.132 10.132 0 0 0 10.132 10.132h55.724a15.198 15.198 0 0 0 15.198-15.198 15.198 
15.198 0 0 0-0.15238-2.0323z" fill="#98c1f1"/>
+  <title>Gnome Symbolic Icons</title>
+ </g>
+</svg>
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index fb7d7b4e4..c8d2868df 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/FractalNext/">
+    <file preprocess="xml-stripblanks">assets/homeserver.svg</file>
     <file preprocess="xml-stripblanks">assets/other-device.svg</file>
     <file preprocess="xml-stripblanks">assets/setup-complete.svg</file>
     <file preprocess="xml-stripblanks">assets/welcome.svg</file>
@@ -52,6 +53,7 @@
     <file compressed="true" preprocess="xml-stripblanks" alias="greeter.ui">ui/greeter.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="identity-verification-widget.ui">ui/identity-verification-widget.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="in-app-notification.ui">ui/in-app-notification.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="login-advanced-dialog.ui">ui/login-advanced-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="media-viewer.ui">ui/media-viewer.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="member-menu.ui">ui/member-menu.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 331a8eabe..38dc3fa17 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -81,10 +81,14 @@ headerbar .suggested-action {
 
 /* Login */
 
-.login {
+login {
   min-width: 250px;
 }
 
+login entry {
+  padding: 18px 24px;
+}
+
 /* Session */
 
 .session-loading-spinner {
diff --git a/data/resources/ui/login-advanced-dialog.ui b/data/resources/ui/login-advanced-dialog.ui
new file mode 100644
index 000000000..762d44072
--- /dev/null
+++ b/data/resources/ui/login-advanced-dialog.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="LoginAdvancedDialog" parent="AdwPreferencesWindow">
+    <property name="modal">True</property>
+    <property name="title" translatable="yes">Homeserver Discovery</property>
+    <property name="destroy-with-parent">True</property>
+    <property name="default-width">500</property>
+    <property name="default-height">300</property>
+    <property name="search-enabled">false</property>
+    <child>
+      <object class="AdwPreferencesPage">
+        <child>
+          <object class="AdwPreferencesGroup">
+            <property name="description" translatable="yes">Auto-discovery, also known as "well-known 
lookup", allows to discover the URL of a Matrix homeserver from a domain name. This should only be disabled 
if your homeserver doesn’t support auto-discovery or if you want to provide the URL yourself.</property>
+            <child>
+              <object class="AdwActionRow">
+                <property name="title" translatable="yes">_Auto-discovery</property>
+                <property name="use-underline">true</property>
+                <child>
+                  <object class="GtkSwitch">
+                    <property name="valign">center</property>
+                    <property name="active" bind-source="LoginAdvancedDialog" bind-property="autodiscovery" 
bind-flags="sync-create|bidirectional"/>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/resources/ui/login.ui b/data/resources/ui/login.ui
index 0743eeeef..790c4bf4e 100644
--- a/data/resources/ui/login.ui
+++ b/data/resources/ui/login.ui
@@ -12,9 +12,9 @@
               </object>
             </property>
             <child type="start">
-              <object class="GtkButton">
-                <property name="action_name">app.show-greeter</property>
+              <object class="GtkButton" id="back_button">
                 <property name="icon-name">go-previous-symbolic</property>
+                <property name="action_name">login.prev</property>
               </object>
             </child>
             <child type="end">
@@ -34,82 +34,156 @@
             <property name="vexpand">True</property>
             <child>
               <object class="GtkStackPage">
-                <property name="name">credentials</property>
+                <property name="name">homeserver</property>
                 <property name="child">
                   <object class="AdwClamp">
-                    <property name="maximum-size">400</property>
-                    <property name="tightening-threshold">300</property>
-                    <property name="valign">center</property>
-                    <child>
+                    <property name="maximum-size">360</property>
+                    <property name="tightening-threshold">360</property>
+                    <property name="margin-top">0</property>
+                    <property name="margin-bottom">24</property>
+                    <property name="margin-start">24</property>
+                    <property name="margin-end">24</property>
+                    <property name="child">
                       <object class="GtkBox">
                         <property name="orientation">vertical</property>
-                        <property name="spacing">18</property>
+                        <property name="valign">center</property>
+                        <property name="spacing">24</property>
+                        <child>
+                          <object class="AdwClamp">
+                            <property name="child">
+                              <object class="GtkPicture">
+                                <property 
name="file">resource:///org/gnome/FractalNext/assets/homeserver.svg</property>
+                              </object>
+                            </property>
+                          </object>
+                        </child>
                         <child>
-                          <object class="GtkListBox">
+                          <object class="GtkBox">
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">6</property>
                             <child>
-                              <object class="GtkListBoxRow">
-                                <property name="focusable">False</property>
-                                <property name="selectable">False</property>
-                                <property name="activatable">False</property>
-                                <property name="child">
-                                  <object class="GtkEntry" id="homeserver_entry">
-                                    <property name="activates-default">True</property>
-                                    <property name="input_purpose">GTK_INPUT_PURPOSE_URL</property>
-                                    <property name="placeholder-text">Homeserver</property>
-                                    <property name="margin-top">6</property>
-                                    <property name="margin-bottom">6</property>
-                                    <property name="margin-start">6</property>
-                                    <property name="margin-end">6</property>
-                                  </object>
-                                </property>
+                              <object class="GtkEntry" id="homeserver_entry">
+                                <style>
+                                  <class name="card"/>
+                                </style>
+                                <property name="activates-default">true</property>
+                                <property name="secondary-icon-name">document-edit-symbolic</property>
+                                <property name="secondary-icon-sensitive">false</property>
+                                <property name="secondary-icon-activatable">false</property>
                               </object>
                             </child>
                             <child>
-                              <object class="GtkListBoxRow">
-                                <property name="focusable">False</property>
-                                <property name="selectable">False</property>
-                                <property name="activatable">False</property>
-                                <property name="child">
-                                  <object class="GtkEntry" id="username_entry">
-                                    <property name="activates-default">True</property>
-                                    <property name="placeholder-text">Matrix Username</property>
-                                    <property name="margin-top">6</property>
-                                    <property name="margin-bottom">6</property>
-                                    <property name="margin-start">6</property>
-                                    <property name="margin-end">6</property>
-                                  </object>
-                                </property>
+                              <object class="GtkLabel" id="homeserver_help">
+                                <style>
+                                  <class name="caption"/>
+                                  <class name="dim-label"/>
+                                </style>
+                                <property name="justify">left</property>
+                                <property name="xalign">0.0</property>
+                                <property name="margin-start">6</property>
+                                <property name="margin-end">6</property>
+                                <property name="wrap">true</property>
+                                <property name="use-markup">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkButton">
+                            <style>
+                              <class name="pill"/>
+                            </style>
+                            <property name="halign">center</property>
+                            <property name="label">Advanced…</property>
+                            <property name="action-name">login.open-advanced</property>
+                          </object>
+                        </child>
+                      </object>
+                    </property>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">password</property>
+                <property name="child">
+                  <object class="AdwClamp">
+                    <property name="maximum-size">360</property>
+                    <property name="tightening-threshold">360</property>
+                    <property name="valign">center</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">30</property>    
+                        <child>
+                          <object class="GtkBox">
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">6</property>
+                            <property name="halign">center</property>
+                            <child>
+                              <object class="GtkLabel" id="password_title">
+                                <style>
+                                  <class name="title-4"/>
+                                </style>
                               </object>
                             </child>
                             <child>
-                              <object class="GtkListBoxRow">
-                                <property name="focusable">False</property>
-                                <property name="selectable">False</property>
-                                <property name="activatable">False</property>
-                                <property name="child">
-                                  <object class="GtkPasswordEntry" id="password_entry">
-                                    <property name="activates-default">True</property>
-                                    <property name="show-peek-icon">True</property>
-                                    <property name="placeholder-text">Password</property>
-                                    <property name="margin-top">6</property>
-                                    <property name="margin-bottom">6</property>
-                                    <property name="margin-start">6</property>
-                                    <property name="margin-end">6</property>
+                              <object class="GtkBox">
+                                <property name="spacing">6</property>
+                                <property name="halign">center</property>
+                                <property name="visible" bind-source="Login" bind-property="autodiscovery" 
bind-flags="sync-create"/>
+                                <property name="tooltip-text" translatable="yes">Homeserver URL</property>
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="icon-name">user-home-symbolic</property>
                                   </object>
-                                </property>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel">
+                                    <style>
+                                      <class name="body"/>
+                                    </style>
+                                    <property name="label" bind-source="Login" bind-property="homeserver" 
bind-flags="sync-create"/>
+                                  </object>
+                                </child>
                               </object>
                             </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="username_entry">
                             <style>
-                              <class name="content"/>
-                              <class name="login"/>
+                              <class name="card"/>
                             </style>
+                            <property name="activates-default">true</property>
+                            <property name="placeholder-text" translatable="true">Matrix Username</property>
+                            <property name="secondary-icon-name">document-edit-symbolic</property>
+                            <property name="secondary-icon-sensitive">false</property>
+                            <property name="secondary-icon-activatable">false</property>
                           </object>
                         </child>
                         <child>
-                          <object class="GtkLinkButton" id="forgot_password">
-                            <property name="use_underline">True</property>
-                            <property name="label" translatable="yes">_Forgot Password?</property>
-                            <property name="uri">https://app.element.io/#/forgot_password</property>
+                          <object class="GtkBox">
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">12</property>
+                            <child>
+                              <object class="GtkPasswordEntry" id="password_entry">
+                                <style>
+                                  <class name="card"/>
+                                </style>
+                                <property name="activates-default">True</property>
+                                <property name="show-peek-icon">True</property>
+                                <property name="placeholder-text" translatable="true">Password</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLinkButton" id="forgot_password">
+                                <property name="use_underline">True</property>
+                                <property name="label" translatable="yes">_Forgot Password?</property>
+                                <property name="uri">https://app.element.io/#/forgot_password</property>
+                              </object>
+                            </child>
                           </object>
                         </child>
                       </object>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 2275d5bdd..39576bb61 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -25,6 +25,7 @@ data/resources/ui/event-menu.ui
 data/resources/ui/event-source-dialog.ui
 data/resources/ui/greeter.ui
 data/resources/ui/identity-verification-widget.ui
+data/resources/ui/login-advanced-dialog.ui
 data/resources/ui/login.ui
 data/resources/ui/member-menu.ui
 data/resources/ui/room-creation.ui
@@ -35,6 +36,7 @@ data/resources/ui/sidebar.ui
 
 # Rust files
 src/application.rs
+src/login.rs
 src/secret.rs
 src/session/account_settings/devices_page/device_list.rs
 src/session/account_settings/devices_page/device_row.rs
diff --git a/src/login.rs b/src/login.rs
index 8129f594d..c477ea4ca 100644
--- a/src/login.rs
+++ b/src/login.rs
@@ -1,12 +1,25 @@
-use adw::subclass::prelude::BinImpl;
-use gtk::{self, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
-use log::debug;
+use adw::{prelude::*, subclass::prelude::BinImpl};
+use gettextrs::gettext;
+use gtk::{self, glib, glib::clone, subclass::prelude::*, CompositeTemplate};
+use log::{debug, warn};
+use matrix_sdk::{
+    config::RequestConfig,
+    ruma::{
+        api::client::unversioned::get_supported_versions, identifiers::Error as IdentifierError,
+        ServerName, UserId,
+    },
+    Client, Result as MatrixResult,
+};
+use tokio::task::JoinHandle;
 use url::{ParseError, Url};
 
-use crate::{components::SpinnerButton, Session};
+use crate::{
+    components::SpinnerButton, error::Error, login_advanced_dialog::LoginAdvancedDialog, spawn,
+    spawn_tokio, user_facing_error::UserFacingError, Session,
+};
 
 mod imp {
-    use std::cell::RefCell;
+    use std::cell::{Cell, RefCell};
 
     use glib::{
         subclass::{InitializingObject, Signal},
@@ -21,18 +34,28 @@ mod imp {
     pub struct Login {
         pub current_session: RefCell<Option<Session>>,
         #[template_child]
+        pub back_button: TemplateChild<gtk::Button>,
+        #[template_child]
         pub next_button: TemplateChild<SpinnerButton>,
         #[template_child]
         pub main_stack: TemplateChild<gtk::Stack>,
         #[template_child]
         pub homeserver_entry: TemplateChild<gtk::Entry>,
         #[template_child]
+        pub homeserver_help: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub password_title: TemplateChild<gtk::Label>,
+        #[template_child]
         pub username_entry: TemplateChild<gtk::Entry>,
         #[template_child]
         pub password_entry: TemplateChild<gtk::PasswordEntry>,
         pub prepared_source_id: RefCell<Option<SignalHandlerId>>,
         pub logged_out_source_id: RefCell<Option<SignalHandlerId>>,
         pub ready_source_id: RefCell<Option<SignalHandlerId>>,
+        /// Whether auto-discovery is enabled.
+        pub autodiscovery: Cell<bool>,
+        /// The homeserver to log into.
+        pub homeserver: RefCell<Option<Url>>,
     }
 
     #[glib::object_subclass]
@@ -43,8 +66,15 @@ mod imp {
 
         fn class_init(klass: &mut Self::Class) {
             Self::bind_template(klass);
+            klass.set_css_name("login");
             klass.set_accessible_role(gtk::AccessibleRole::Group);
             klass.install_action("login.next", None, move |widget, _, _| widget.forward());
+            klass.install_action("login.prev", None, move |widget, _, _| widget.backward());
+            klass.install_action("login.open-advanced", None, move |widget, _, _| {
+                spawn!(clone!(@weak widget => async move {
+                    widget.open_advanced_dialog().await;
+                }));
+            });
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -65,17 +95,67 @@ mod imp {
             SIGNALS.as_ref()
         }
 
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpecString::new(
+                        "homeserver",
+                        "Homeserver",
+                        "The homeserver to log into",
+                        None,
+                        glib::ParamFlags::READABLE,
+                    ),
+                    glib::ParamSpecBoolean::new(
+                        "autodiscovery",
+                        "Auto-discovery",
+                        "Whether auto-discovery is enabled",
+                        true,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "homeserver" => obj.homeserver_pretty().to_value(),
+                "autodiscovery" => obj.autodiscovery().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "autodiscovery" => obj.set_autodiscovery(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
         fn constructed(&self, obj: &Self::Type) {
             obj.action_set_enabled("login.next", false);
 
             self.parent_constructed(obj);
 
+            self.main_stack
+                .connect_visible_child_notify(clone!(@weak obj => move |_|
+                    obj.update_next_action()
+                ));
+            obj.update_next_action();
+
             self.homeserver_entry
-                .connect_changed(clone!(@weak obj => move |_| obj.enable_next_action()));
+                .connect_changed(clone!(@weak obj => move |_| obj.update_next_action()));
             self.username_entry
-                .connect_changed(clone!(@weak obj => move |_| obj.enable_next_action()));
+                .connect_changed(clone!(@weak obj => move |_| obj.update_next_action()));
             self.password_entry
-                .connect_changed(clone!(@weak obj => move |_| obj.enable_next_action()));
+                .connect_changed(clone!(@weak obj => move |_| obj.update_next_action()));
         }
     }
 
@@ -85,6 +165,7 @@ mod imp {
 }
 
 glib::wrapper! {
+    /// A widget handling the login flows.
     pub struct Login(ObjectSubclass<imp::Login>)
         @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
 }
@@ -94,28 +175,223 @@ impl Login {
         glib::Object::new(&[]).expect("Failed to create Login")
     }
 
-    fn enable_next_action(&self) {
+    pub fn homeserver(&self) -> Option<Url> {
+        self.imp().homeserver.borrow().clone()
+    }
+
+    pub fn homeserver_pretty(&self) -> Option<String> {
+        let homeserver = self.homeserver();
+        homeserver
+            .as_ref()
+            .and_then(|url| url.as_ref().strip_suffix('/').map(ToOwned::to_owned))
+            .or_else(|| homeserver.as_ref().map(ToString::to_string))
+    }
+
+    pub fn set_homeserver(&self, homeserver: Option<Url>) {
+        let priv_ = imp::Login::from_instance(self);
+
+        if self.homeserver() == homeserver {
+            return;
+        }
+
+        priv_.homeserver.replace(homeserver);
+        self.notify("homeserver");
+    }
+
+    fn visible_child(&self) -> String {
+        let priv_ = imp::Login::from_instance(self);
+        priv_.main_stack.visible_child_name().unwrap().into()
+    }
+
+    fn set_visible_child(&self, visible_child: &str) {
+        let priv_ = imp::Login::from_instance(self);
+        priv_.main_stack.set_visible_child_name(visible_child);
+    }
+
+    fn update_next_action(&self) {
+        let priv_ = imp::Login::from_instance(self);
+        match self.visible_child().as_ref() {
+            "homeserver" => {
+                let homeserver = priv_.homeserver_entry.text();
+                let enabled = if self.autodiscovery() {
+                    build_server_name(homeserver.as_str()).is_ok()
+                } else {
+                    build_homeserver_url(homeserver.as_str()).is_ok()
+                };
+                self.action_set_enabled("login.next", enabled);
+                priv_.next_button.set_visible(true);
+            }
+            "password" => {
+                let username_length = priv_.username_entry.text_length();
+                let password_length = priv_.password_entry.text().len();
+                self.action_set_enabled("login.next", username_length != 0 && password_length != 0);
+                priv_.next_button.set_visible(true);
+            }
+            _ => {
+                priv_.next_button.set_visible(false);
+            }
+        }
+    }
+
+    fn forward(&self) {
+        match self.visible_child().as_ref() {
+            "homeserver" => {
+                if self.autodiscovery() {
+                    self.try_autodiscovery();
+                } else {
+                    self.check_homeserver();
+                }
+            }
+            "password" => self.login_with_password(),
+            _ => {}
+        }
+    }
+
+    fn backward(&self) {
+        match self.visible_child().as_ref() {
+            "password" => self.set_visible_child("homeserver"),
+            _ => {
+                self.activate_action("app.show-greeter", None).unwrap();
+            }
+        }
+    }
+
+    pub fn autodiscovery(&self) -> bool {
+        self.imp().autodiscovery.get()
+    }
+
+    fn set_autodiscovery(&self, autodiscovery: bool) {
         let priv_ = self.imp();
-        let homeserver = priv_.homeserver_entry.text();
-        let username_length = priv_.username_entry.text_length();
-        let password_length = priv_.password_entry.text().len();
-
-        self.action_set_enabled(
-            "login.next",
-            homeserver.len() != 0
-                && build_homeserver_url(homeserver.as_str()).is_ok()
-                && username_length != 0
-                && password_length != 0,
+
+        priv_.autodiscovery.set(autodiscovery);
+        if autodiscovery {
+            priv_
+                .homeserver_entry
+                .set_placeholder_text(Some(&gettext("Domain Name…")));
+            priv_.homeserver_help.set_markup(&gettext(
+                "The domain of your Matrix homeserver, for example gnome.org",
+            ));
+        } else {
+            priv_
+                .homeserver_entry
+                .set_placeholder_text(Some(&gettext("Homeserver URL…")));
+            priv_.homeserver_help.set_markup(&gettext("The URL of your Matrix homeserver, for example <span 
segment=\"word\">https://gnome.modular.im</span>"));
+        }
+        self.update_next_action();
+    }
+
+    async fn open_advanced_dialog(&self) {
+        let dialog =
+            LoginAdvancedDialog::new(self.root().unwrap().downcast_ref::<gtk::Window>().unwrap());
+        self.bind_property("autodiscovery", &dialog, "autodiscovery")
+            .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
+            .build();
+        dialog.run_future().await;
+    }
+
+    fn try_autodiscovery(&self) {
+        let server = build_server_name(self.imp().homeserver_entry.text().as_str()).unwrap();
+        let mxid = UserId::parse_with_server_name("user", &server).unwrap();
+
+        self.freeze();
+
+        let handle = spawn_tokio!(async move { Client::new_from_user_id(&mxid).await });
+
+        spawn!(
+            glib::PRIORITY_DEFAULT_IDLE,
+            clone!(@weak self as obj => async move {
+                match handle.await.unwrap() {
+                    Ok(client) => {
+                        let homeserver = client.homeserver().await;
+                        obj.set_homeserver(Some(homeserver));
+                        obj.show_password_page();
+                    }
+                    Err(error) => {
+                        warn!("Failed to discover homeserver: {}", error);
+                        let error_string = error.to_user_facing();
+
+                        obj.parent_window().append_error(&Error::new(move |_| {
+                            let error_label = gtk::Label::builder()
+                                .label(&error_string)
+                                .wrap(true)
+                                .build();
+                            Some(error_label.upcast())
+                        }));
+                    }
+                };
+                obj.unfreeze();
+            })
         );
     }
 
-    fn forward(&self) {
-        self.login();
+    fn check_homeserver(&self) {
+        let homeserver = build_homeserver_url(self.imp().homeserver_entry.text().as_str()).unwrap();
+        let homeserver_clone = homeserver.clone();
+
+        self.freeze();
+
+        let handle: JoinHandle<MatrixResult<_>> = spawn_tokio!(async move {
+            let client = Client::new(homeserver_clone)?;
+            Ok(client
+                .send(
+                    get_supported_versions::Request::new(),
+                    Some(RequestConfig::new().disable_retry()),
+                )
+                .await?)
+        });
+
+        spawn!(
+            glib::PRIORITY_DEFAULT_IDLE,
+            clone!(@weak self as obj => async move {
+                match handle.await.unwrap() {
+                    Ok(_) => {
+                        obj.set_homeserver(Some(homeserver));
+                        obj.show_password_page();
+                    }
+                    Err(error) => {
+                        warn!("Failed to check homeserver: {}", error);
+                        let error_string = error.to_user_facing();
+
+                        obj.parent_window().append_error(&Error::new(move |_| {
+                            let error_label = gtk::Label::builder()
+                                .label(&error_string)
+                                .wrap(true)
+                                .build();
+                            Some(error_label.upcast())
+                        }));
+                    }
+                };
+                obj.unfreeze();
+            })
+        );
     }
 
-    fn login(&self) {
+    fn show_password_page(&self) {
         let priv_ = self.imp();
-        let homeserver = priv_.homeserver_entry.text().to_string();
+        if self.autodiscovery() {
+            // Translators: the variable is a domain name, eg. gnome.org.
+            priv_.password_title.set_markup(&gettext!(
+                "Connecting to {}",
+                format!(
+                    "<span segment=\"word\">{}</span>",
+                    priv_.homeserver_entry.text()
+                )
+            ));
+        } else {
+            priv_.password_title.set_markup(&gettext!(
+                "Connecting to {}",
+                format!(
+                    "<span segment=\"word\">{}</span>",
+                    self.homeserver_pretty().unwrap()
+                )
+            ));
+        }
+        self.set_visible_child("password");
+    }
+
+    fn login_with_password(&self) {
+        let priv_ = self.imp();
+        let homeserver = self.homeserver().unwrap();
         let username = priv_.username_entry.text().to_string();
         let password = priv_.password_entry.text().to_string();
 
@@ -124,11 +400,7 @@ impl Login {
         let session = Session::new();
         self.set_handler_for_prepared_session(&session);
 
-        session.login_with_password(
-            build_homeserver_url(homeserver.as_str()).unwrap(),
-            username,
-            password,
-        );
+        session.login_with_password(homeserver, username, password, self.autodiscovery());
         priv_.current_session.replace(Some(session));
     }
 
@@ -137,6 +409,7 @@ impl Login {
         priv_.homeserver_entry.set_text("");
         priv_.username_entry.set_text("");
         priv_.password_entry.set_text("");
+        priv_.autodiscovery.set(true);
         self.unfreeze();
         self.drop_session_reference();
     }
@@ -152,9 +425,9 @@ impl Login {
     fn unfreeze(&self) {
         let priv_ = self.imp();
 
-        self.action_set_enabled("login.next", true);
         priv_.next_button.set_loading(false);
         priv_.main_stack.set_sensitive(true);
+        self.update_next_action();
     }
 
     pub fn connect_new_session<F: Fn(&Self, Session) + 'static>(
@@ -240,6 +513,14 @@ impl Default for Login {
     }
 }
 
+fn build_server_name(server: &str) -> Result<Box<ServerName>, IdentifierError> {
+    let server = server
+        .strip_prefix("http://";)
+        .or_else(|| server.strip_prefix("https://";))
+        .unwrap_or(server);
+    ServerName::parse(server)
+}
+
 fn build_homeserver_url(server: &str) -> Result<Url, ParseError> {
     if server.starts_with("http://";) || server.starts_with("https://";) {
         Url::parse(server)
diff --git a/src/login_advanced_dialog.rs b/src/login_advanced_dialog.rs
new file mode 100644
index 000000000..8985cb67d
--- /dev/null
+++ b/src/login_advanced_dialog.rs
@@ -0,0 +1,118 @@
+use std::cell::Cell;
+
+use adw::subclass::prelude::*;
+use gtk::{gdk, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+mod imp {
+    use glib::subclass::InitializingObject;
+    use once_cell::sync::Lazy;
+
+    use super::*;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/login-advanced-dialog.ui")]
+    pub struct LoginAdvancedDialog {
+        pub autodiscovery: Cell<bool>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for LoginAdvancedDialog {
+        const NAME: &'static str = "LoginAdvancedDialog";
+        type Type = super::LoginAdvancedDialog;
+        type ParentType = adw::PreferencesWindow;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+
+            klass.add_binding_signal(
+                gdk::Key::Escape,
+                gdk::ModifierType::empty(),
+                "close-request",
+                None,
+            );
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for LoginAdvancedDialog {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpecBoolean::new(
+                    "autodiscovery",
+                    "Auto-discovery",
+                    "Whether auto-discovery is enabled",
+                    true,
+                    glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "autodiscovery" => obj.autodiscovery().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "autodiscovery" => obj.set_autodiscovery(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl WidgetImpl for LoginAdvancedDialog {}
+    impl WindowImpl for LoginAdvancedDialog {}
+    impl AdwWindowImpl for LoginAdvancedDialog {}
+    impl PreferencesWindowImpl for LoginAdvancedDialog {}
+}
+
+glib::wrapper! {
+    pub struct LoginAdvancedDialog(ObjectSubclass<imp::LoginAdvancedDialog>)
+        @extends gtk::Widget, gtk::Window, adw::Window, adw::PreferencesWindow, @implements gtk::Accessible;
+}
+
+impl LoginAdvancedDialog {
+    pub fn new(window: &gtk::Window) -> Self {
+        glib::Object::new(&[("transient-for", window)])
+            .expect("Failed to create LoginAdvancedDialog")
+    }
+
+    pub fn autodiscovery(&self) -> bool {
+        self.imp().autodiscovery.get()
+    }
+
+    pub fn set_autodiscovery(&self, autodiscovery: bool) {
+        let priv_ = self.imp();
+
+        priv_.autodiscovery.set(autodiscovery);
+        self.notify("autodiscovery");
+    }
+
+    pub async fn run_future(&self) {
+        let (sender, receiver) = futures::channel::oneshot::channel();
+        let sender = Cell::new(Some(sender));
+
+        self.connect_close_request(move |_| {
+            if let Some(sender) = sender.take() {
+                sender.send(()).unwrap();
+            }
+            gtk::Inhibit(false)
+        });
+
+        self.show();
+        receiver.await.unwrap();
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 4f65274ac..97162920b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,6 +12,7 @@ mod contrib;
 mod error;
 mod greeter;
 mod login;
+mod login_advanced_dialog;
 mod secret;
 mod session;
 mod user_facing_error;
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 5af814037..eb91c29c3 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -284,7 +284,13 @@ impl Session {
         }
     }
 
-    pub fn login_with_password(&self, homeserver: Url, username: String, password: String) {
+    pub fn login_with_password(
+        &self,
+        homeserver: Url,
+        username: String,
+        password: String,
+        use_discovery: bool,
+    ) {
         self.imp().logout_on_dispose.set(true);
         let mut path = glib::user_data_dir();
         path.push(
@@ -307,6 +313,12 @@ impl Session {
                 .passphrase(passphrase.clone())
                 .store_path(path.clone());
 
+            let config = if use_discovery {
+                config.use_discovery_response()
+            } else {
+                config
+            };
+
             let client = Client::new_with_config(homeserver.clone(), config).unwrap();
             let response = client
                 .login(&username, &password, None, Some("Fractal Next"))


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]