From d7e65d5cd3ca87b7ebc11cc5f6e9bfd4e9cefbfd Mon Sep 17 00:00:00 2001
From: "kali kaneko (leap communications)" <kali@leap.se>
Date: Mon, 27 Sep 2021 19:31:51 +0200
Subject: [PATCH] [ui] pseudo-load based on distance

---
 gui/components/Locations.qml   | 183 +++++++++++++++++++--------------
 gui/components/Preferences.qml |   4 +-
 gui/main.qml                   |  49 ++++-----
 pkg/vpn/bonafide/gateways.go   |   8 +-
 4 files changed, 136 insertions(+), 108 deletions(-)

diff --git a/gui/components/Locations.qml b/gui/components/Locations.qml
index a0e7a30c..ce510c5c 100644
--- a/gui/components/Locations.qml
+++ b/gui/components/Locations.qml
@@ -5,10 +5,8 @@ import QtGraphicalEffects 1.0
 
 import "../themes/themes.js" as Theme
 
+
 /* TODO
- [ ] build a better gateway object list (location, user-friendly, country, bridge, etc)
- [ ] ui: mark manual override icon somehow?
- [ ] ui: auto radiobutton should use also the bridge icon
  [ ] corner case: manual override, not full list yet
      [ ] persist bridges
      [ ] persist manual selection
@@ -17,13 +15,12 @@ import "../themes/themes.js" as Theme
      (I think the backend should discard any manual selection when selecting bridges...
       unless the current selection provides the bridge, in which case we can maintain it)
  */
