[fractal/fractal-next] login: Add password login and session creation



commit 37c8e184166dc6669314735006cbfee1e00f68ad
Author: Julian Sparber <julian sparber net>
Date:   Wed Mar 24 11:49:25 2021 +0100

    login: Add password login and session creation
    
    Since the login flow for matrix will change soonish, therefore, we
    implement only login via password.

 Cargo.lock                                       |   1 +
 Cargo.toml                                       |   1 +
 data/resources/icons/scalable/status/welcome.svg | 273 +++++++++++++++++++++++
 data/resources/resources.gresource.xml           |   1 +
 data/resources/style.css                         |   5 +
 data/resources/ui/login.ui                       | 139 +++++++++++-
 data/resources/ui/window.ui                      |   6 +-
 src/login.rs                                     | 153 ++++++++++++-
 src/session/mod.rs                               | 220 +++++++++++++++++-
 src/window.rs                                    |  16 +-
 10 files changed, 779 insertions(+), 36 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index 79bc950f..68fe1bf0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -470,6 +470,7 @@ dependencies = [
  "pretty_env_logger",
  "serde_json",
  "tokio",
+ "url",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 1e6e60e0..3b799dd2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ gtk-macros = "0.2"
 once_cell = "1.5"
 serde_json = "1.0"
 tokio = { version = "1.2", features = ["rt", "rt-multi-thread"] }
+url = "2.2"
 
 [dependencies.gtk]
 package = "gtk4"
diff --git a/data/resources/icons/scalable/status/welcome.svg 
b/data/resources/icons/scalable/status/welcome.svg
new file mode 100644
index 00000000..915dec91
--- /dev/null
+++ b/data/resources/icons/scalable/status/welcome.svg
@@ -0,0 +1,273 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   width="340"
+   height="200"
+   viewBox="0 0 89.958331 52.916668"
+   version="1.1"
+   id="svg8662">
+  <defs
+     id="defs8656">
+    <linearGradient
+       xlink:href="#linearGradient8161"
+       id="linearGradient8163"
+       x1="-3292.5"
+       y1="-438.48035"
+       x2="-3272.5"
+       y2="-438.48035"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(3720.9803,-2844.0197)" />
+    <linearGradient
+       id="linearGradient8161">
+      <stop
+         style="stop-color:#ed333b;stop-opacity:1;"
+         offset="0"
+         id="stop8157" />
+      <stop
+         style="stop-color:#f76d5f;stop-opacity:1"
+         offset="1"
+         id="stop8159" />
+    </linearGradient>
+    <linearGradient
+       xlink:href="#linearGradient8145"
+       id="linearGradient8155"
+       x1="-3292.5"
+       y1="-438.48035"
+       x2="-3272.5"
+       y2="-438.48035"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.1500001,0,0,1.1500001,4215.3556,-2778.2476)" />
+    <linearGradient
+       id="linearGradient8145">
+      <stop
+         style="stop-color:#33d17a;stop-opacity:1"
+         offset="0"
+         id="stop8141" />
+      <stop
+         style="stop-color:#8ff0a4;stop-opacity:1"
+         offset="1"
+         id="stop8143" />
+    </linearGradient>
+  </defs>
+  <metadata
+     id="metadata8659">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1">
+    <g
+       id="g9553"
+       transform="translate(-57.074405,-5.2916687)">
+      <g
+         id="g10404"
+         transform="matrix(0.26458333,0,0,0.26458333,-32.869614,144.94489)">
+        <g
+           style="stroke-width:0.751809"
+           id="g8028"
+           transform="matrix(1.7692307,0,0,1,6624.5611,29.65709)">
+          <rect
+             
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#2ec27e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.02834;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+             id="rect8024"
+             width="65.000107"
+             height="21.999973"
+             x="3425"
+             y="-399.48032"
+             rx="6.782609"
+             ry="12"
+             transform="scale(-1,1)" />
+          <rect
+             transform="scale(-1,1)"
+             ry="12.000001"
+             rx="6.782609"
+             y="-432.48032"
+             x="3425"
+             height="51.999969"
+             width="65.000107"
+             id="rect8026"
+             
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#57e389;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.02834;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
 />
+        </g>
+        <g
+           transform="matrix(1.5,0,0,1.5,6384.9459,-80.602744)"
+           id="g4708"
+           style="stroke-width:0.666667">
+          <path
+             
style="display:inline;fill:#1c71d8;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
+             d="m -3912.0003,-233.37879 c -4.4319,0 -8,3.56799 -8,8 v 27.89844 c 0,4.43202 3.5681,8 8,8 h 
36.3333 l 12.6667,12.66667 v -12.66667 h 8.3333 c 4.432,0 8,-3.56798 8,-8 v -27.89844 c 0,-4.43201 -3.568,-8 
-8,-8 z"
+             id="path4704" />
+          <path
+             id="path4706"
+             d="m -3920.0003,-199.48035 v 2 c 0,4.43202 3.5681,8 8,8 h 36.3333 l 12.6667,12.66667 v -2 l 
-12.6667,-12.66667 h -36.3333 c -4.4319,0 -8,-3.56798 -8,-8 z m 73.3333,0 c 0,4.43202 -3.568,8 -8,8 h -8.3333 
v 2 h 8.3333 c 4.432,0 8,-3.56798 8,-8 z"
+             
style="display:inline;fill:#1a5fb4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
 />
+        </g>
+        <g
+           id="g5222"
+           transform="translate(4089.9459,-0.342944)">
+          <rect
+             
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#f5c211;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0376957;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+             id="rect5218"
+             width="49.99995"
+             height="22.999973"
+             x="3455"
+             y="-420.48032"
+             rx="8"
+             ry="8"
+             transform="scale(-1,1)" />
+          <rect
+             transform="scale(-1,1)"
+             ry="8"
+             rx="8"
+             y="-457.48032"
+             x="3455"
+             height="57.000031"
+             width="49.99995"
+             id="rect5220"
+             
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#f8e45c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0376957;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
 />
+        </g>
+        <g
+           transform="matrix(1.5,0,0,1.5,6272.4459,-121.60274)"
+           id="g4715"
+           style="stroke-width:0.666667">
+          <path
+             id="path4710"
+             d="m -3913.6666,-217.48035 c -4.4319,0 -8,3.56798 -8,8 v 24 c 0,4.43202 3.5681,8 8,8 h 15.3333 
v 12.66667 l 12.6667,-12.66667 h 45.9999 c 4.432,0 8,-3.56798 8,-8 v -24 c 0,-4.43202 -3.568,-8 -8,-8 z"
+             
style="display:inline;fill:#3d3846;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
 />
+          <path
+             id="path4712"
+             d="m -3921.6666,-187.48035 v 2 c 0,4.43202 3.5681,8 8,8 h 15.3333 v -2 h -15.3333 c -4.4319,0 
-8,-3.56798 -8,-8 z m 89.9999,0 c 0,4.43202 -3.568,8 -8,8 h -45.9999 l -12.6667,12.66667 v 2 l 
12.6667,-12.66667 h 45.9999 c 4.432,0 8,-3.56798 8,-8 z"
+             
style="display:inline;fill:#241f31;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
 />
+          <rect
+             
style="opacity:1;vector-effect:none;fill:#77767b;fill-opacity:1;stroke:none;stroke-width:9.33333;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+             id="rect7985-1"
+             width="56.666668"
+             height="3.3333311"
+             x="-3905"
+             y="-207.48033" />
+          <rect
+             
style="opacity:1;vector-effect:none;fill:#77767b;fill-opacity:1;stroke:none;stroke-width:9.33333;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+             id="rect7989-4"
+             width="56.666668"
+             height="3.3333311"
+             x="-3905"
+             y="-200.81364" />
+          <rect
+             y="-194.14696"
+             x="-3905"
+             height="3.3333311"
+             width="40"
+             id="rect7991-9"
+             
style="opacity:1;vector-effect:none;fill:#77767b;fill-opacity:1;stroke:none;stroke-width:9.33333;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
 />