-
 ThemedPage {
 
     id: locationPage
     title: qsTr("Select Location")
 
-    // TODO add ScrollIndicator 
+    // TODO add ScrollIndicator
     // https://doc.qt.io/qt-5.12//qml-qtquick-controls2-scrollindicator.html
 
     //: this is in the radio button for the auto selection
@@ -68,7 +65,7 @@ ThemedPage {
             WrappedRadioButton {
                 id: autoRadioButton
                 anchors.top: recommendedLabel.bottom
-                text: getAutoLabel() 
+                text: getAutoLabel()
                 ButtonGroup.group: locsel
                 checked: false
                 onClicked: {
@@ -84,85 +81,109 @@ ThemedPage {
         id: manualBox
         visible: root.locationsModel.length > 0
         width: root.width * 0.90
-        height: getManualBoxHeight()
         radius: 10
         color: Theme.fgColor
+        height: root.height * 0.60
+
         anchors {
             horizontalCenter: parent.horizontalCenter
             top: autoBox.bottom
             margins: 10
         }
-        Rectangle {
-            anchors {
-                fill: parent
-                margins: 10
-            }
-            Label {
-                id: manualLabel
-                text: manualSelectionLabel
-                font.bold: true
-            }
-            Label {
-                id: bridgeWarning
-                text: onlyBridgesWarning
-                color: "gray"
-                visible: isBridgeSelected()
-                wrapMode: Text.Wrap
-                anchors {
-                    topMargin: 5
-                    top: manualLabel.bottom
-                }
-                font.pixelSize: Theme.fontSize - 3
-            }
-            ColumnLayout {
-                id: gatewayListColumn
+
+        ScrollView {
+            id: frame
+            clip: true
+            anchors.fill: parent
+            ScrollBar.vertical.policy: ScrollBar.AlwaysOff
+
+            Flickable {
+                id: flickable
+                contentHeight: getManualBoxHeight()
                 width: parent.width
-                spacing: 1
-                anchors.top: getManualAnchor()
 
-                Repeater {
-                    id: gwManualSelectorList
-                    width: parent.width
-                    model: root.locationsModel
+                ScrollIndicator.vertical: ScrollIndicator {
+                    size: 5
+                    contentItem: Rectangle {
+                        implicitWidth: 5
+                        implicitHeight: 100
+                        color: "grey"
+                    }
+                }
 
-                    RowLayout {
+                Rectangle {
+                    anchors {
+                        fill: parent
+                        margins: 10
+                    }
+                    Label {
+                        id: manualLabel
+                        text: manualSelectionLabel
+                        font.bold: true
+                    }
+                    Label {
+                        id: bridgeWarning
+                        text: onlyBridgesWarning
+                        color: "gray"
+                        visible: isBridgeSelected()
+                        wrapMode: Text.Wrap
+                        anchors {
+                            topMargin: 5
+                            top: manualLabel.bottom
+                        }
+                        font.pixelSize: Theme.fontSize - 3
+                    }
+
+                    ColumnLayout {
+                        id: gatewayListColumn
                         width: parent.width
-                        WrappedRadioButton {
-                            text: getLocationLabel(modelData)
-                            location: modelData
-                            ButtonGroup.group: locsel
-                            checked: false
-                            enabled: locationPage.switching ? false : true
-                            onClicked: {
-                                if (ctx.status == "on") {
-                                    locationPage.switching = true
+                        spacing: 1
+                        anchors.top: getManualAnchor()
+
+                        Repeater {
+                            id: gwManualSelectorList
+                            width: parent.width
+                            model: root.locationsModel
+
+                            RowLayout {
+                                width: parent.width
+                                WrappedRadioButton {
+                                    text: getLocationLabel(modelData)
+                                    location: modelData
+                                    ButtonGroup.group: locsel
+                                    checked: false
+                                    enabled: locationPage.switching ? false : true
+                                    onClicked: {
+                                        if (ctx.status == "on") {
+                                            locationPage.switching = true
+                                        }
+                                        root.selectedGateway = location
+                                        backend.useLocation(location)
+                                    }
+                                }
+                                Item {
+                                    Layout.fillWidth: true
+                                }
+                                Image {
+                                    height: 16
+                                    width: 16
+                                    visible: isBridgeSelected()
+                                    source: "../resources/bridge.png"
+                                    Layout.alignment: Qt.AlignRight
+                                    Layout.rightMargin: 10
+                                }
+                                SignalIcon {
+                                    quality: getSignalFor(modelData)
+                                    Layout.alignment: Qt.AlignRight
+                                    Layout.rightMargin: 20
                                 }
-                                root.selectedGateway = location
-                                backend.useLocation(location)
                             }
                         }
-                        Item {
-                            Layout.fillWidth: true
-                        }
-                        Image {
-                            height: 16
-                            width: 16
-                            visible: isBridgeSelected()
-                            source: "../resources/bridge.png"
-                            Layout.alignment: Qt.AlignRight
-                            Layout.rightMargin: 10
-                        }
-			SignalIcon {
-                            // TODO mocked!
-			    quality: getSignalFor(modelData)
-			    Layout.alignment: Qt.AlignRight
-			    Layout.rightMargin: 20
-			}
                     }
                 }
-            }
-        }
-    }
+            } //flickable
+        } // scrollview
+    } // manualbox
 
     StateGroup {
         states: [
@@ -199,7 +220,9 @@ ThemedPage {
     }
 
     function getLocationLabel(location) {
-        if (!ctx) { return ""}
+        if (!ctx) {
+            return ""
+        }
         let l = ctx.locationLabels[location]
         return l[0] + ", " + l[1]
     }
@@ -213,13 +236,13 @@ ThemedPage {
     }
 
     function getSignalFor(location) {
-        switch(location) {
-        case "amsterdam":
-        case "paris":
+        // this is an ad-hoc solution for the no-menshen, riseup case.
+        // when menshen is deployed we'll want to tweak the values for each bucket.
+        let load = ctx.locations[location]
+        switch (true) {
+        case (load > 0.5):
             return "good"
-        case "newyork":
-            return "medium"
-        case "montreal":
+        case (load > 0.25):
             return "medium"
         default:
             return "low"
@@ -244,14 +267,14 @@ ThemedPage {
 
     Component.onCompleted: {
         if (root.selectedGateway == "auto") {
-            autoRadioButton.checked = true;
+            autoRadioButton.checked = true
         } else {
             let match = false
-            for (var i=1; i<locsel.buttons.length; i++) {
+            for (var i = 1; i < locsel.buttons.length; i++) {
                 let b = locsel.buttons[i]
                 if (b.location == root.selectedGateway) {
-                    match = true;
-                    b.checked = true;
+                    match = true
+                    b.checked = true
                 }
             }
         }
diff --git a/gui/components/Preferences.qml b/gui/components/Preferences.qml
index 2b0aae8a..cc251014 100644
--- a/gui/components/Preferences.qml
+++ b/gui/components/Preferences.qml
@@ -34,9 +34,9 @@ ThemedPage {
             Rectangle {
                 id: turnOffWarning
                 visible: false
-                height: 40
+                height: 20
                 width: parent.width
-                color: Theme.bgColor
+                color: "white"
 
 
                 Label {
diff --git a/gui/main.qml b/gui/main.qml
index d8bfb91f..00246703 100644
--- a/gui/main.qml
+++ b/gui/main.qml
@@ -2,21 +2,11 @@
 
 /*
  TODO (ui rewrite)
- - [x] add systray
- - [x] systray status
- - [x] splash screen
- - [x] splash delay/transitions
- - [x] font: monserrat
- - [x] nested states
- - [x] splash init errors
- - [x] gateway selector
- - [ ] bridges
+ See https://0xacab.org/leap/bitmask-vpn/-/issues/523
+ - [x] udp support
  - [ ] minimize/hide from systray
  - [ ] control actions from systray
  - [ ] add gateway to systray
- - [ ] donation dialog
- - [ ] parse ctx flags (need dialog, etc)
- - [ ] udp support
 */
 import QtQuick 2.0
 import QtQuick.Controls 2.4
@@ -42,7 +32,6 @@ ApplicationWindow {
     minimumHeight: appHeight
     maximumHeight: appHeight
 
-
     title: ctx ? ctx.appName : "VPN"
     Material.accent: Material.Green
 
@@ -56,6 +45,7 @@ ApplicationWindow {
     // TODO get from persistance
     property var selectedGateway: "auto"
 
+    // FIXME get svg icons
     property var icons: {
         "off": "qrc:/assets/icon/png/white/vpn_off.png",
         "on": "qrc:/assets/icon/png/white/vpn_on.png",
@@ -90,7 +80,6 @@ ApplicationWindow {
         id: systray
     }
 
-
     Connections {
         target: jsonModel
         function onDataChanged() {
@@ -100,7 +89,8 @@ ApplicationWindow {
             }
             ctx = JSON.parse(j)
             if (ctx != undefined) {
-                locationsModel = Object.keys(ctx.locations)
+                locationsModel = getSortedLocations()
+                console.debug("Got sorted locations:" + locationsModel)
             }
             if (ctx.errors) {
                 console.debug("errors, setting root.error")
@@ -109,10 +99,10 @@ ApplicationWindow {
                 root.error = ""
             }
             if (ctx.donateURL) {
-                isDonationService = true;
+                isDonationService = true
             }
             if (ctx.donateDialog == 'true') {
-                showDonationReminder = true;
+                showDonationReminder = true
             }
 
             // TODO check donation
@@ -122,15 +112,9 @@ ApplicationWindow {
             //    // move this to onClick of "close" for widget
             //    backend.donateSeen();
             //}
-            // TODO refactor donate widget into main view (with close window!)
-            //if (ctx.status == "on") {
-            //    gwNextConnectionText.visible = false
-            //    gwReconnectText.visible = false
-            // when: vpn.status == "on"
-            //}
 
             /*
-            TODO libraries need login 
+            TODO libraries need login
             if (ctx.loginDialog == 'true') {
                 login.visible = true
             }
@@ -141,6 +125,23 @@ ApplicationWindow {
         }
     }
 
+    function getSortedLocations() {
+        let obj = ctx.locations
+        var arr = []
+        for (var prop in obj) {
+            if (obj.hasOwnProperty(prop)) {
+                arr.push({
+                             "key": prop,
+                             "value": obj[prop]
+                         })
+            }
+        }
+        arr.sort(function (a, b) {
+            return a.value - b.value
+        }).reverse()
+        return Array.from(arr, (k,_) => k.key);
+    }
+
     onSceneGraphError: function (error, msg) {
         console.debug("ERROR while initializing scene")
         console.debug(msg)
diff --git a/pkg/vpn/bonafide/gateways.go b/pkg/vpn/bonafide/gateways.go
index 4299bb25..633493e0 100644
--- a/pkg/vpn/bonafide/gateways.go
+++ b/pkg/vpn/bonafide/gateways.go
@@ -96,14 +96,18 @@ func (p *gatewayPool) listLocationFullness(transport string) map[string]float64
 		return cm
 	}
 	if len(p.recommended) != 0 {
-		for _, gw := range p.recommended {
+		for idx, gw := range p.recommended {
 			if gw.gateway.Transport != transport {
 				continue
 			}
 			if _, ok := cm[gw.gateway.Location]; ok {
 				continue
 			}
-			cm[gw.gateway.Location] = gw.Fullness
+			if gw.Fullness != -1 {
+				cm[gw.gateway.Location] = gw.Fullness
+			} else {
+				cm[gw.gateway.Location] = 1 - float64(idx)/float64(len(p.recommended))
+			}
 		}
 	} else {
 		for _, location := range locations {
-- 
GitLab