+        </g>
+        <g
+           transform="matrix(1.5937492,0,0,1,5992.1307,-55.342911)"
+           id="g5032"
+           style="stroke-width:0.792118">
+          <rect
+             
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#c01c28;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0298595;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+             id="rect5026"
+             width="80.000107"
+             height="50.000008"
+             x="3410"
+             y="-447.48035"
+             rx="5.0196104"
+             ry="8"
+             transform="scale(-1,1)" />
+          <rect
+             transform="scale(-1,1)"
+             ry="8"
+             rx="5.0196104"
+             y="-452.48032"
+             x="3410"
+             height="51.999996"
+             width="80.000107"
+             id="rect5028"
+             
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#ed333b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0298595;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
 />
+        </g>
+        <g
+           id="g8139"
+           transform="translate(3889.9459,-65.34288)">
+          <g
+             style="stroke-width:0.666667"
+             transform="matrix(1.5,0,0,1.5,2465.0001,-75.912199)"
+             id="g4721">
+            <path
+               
style="display:inline;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
+               d="m -3905.3334,-234.37879 c -4.4319,0 -8,3.56799 -8,8 v 27.33333 c 0,4.43202 3.5681,8 8,8 h 
19 l 12.6667,12.66667 v -12.66667 H -3848 c 4.432,0 8,-3.56798 8,-8 v -27.33333 c 0,-4.43201 -3.568,-8 -8,-8 
z"
+               id="path4717" />
+            <path
+               id="path4719"
+               d="m -3913.3334,-201.04546 v 2 c 0,4.43202 3.5681,8 8,8 h 19 l 12.6667,12.66667 v -2 l 
-12.6667,-12.66667 h -19 c -4.4319,0 -8,-3.56798 -8,-8 z m 73.3334,0 c 0,4.43202 -3.568,8 -8,8 h -25.6667 v 2 
H -3848 c 4.432,0 8,-3.56798 8,-8 z"
+               
style="display:inline;fill:#deddda;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00753214px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new"
 />
+          </g>
+          <rect
+             y="-412.48035"
+             x="-3385"
+             height="4.9999967"
+             width="70"
+             id="rect7985"
+             
style="opacity:1;vector-effect:none;fill:#deddda;fill-opacity:1;stroke:none;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
 />
+          <rect
+             y="-402.48035"
+             x="-3385"
+             height="4.9999967"
+             width="70"
+             id="rect7989"
+             
style="opacity:1;vector-effect:none;fill:#deddda;fill-opacity:1;stroke:none;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
 />
+          <rect
+             
style="opacity:1;vector-effect:none;fill:#deddda;fill-opacity:1;stroke:none;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+             id="rect7991"
+             width="45"
+             height="4.9999967"
+             x="-3385"
+             y="-392.48035" />
+        </g>
+        <g
+           id="g7871"
+           transform="translate(3912.4459,32.65712)">
+          <circle
+             
style="opacity:1;vector-effect:none;fill:#c01c28;fill-opacity:1;stroke:none;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+             id="circle7867"
+             cx="-3282.5"
+             cy="-435.48035"
+             r="10" />
+          <circle
+             r="10"
+             cy="-3282.5"
+             cx="438.48035"
+             id="circle7869"
+             
style="opacity:1;vector-effect:none;fill:url(#linearGradient8163);fill-opacity:1;stroke:none;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+             transform="rotate(-90)" />
+        </g>
+        <g
+           id="g7977"
+           transform="translate(3676.4459,-6.842912)">
+          <circle
+             
style="opacity:1;vector-effect:none;fill:#26a269;fill-opacity:1;stroke:none;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+             id="circle7973"
+             cx="-3282.5"
+             cy="-437.48032"
+             r="11.500001" />
+          <circle
+             r="11.500001"
+             cy="-3282.5"
+             cx="440.48032"
+             id="circle7975"
+             
style="opacity:1;vector-effect:none;fill:url(#linearGradient8155);fill-opacity:1;stroke:none;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+             transform="rotate(-90)" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 75e043dd..6d2f72a4 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -9,5 +9,6 @@
     <file compressed="true" preprocess="xml-stripblanks" alias="window.ui">ui/window.ui</file>
     <file compressed="true">style.css</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
   </gresource>
 </gresources>
diff --git a/data/resources/style.css b/data/resources/style.css
index c3d88b6e..6f75520d 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -1,3 +1,8 @@
+/* Login */
+.login {
+  min-width: 250px;
+}
+
 .title-header {
   font-size: 36px;
   font-weight: bold;
diff --git a/data/resources/ui/login.ui b/data/resources/ui/login.ui
index 94347e78..0a0efc4e 100644
--- a/data/resources/ui/login.ui
+++ b/data/resources/ui/login.ui
@@ -3,15 +3,136 @@
   <template class="FrctlLogin" parent="AdwBin">
     <child>
       <object class="GtkBox">
-        <property name="orientation">vertical</property>
-        <child>
-          <object class="GtkHeaderBar" id="headerbar" />
-        </child>
-        <child>
-          <object class="AdwStatusPage">
-            <!-- TODO implement login -->
-          </object>
-        </child>
+       <property name="orientation">vertical</property>
+       <child>
+         <object class="GtkHeaderBar">
+           <property name="title-widget">
+             <object class="AdwWindowTitle">
+               <property name="title">Fractal</property>
+             </object>
+           </property>
+           <child type="end">
+             <object class="GtkButton">
+               <property name="action_name">login.next</property>
+               <property name="child">
+                 <object class="GtkStack" id="next_stack">
+                   <child>
+                     <object class="GtkLabel" id="next_label">
+                       <property name="use_underline">True</property>
+                       <property name="label" translatable="yes">_Next</property>
+                     </object>
+                   </child>
+                   <child>
+                     <object class="GtkSpinner" id="next_spinner">
+                       <property name="spinning">True</property>
+                       <property name="valign">center</property>
+                       <property name="halign">center</property>
+                     </object>
+                   </child>
+                 </object>
+               </property>
+               <style>
+                 <class name="suggested-action"/>
+               </style>
+             </object>
+           </child>
+         </object>
+       </child>
+       <child>
+         <object class="GtkStack" id="main_stack">
+           <child>
+             <object class="GtkStackPage">
+               <property name="name">greeter</property>
+               <property name="child">
+                 <object class="AdwStatusPage">
+                   <property name="icon-name">welcome</property>
+                   <property name="title" translatable="yes">Welcome to Fractal</property>
+                   <child>
+                     <object class="GtkBox">
+                       <property name="orientation">vertical</property>
+                       <property name="spacing">18</property>
+                       <property name="halign">center</property>
+                       <child>
+                         <object class="GtkLabel" id="error_message">
+                           <property name="visible">False</property>
+                         </object>
+                       </child>
+                       <child>
+                         <object class="GtkListBox">
+                           <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>
+                           </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>
+                           </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>
+                               </property>
+                             </object>
+                           </child>
+                           <style>
+                             <class name="content"/>
+                             <class name="login"/>
+                           </style>
+                         </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>
+               </property>
+             </object>
+           </child>
+         </object>
+       </child>
       </object>
     </child>
   </template>
diff --git a/data/resources/ui/window.ui b/data/resources/ui/window.ui
index 95c2b884..3a83e4e6 100644
--- a/data/resources/ui/window.ui
+++ b/data/resources/ui/window.ui
@@ -5,15 +5,11 @@
     <property name="default-height">400</property>
     <child>
       <object class="GtkStack" id="main_stack">
-        <property name="visible-child">session</property>
+        <property name="visible-child">login</property>
         <property name="transition-type">crossfade</property>
         <child>
           <object class="FrctlLogin" id="login" />
         </child>
-        <!-- We currently only support one session, add other sessions to the main_Stack -->
-        <child>
-          <object class="FrctlSession" id="session" />
-        </child>
       </object>
     </child>
   </template>
diff --git a/src/login.rs b/src/login.rs
index bd3bfe4c..7f3c646d 100644
--- a/src/login.rs
+++ b/src/login.rs
@@ -1,18 +1,37 @@
+use crate::FrctlSession;
+
 use adw;
 use adw::subclass::prelude::BinImpl;
+use gettextrs::gettext;
 use gtk::subclass::prelude::*;
 use gtk::{self, prelude::*};
-use gtk::{glib, CompositeTemplate};
+use gtk::{glib, glib::clone, CompositeTemplate};
+use log::debug;
 
 mod imp {
     use super::*;
-    use glib::subclass::InitializingObject;
+    use glib::subclass::{InitializingObject, Signal};
+    use once_cell::sync::Lazy;
 
-    #[derive(Debug, CompositeTemplate)]
+    #[derive(Debug, Default, CompositeTemplate)]
     #[template(resource = "/org/gnome/FractalNext/login.ui")]
     pub struct FrctlLogin {
         #[template_child]
-        pub headerbar: TemplateChild<gtk::HeaderBar>,
+        pub next_stack: TemplateChild<gtk::Stack>,
+        #[template_child]
+        pub next_label: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub next_spinner: TemplateChild<gtk::Spinner>,
+        #[template_child]
+        pub main_stack: TemplateChild<gtk::Stack>,
+        #[template_child]
+        pub homeserver_entry: TemplateChild<gtk::Entry>,
+        #[template_child]
+        pub username_entry: TemplateChild<gtk::Entry>,
+        #[template_child]
+        pub password_entry: TemplateChild<gtk::PasswordEntry>,
+        #[template_child]
+        pub error_message: TemplateChild<gtk::Label>,
     }
 
     #[glib::object_subclass]
@@ -21,14 +40,10 @@ mod imp {
         type Type = super::FrctlLogin;
         type ParentType = adw::Bin;
 
-        fn new() -> Self {
-            Self {
-                headerbar: TemplateChild::default(),
-            }
-        }
-
         fn class_init(klass: &mut Self::Class) {
             Self::bind_template(klass);
+            klass.set_accessible_role(gtk::AccessibleRole::Group);
+            klass.install_action("login.next", None, move |widget, _, _| widget.forward());
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -36,8 +51,35 @@ mod imp {
         }
     }
 
-    impl ObjectImpl for FrctlLogin {}
+    impl ObjectImpl for FrctlLogin {
+        fn signals() -> &'static [Signal] {
+            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
+                vec![Signal::builder(
+                    "new-session",
+                    &[FrctlSession::static_type().into()],
+                    <()>::static_type().into(),
+                )
+                .build()]
+            });
+            SIGNALS.as_ref()
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            obj.action_set_enabled("login.next", false);
+
+            self.parent_constructed(obj);
+
+            self.homeserver_entry
+                .connect_changed(clone!(@weak obj => move |_| obj.enable_next_action()));
+            self.username_entry
+                .connect_changed(clone!(@weak obj => move |_| obj.enable_next_action()));
+            self.password_entry
+                .connect_changed(clone!(@weak obj => move |_| obj.enable_next_action()));
+        }
+    }
+
     impl WidgetImpl for FrctlLogin {}
+
     impl BinImpl for FrctlLogin {}
 }
 
@@ -50,4 +92,93 @@ impl FrctlLogin {
     pub fn new() -> Self {
         glib::Object::new(&[]).expect("Failed to create FrctlLogin")
     }
+
+    fn enable_next_action(&self) {
+        let priv_ = imp::FrctlLogin::from_instance(&self);
+        let homeserver = priv_.homeserver_entry.get_text();
+        let username = priv_.username_entry.get_text_length();
+        let password = priv_.password_entry.get_text().len();
+
+        self.action_set_enabled(
+            "login.next",
+            homeserver.len() != 0
+                && url::Url::parse(homeserver.as_str()).is_ok()
+                && username != 0
+                && password != 0,
+        );
+    }
+
+    fn forward(&self) {
+        self.login();
+    }
+
+    fn login(&self) {
+        let priv_ = imp::FrctlLogin::from_instance(&self);
+        let homeserver = priv_.homeserver_entry.get_text().to_string();
+        let username = priv_.username_entry.get_text().to_string();
+        let password = priv_.password_entry.get_text().to_string();
+
+        self.freeze();
+
+        let session = FrctlSession::new(homeserver);
+
+        session.connect_ready(clone!(@weak self as obj, @strong session => move |_| {
+            if let Some(error) = session.get_error() {
+                let error_message = &imp::FrctlLogin::from_instance(&obj).error_message;
+                // TODO: show more specific error
+                error_message.set_text(&gettext("⚠️ The Login failed."));
+                error_message.show();
+                debug!("Failed to create a new session: {:?}", error);
+
+                obj.unfreeze();
+            } else {
+                debug!("A new session is ready");
+                obj.emit_by_name("new-session", &[&session]).unwrap();
+                obj.clean();
+            }
+        }));
+
+        session.login_with_password(username, password);
+    }
+
+    fn clean(&self) {
+        let priv_ = imp::FrctlLogin::from_instance(&self);
+        priv_.homeserver_entry.set_text("");
+        priv_.username_entry.set_text("");
+        priv_.password_entry.set_text("");
+        self.unfreeze();
+    }
+
+    fn freeze(&self) {
+        let priv_ = imp::FrctlLogin::from_instance(&self);
+
+        self.action_set_enabled("login.next", false);
+        priv_
+            .next_stack
+            .set_visible_child(&priv_.next_spinner.get());
+        priv_.main_stack.set_sensitive(false);
+    }
+
+    fn unfreeze(&self) {
+        let priv_ = imp::FrctlLogin::from_instance(&self);
+
+        self.action_set_enabled("login.next", true);
+        priv_.next_stack.set_visible_child(&priv_.next_label.get());
+        priv_.main_stack.set_sensitive(true);
+    }
+
+    pub fn connect_new_session<F: Fn(&Self, &FrctlSession) + 'static>(
+        &self,
+        f: F,
+    ) -> glib::SignalHandlerId {
+        self.connect_local("new-session", true, move |values| {
+            let obj = values[0].get::<Self>().unwrap().unwrap();
+            let session = values[1].get::<FrctlSession>().unwrap().unwrap();
+
+            f(&obj, &session);
+
+            None
+        })
+        .unwrap()
+    }
 }
diff --git a/src/session/mod.rs b/src/session/mod.rs
index a387511a..57c9ee8a 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -3,15 +3,28 @@ mod sidebar;
 
 use self::content::FrctlContent;
 use self::sidebar::FrctlSidebar;
+
+use crate::RUNTIME;
+
 use adw;
 use adw::subclass::prelude::BinImpl;
 use gtk::subclass::prelude::*;
 use gtk::{self, prelude::*};
-use gtk::{glib, CompositeTemplate};
+use gtk::{glib, glib::clone, CompositeTemplate};
+use gtk_macros::send;
+use log::error;
+use matrix_sdk::api::r0::{
+    filter::{FilterDefinition, RoomFilter},
+    session::login,
+};
+use matrix_sdk::{self, Client, ClientConfig, SyncSettings};
+use std::time::Duration;
 
 mod imp {
     use super::*;
-    use glib::subclass::InitializingObject;
+    use glib::subclass::{InitializingObject, Signal};
+    use once_cell::sync::{Lazy, OnceCell};
+    use std::cell::RefCell;
 
     #[derive(Debug, CompositeTemplate)]
     #[template(resource = "/org/gnome/FractalNext/session.ui")]
@@ -20,6 +33,9 @@ mod imp {
         pub sidebar: TemplateChild<FrctlSidebar>,
         #[template_child]
         pub content: TemplateChild<FrctlContent>,
+        pub homeserver: OnceCell<String>,
+        /// Contains the error if something went wrong
+        pub error: RefCell<Option<matrix_sdk::Error>>,
     }
 
     #[glib::object_subclass]
@@ -32,6 +48,8 @@ mod imp {
             Self {
                 sidebar: TemplateChild::default(),
                 content: TemplateChild::default(),
+                homeserver: OnceCell::new(),
+                error: RefCell::new(None),
             }
         }
 
@@ -44,18 +62,210 @@ mod imp {
         }
     }
 
-    impl ObjectImpl for FrctlSession {}
+    impl ObjectImpl for FrctlSession {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpec::string(
+                    "homeserver",
+                    "Homeserver",
+                    "The matrix homeserver of this session",
+                    None,
+                    glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.get_name() {
+                "homeserver" => {
+                    let homeserver = value
+                        .get()
+                        .expect("type conformity checked by `Object::set_property`");
+                    let _ = self.homeserver.set(homeserver.unwrap());
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn get_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            pspec: &glib::ParamSpec,
+        ) -> glib::Value {
+            match pspec.get_name() {
+                "homeserver" => self.homeserver.get().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn signals() -> &'static [Signal] {
+            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
+                vec![Signal::builder("ready", &[], <()>::static_type().into()).build()]
+            });
+            SIGNALS.as_ref()
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+        }
+    }
     impl WidgetImpl for FrctlSession {}
     impl BinImpl for FrctlSession {}
 }
 
+/// Enum containing the supported methods to create a `FrctlSession`.
+#[derive(Clone, Debug)]
+enum CreationMethod {
+    /// Restore a previous session: `matrix_sdk::Session`
+    SessionRestore(matrix_sdk::Session),
+    /// Password Login: `username`, 'password`
+    Password(String, String),
+}
+
 glib::wrapper! {
     pub struct FrctlSession(ObjectSubclass<imp::FrctlSession>)
         @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
 }
 
 impl FrctlSession {
-    pub fn new() -> Self {
-        glib::Object::new(&[]).expect("Failed to create FrctlSession")
+    pub fn new(homeserver: String) -> Self {
+        glib::Object::new(&[("homeserver", &homeserver)]).expect("Failed to create FrctlSession")
+    }
+
+    pub fn login_with_password(&self, username: String, password: String) {
+        let method = CreationMethod::Password(username, password);
+        self.login(method);
+    }
+
+    pub fn login_with_previous_session(&self, session: matrix_sdk::Session) {
+        let method = CreationMethod::SessionRestore(session);
+        self.login(method);
+    }
+
+    fn login(&self, method: CreationMethod) {
+        let priv_ = &imp::FrctlSession::from_instance(self);
+        let homeserver = priv_.homeserver.get().unwrap();
+
+        let sender = self.setup();
+
+        let config = ClientConfig::new().timeout(Duration::from_secs(15));
+        // Please note the homeserver needs to be a valid url or the client will panic!
+        let client = Client::new_with_config(homeserver.as_str(), config);
+
+        if let Err(error) = client {
+            send!(sender, Err(error));
+            return;
+        }
+
+        let client = client.unwrap();
+
+        RUNTIME.block_on(async {
+            tokio::spawn(async move {
+                let success = match method {
+                    CreationMethod::SessionRestore(_session) => {
+                        todo!("Implement session restore")
+                    }
+                    CreationMethod::Password(username, password) => {
+                        // FIXME: client won't return if the homeserver isn't any real domain, I think
+                        // it has to do something with the dns lookup, therefore, we add a timeout of
+                        // 15s for the login and return a mocked Error.
+                        let response = tokio::time::timeout(
+                            Duration::from_secs(15),
+                            client.login(&username, &password, None, Some("Fractal Next")),
+                        )
+                        .await;
+
+                        if let Err(_) = response {
+                            send!(
+                                sender,
+                                Err(matrix_sdk::Error::Http(
+                                    matrix_sdk::HttpError::NotClientRequest
+                                ))
+                            );
+                            return;
+                        }
+
+                        let response = response.unwrap();
+
+                        let success = response.is_ok();
+                        send!(sender, response);
+                        success
+                    }
+                };
+
+                if success {
+                    // We need the filter or else left rooms won't be shown
+                    let mut room_filter = RoomFilter::empty();
+                    room_filter.include_leave = true;
+
+                    let mut filter = FilterDefinition::empty();
+                    filter.room = room_filter;
+
+                    let sync_settings = SyncSettings::new()
+                        .timeout(Duration::from_secs(30))
+                        .full_state(true)
+                        .filter(filter.into());
+                    client.sync(sync_settings).await;
+                }
+            });
+        });
+    }
+
+    fn setup(&self) -> glib::SyncSender<matrix_sdk::Result<login::Response>> {
+        let (sender, receiver) = glib::MainContext::sync_channel::<
+            matrix_sdk::Result<login::Response>,
+        >(Default::default(), 100);
+        receiver.attach(
+            None,
+            clone!(@weak self as obj => move |result| {
+                match result {
+                    Err(error) => {
+                        let priv_ = &imp::FrctlSession::from_instance(&obj);
+                        priv_.error.replace(Some(error));
+                    }
+                    Ok(response) => {
+                        // TODO: store this session to the SecretService so we can use it for the next login
+                        let _session = matrix_sdk::Session {
+                            access_token: response.access_token,
+                            user_id: response.user_id,
+                            device_id: response.device_id,
+                        };
+                    }
+                }
+
+                obj.emit_by_name("ready", &[]).unwrap();
+
+                glib::Continue(false)
+            }),
+        );
+        sender
+    }
+
+    /// Returns and consumes the `error` that was generated when the session failed to login,
+    /// on a successful login this will be `None`.
+    /// Unfortunatly it's not possible to connect the Error direclty to the `ready` signals.
+    pub fn get_error(&self) -> Option<matrix_sdk::Error> {
+        let priv_ = &imp::FrctlSession::from_instance(self);
+        priv_.error.take()
+    }
+
+    pub fn connect_ready<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_local("ready", true, move |values| {
+            let obj = values[0].get::<Self>().unwrap().unwrap();
+
+            f(&obj);
+
+            None
+        })
+        .unwrap()
     }
 }
diff --git a/src/window.rs b/src/window.rs
index 04905606..bbae15ee 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -6,7 +6,7 @@ use adw::subclass::prelude::AdwApplicationWindowImpl;
 use glib::signal::Inhibit;
 use gtk::subclass::prelude::*;
 use gtk::{self, prelude::*};
-use gtk::{gio, glib, CompositeTemplate};
+use gtk::{gio, glib, glib::clone, CompositeTemplate};
 use log::warn;
 
 mod imp {
@@ -20,10 +20,6 @@ mod imp {
         pub main_stack: TemplateChild<gtk::Stack>,
         #[template_child]
         pub login: TemplateChild<FrctlLogin>,
-        // Eventually we want to create the session dynamically, since we want multi account
-        // support
-        #[template_child]
-        pub session: TemplateChild<FrctlSession>,
         pub settings: gio::Settings,
     }
 
@@ -37,7 +33,6 @@ mod imp {
             Self {
                 main_stack: TemplateChild::default(),
                 login: TemplateChild::default(),
-                session: TemplateChild::default(),
                 settings: gio::Settings::new(APP_ID),
             }
         }
@@ -66,6 +61,9 @@ mod imp {
 
             // load latest window state
             obj.load_window_size();
+            self.login.connect_new_session(
+                clone!(@weak obj => move |_login, session| obj.add_session(session)),
+            );
         }
     }
 
@@ -95,6 +93,12 @@ impl FrctlWindow {
             .expect("Failed to create FrctlWindow")
     }
 
+    pub fn add_session(&self, session: &FrctlSession) {
+        let priv_ = &imp::FrctlWindow::from_instance(self);
+        priv_.main_stack.add_child(session);
+        priv_.main_stack.set_visible_child(session);
+    }
+
     pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
         let settings = &imp::FrctlWindow::from_instance(self).settings;
 


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