From 8fa6f4f9fdab1210845c78d4282d3f024955378a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Aug 2021 17:20:31 +0200 Subject: [PATCH 001/108] add lint command for typescript and detect missing await --- .ts-eslintrc.js | 29 +++++ package.json | 5 +- yarn.lock | 285 ++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 282 insertions(+), 37 deletions(-) create mode 100644 .ts-eslintrc.js diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js new file mode 100644 index 00000000..6b81ad99 --- /dev/null +++ b/.ts-eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + root: true, + env: { + "browser": true, + "es6": true + }, + extends: [ + "eslint:recommended" + // "plugin:@typescript-eslint/recommended", + // "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + parser: '@typescript-eslint/parser', + parserOptions: { + "ecmaVersion": 2020, + "sourceType": "module", + "project": "./tsconfig.json" + }, + plugins: [ + '@typescript-eslint', + ], + rules: { + "no-console": "off", + "no-empty": "off", + "no-prototype-builtins": "off", + "no-unused-vars": "warn", + "@typescript-eslint/no-floating-promises": 2, + "@typescript-eslint/no-misused-promises": 2 + } +}; diff --git a/package.json b/package.json index ad1e7489..bc4fb969 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "scripts": { "lint": "eslint --cache src/", + "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", "lint-ci": "eslint src/", "test": "impunity --entry-point src/main.js --force-esm-dirs lib/ src/", "start": "snowpack dev --port 3000", @@ -32,11 +33,13 @@ "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-multi-entry": "^4.0.0", "@rollup/plugin-node-resolve": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^4.29.2", + "@typescript-eslint/parser": "^4.29.2", "autoprefixer": "^10.2.6", "cheerio": "^1.0.0-rc.3", "commander": "^6.0.0", "core-js": "^3.6.5", - "eslint": "^7.25.0", + "eslint": "^7.32.0", "fake-indexeddb": "^3.1.2", "finalhandler": "^1.1.1", "impunity": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index 7e547281..638e0cd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -867,25 +867,60 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@eslint/eslintrc@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547" - integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog== +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== dependencies: ajv "^6.12.4" debug "^4.1.1" espree "^7.3.0" - globals "^12.1.0" + globals "^13.9.0" ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" + integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": version "3.2.3" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@npmcli/arborist@^2.6.4": version "2.8.0" resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-2.8.0.tgz#ff078287eba44595383eb58ad8aa8540bc8aae9e" @@ -1154,6 +1189,11 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== +"@types/json-schema@^7.0.7": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + "@types/keyv@*": version "3.1.2" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.2.tgz#5d97bb65526c20b6e0845f6b0d2ade4f28604ee5" @@ -1185,6 +1225,75 @@ dependencies: "@types/node" "*" +"@typescript-eslint/eslint-plugin@^4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.2.tgz#f54dc0a32b8f61c6024ab8755da05363b733838d" + integrity sha512-x4EMgn4BTfVd9+Z+r+6rmWxoAzBaapt4QFqE+d8L8sUtYZYLDTK6VG/y/SMMWA5t1/BVU5Kf+20rX4PtWzUYZg== + dependencies: + "@typescript-eslint/experimental-utils" "4.29.2" + "@typescript-eslint/scope-manager" "4.29.2" + debug "^4.3.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.1.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.2.tgz#5f67fb5c5757ef2cb3be64817468ba35c9d4e3b7" + integrity sha512-P6mn4pqObhftBBPAv4GQtEK7Yos1fz/MlpT7+YjH9fTxZcALbiiPKuSIfYP/j13CeOjfq8/fr9Thr2glM9ub7A== + dependencies: + "@types/json-schema" "^7.0.7" + "@typescript-eslint/scope-manager" "4.29.2" + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/typescript-estree" "4.29.2" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/parser@^4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.2.tgz#1c7744f4c27aeb74610c955d3dce9250e95c370a" + integrity sha512-WQ6BPf+lNuwteUuyk1jD/aHKqMQ9jrdCn7Gxt9vvBnzbpj7aWEf+aZsJ1zvTjx5zFxGCt000lsbD9tQPEL8u6g== + dependencies: + "@typescript-eslint/scope-manager" "4.29.2" + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/typescript-estree" "4.29.2" + debug "^4.3.1" + +"@typescript-eslint/scope-manager@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.2.tgz#442b0f029d981fa402942715b1718ac7fcd5aa1b" + integrity sha512-mfHmvlQxmfkU8D55CkZO2sQOueTxLqGvzV+mG6S/6fIunDiD2ouwsAoiYCZYDDK73QCibYjIZmGhpvKwAB5BOA== + dependencies: + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/visitor-keys" "4.29.2" + +"@typescript-eslint/types@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.2.tgz#fc0489c6b89773f99109fb0aa0aaddff21f52fcd" + integrity sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ== + +"@typescript-eslint/typescript-estree@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.2.tgz#a0ea8b98b274adbb2577100ba545ddf8bf7dc219" + integrity sha512-TJ0/hEnYxapYn9SGn3dCnETO0r+MjaxtlWZ2xU+EvytF0g4CqTpZL48SqSNn2hXsPolnewF30pdzR9a5Lj3DNg== + dependencies: + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/visitor-keys" "4.29.2" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/visitor-keys@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz#d2da7341f3519486f50655159f4e5ecdcb2cd1df" + integrity sha512-bDgJLQ86oWHJoZ1ai4TZdgXzJxsea3Ee9u9wsTAvjChdj2WLcVsgWYAPeY7RQMn16tKrlQaBnpKv7KBfs4EQag== + dependencies: + "@typescript-eslint/types" "4.29.2" + eslint-visitor-keys "^2.0.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1321,6 +1430,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -1461,7 +1575,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1874,7 +1988,7 @@ debug@2.6.9, debug@^2.6.0: dependencies: ms "2.0.0" -debug@4: +debug@4, debug@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -1979,6 +2093,13 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -2214,6 +2335,11 @@ escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + esinstall@^1.0.0, esinstall@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/esinstall/-/esinstall-1.1.7.tgz#ceabeb4b8685bf48c805a503e292dfafe4e0cb22" @@ -2255,6 +2381,13 @@ eslint-utils@^2.1.0: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" @@ -2265,28 +2398,31 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint@^7.25.0: - version "7.25.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.25.0.tgz#1309e4404d94e676e3e831b3a3ad2b050031eb67" - integrity sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw== +eslint@^7.32.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== dependencies: "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.0" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" enquirer "^2.3.5" + escape-string-regexp "^4.0.0" eslint-scope "^5.1.1" eslint-utils "^2.1.0" eslint-visitor-keys "^2.0.0" espree "^7.3.1" esquery "^1.4.0" esutils "^2.0.2" + fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" + glob-parent "^5.1.2" globals "^13.6.0" ignore "^4.0.6" import-fresh "^3.0.0" @@ -2295,7 +2431,7 @@ eslint@^7.25.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.21" + lodash.merge "^4.6.2" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -2304,7 +2440,7 @@ eslint@^7.25.0: semver "^7.2.1" strip-ansi "^6.0.0" strip-json-comments "^3.1.0" - table "^6.0.4" + table "^6.0.9" text-table "^0.2.0" v8-compile-cache "^2.0.3" @@ -2419,11 +2555,22 @@ fake-indexeddb@^3.1.2: realistic-structured-clone "^2.0.1" setimmediate "^1.0.5" -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.1.1: + version "3.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -2434,6 +2581,13 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastq@^1.6.0: + version "1.11.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.1.tgz#5d8175aae17db61947f8b162cfc7f63264d22807" + integrity sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw== + dependencies: + reusify "^1.0.4" + fdir@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-5.1.0.tgz#973e4934e6a3666b59ebdfc56f60bb8e9b16acb8" @@ -2605,7 +2759,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob-parent@^5.0.0, glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2641,13 +2795,6 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== - dependencies: - type-fest "^0.8.1" - globals@^13.6.0: version "13.8.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3" @@ -2655,6 +2802,25 @@ globals@^13.6.0: dependencies: type-fest "^0.20.2" +globals@^13.9.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7" + integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.3: + version "11.0.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + got@^11.1.4: version "11.8.2" resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" @@ -2847,6 +3013,11 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -3239,6 +3410,11 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -3254,7 +3430,7 @@ lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3332,11 +3508,24 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + meriyah@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/meriyah/-/meriyah-3.1.6.tgz#56c9c0edb63f9640c7609a39a413c60b038e4451" integrity sha512-JDOSi6DIItDc33U5N52UdV6P8v+gn+fqZKfbAfHzdWApRQyQWdcvxPvAr9t01bI2rBxGvSrKRQSCg3SkZC1qeg== +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + mime-db@1.49.0, "mime-db@>= 1.43.0 < 2": version "1.49.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" @@ -3921,7 +4110,7 @@ periscopic@^2.0.3: estree-walker "^2.0.2" is-reference "^1.1.4" -picomatch@^2.0.4, picomatch@^2.3.0: +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== @@ -4153,6 +4342,11 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" @@ -4365,6 +4559,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -4415,6 +4614,13 @@ rollup@~2.37.1: optionalDependencies: fsevents "~2.1.2" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -4541,7 +4747,7 @@ skypack@^0.3.0: rollup "^2.23.0" validate-npm-package-name "^3.0.0" -slash@~3.0.0: +slash@^3.0.0, slash@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== @@ -4794,10 +5000,10 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -table@^6.0.4: - version "6.7.0" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.0.tgz#26274751f0ee099c547f6cb91d3eff0d61d155b2" - integrity sha512-SAM+5p6V99gYiiy2gT5ArdzgM1dLDed0nkrWmG6Fry/bUS/m9x83BwpJUOf1Qj/x2qJd+thL6IkIx7qPGRxqBw== +table@^6.0.9: + version "6.7.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" + integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== dependencies: ajv "^8.0.1" lodash.clonedeep "^4.5.0" @@ -4865,11 +5071,23 @@ treeverse@^1.0.4: resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-1.0.4.tgz#a6b0ebf98a1bca6846ddc7ecbc900df08cb9cd5f" integrity sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g== +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -4894,11 +5112,6 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" From e2cdadc71417b5d54181c00759c097a12ed4598b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Aug 2021 17:42:12 +0200 Subject: [PATCH 002/108] don't use any non-ts eslint rules, they don't understand abstract method --- .ts-eslintrc.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js index 6b81ad99..43392aef 100644 --- a/.ts-eslintrc.js +++ b/.ts-eslintrc.js @@ -5,9 +5,8 @@ module.exports = { "es6": true }, extends: [ - "eslint:recommended" - // "plugin:@typescript-eslint/recommended", - // "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", ], parser: '@typescript-eslint/parser', parserOptions: { @@ -18,12 +17,5 @@ module.exports = { plugins: [ '@typescript-eslint', ], - rules: { - "no-console": "off", - "no-empty": "off", - "no-prototype-builtins": "off", - "no-unused-vars": "warn", - "@typescript-eslint/no-floating-promises": 2, - "@typescript-eslint/no-misused-promises": 2 - } + rules: {} }; From e1481f2947ba6c9890bd100d9f862f0188bd9750 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Aug 2021 17:46:37 +0200 Subject: [PATCH 003/108] run typescript checks in ci --- .github/workflows/codechecks.js.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codechecks.js.yml b/.github/workflows/codechecks.js.yml index 2e15c8e7..733e8b55 100644 --- a/.github/workflows/codechecks.js.yml +++ b/.github/workflows/codechecks.js.yml @@ -42,4 +42,8 @@ jobs: - name: Unit tests run: yarn test - name: Lint - run: yarn run lint-ci \ No newline at end of file + run: yarn run lint-ci + - name: Typescript + run: yarn run tsc + - name: Typescript lint + run: yarn run lint-ts From 4b5f5ddffa56484bf678087c8086debdc60e86ad Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Aug 2021 18:15:03 +0200 Subject: [PATCH 004/108] fix ts lint error by making methods abstract --- src/matrix/room/timeline/entries/BaseEntry.ts | 21 +++++++------------ .../room/timeline/entries/EventEntry.js | 1 - 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/matrix/room/timeline/entries/BaseEntry.ts b/src/matrix/room/timeline/entries/BaseEntry.ts index c592ecb6..1ef6a863 100644 --- a/src/matrix/room/timeline/entries/BaseEntry.ts +++ b/src/matrix/room/timeline/entries/BaseEntry.ts @@ -22,20 +22,15 @@ interface FragmentIdComparer { compare: (a: number, b: number) => number } -export class BaseEntry { - protected _fragmentIdComparer: FragmentIdComparer - - constructor(fragmentIdComparer: FragmentIdComparer) { - this._fragmentIdComparer = fragmentIdComparer; +export abstract class BaseEntry { + constructor( + protected readonly _fragmentIdComparer: FragmentIdComparer + ) { } - get fragmentId(): number { - throw new Error("unimplemented"); - } - - get entryIndex(): number { - throw new Error("unimplemented"); - } + abstract get fragmentId(): number; + abstract get entryIndex(): number; + abstract updateFrom(other: BaseEntry): void; compare(otherEntry: BaseEntry): number { if (this.fragmentId === otherEntry.fragmentId) { @@ -53,6 +48,4 @@ export class BaseEntry { asEventKey(): EventKey { return new EventKey(this.fragmentId, this.entryIndex); } - - updateFrom(other: BaseEntry) {} } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 55219cd6..89d3f379 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -33,7 +33,6 @@ export class EventEntry extends BaseEventEntry { } updateFrom(other) { - super.updateFrom(other); if (other._decryptionResult && !this._decryptionResult) { this._decryptionResult = other._decryptionResult; } From e0fcec910220bfb9965b1f0c1ae6e91a93f414f9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 27 Jul 2021 19:23:46 +0530 Subject: [PATCH 005/108] Add method to query login types Signed-off-by: RMidhunSuresh --- src/domain/LoginViewModel.js | 1 + src/matrix/SessionContainer.js | 6 ++++++ src/matrix/net/HomeServerApi.js | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 1b52e1a5..9e1db503 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -49,6 +49,7 @@ export class LoginViewModel extends ViewModel { this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ createAndStartSessionContainer: () => { this._sessionContainer = this._createSessionContainer(); + this._sessionContainer.queryLogin(homeserver); this._sessionContainer.startWithLogin(homeserver, username, password); return this._sessionContainer; }, diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 07c4a870..1106644c 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -97,6 +97,12 @@ export class SessionContainer { }); } + async queryLogin(homeServer) { + homeServer = normalizeHomeserver(homeServer); + const hsApi = new HomeServerApi({homeServer, request: this._platform.request}); + const response = hsApi.queryLogin(); + } + async startWithLogin(homeServer, username, password) { if (this._status.get() !== LoadStatus.NotLoading) { return; diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index a9b63f8e..6dec6bd6 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -134,6 +134,10 @@ export class HomeServerApi { return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); } + queryLogin() { + return this._unauthedRequest("GET", this._url("/login"), null, null, null); + } + passwordLogin(username, password, initialDeviceDisplayName, options = null) { return this._unauthedRequest("POST", this._url("/login"), null, { "type": "m.login.password", From 20765d96885948abe9f163ec4a962edeb46ab1e9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 13:34:25 +0530 Subject: [PATCH 006/108] Create LoginMethod for password login Signed-off-by: RMidhunSuresh --- src/matrix/LoginMethod.js | 10 ++++++++++ src/matrix/PasswordLoginMethod.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/matrix/LoginMethod.js create mode 100644 src/matrix/PasswordLoginMethod.js diff --git a/src/matrix/LoginMethod.js b/src/matrix/LoginMethod.js new file mode 100644 index 00000000..0d57499e --- /dev/null +++ b/src/matrix/LoginMethod.js @@ -0,0 +1,10 @@ +export class LoginMethod { + constructor({homeServer, platform}) { + this.homeServer = homeServer; + this._platform = platform; + } + + async login(hsApi, deviceName) { + throw("Not Implemented"); + } +} diff --git a/src/matrix/PasswordLoginMethod.js b/src/matrix/PasswordLoginMethod.js new file mode 100644 index 00000000..81f2204f --- /dev/null +++ b/src/matrix/PasswordLoginMethod.js @@ -0,0 +1,15 @@ +import { LoginMethod } from "./LoginMethod.js"; + +export class PasswordLoginMethod extends LoginMethod { + constructor(options) { + super(options); + this.username = options.username; + this.password = options.password; + } + + async login(hsApi, deviceName) { + return this._platform.logger.run("passwordLogin", async log => + await hsApi.passwordLogin(this.username, this.password, deviceName, {log}).response() + ); + } +} From a53e29767f8d69a03c68dcd7128d77c883400dd5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 13:45:35 +0530 Subject: [PATCH 007/108] Rewrite password login to use PasswordLoginMethod Signed-off-by: RMidhunSuresh --- src/domain/LoginViewModel.js | 8 +++++--- src/domain/SessionLoadViewModel.js | 2 +- src/matrix/SessionContainer.js | 24 ++++++++++++++++++++---- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 9e1db503..4e9993aa 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -47,10 +47,12 @@ export class LoginViewModel extends ViewModel { this._loadViewModel = this.disposeTracked(this._loadViewModel); } this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ - createAndStartSessionContainer: () => { + createAndStartSessionContainer: async () => { this._sessionContainer = this._createSessionContainer(); - this._sessionContainer.queryLogin(homeserver); - this._sessionContainer.startWithLogin(homeserver, username, password); + const loginOptions = await this._sessionContainer.queryLogin(homeserver); + if (loginOptions.password) { + this._sessionContainer.startWithLogin(loginOptions.password(username, password)); + } return this._sessionContainer; }, ready: sessionContainer => { diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 0b785e47..83bf5966 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -38,7 +38,7 @@ export class SessionLoadViewModel extends ViewModel { try { this._loading = true; this.emitChange("loading"); - this._sessionContainer = this._createAndStartSessionContainer(); + this._sessionContainer = await this._createAndStartSessionContainer(); this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { this.emitChange("loadLabel"); // wait for initial sync, but not catchup sync diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 1106644c..cfc83255 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -23,6 +23,7 @@ import {MediaRepository} from "./net/MediaRepository.js"; import {RequestScheduler} from "./net/RequestScheduler.js"; import {Sync, SyncStatus} from "./Sync.js"; import {Session} from "./Session.js"; +import {PasswordLoginMethod} from "./PasswordLoginMethod.js"; export const LoadStatus = createEnum( "NotLoading", @@ -97,25 +98,40 @@ export class SessionContainer { }); } + parseLoginOptions(options, homeServer) { + /* Take server response and return new object which has two props password and sso which + implements LoginMethod + */ + const flows = options.flows; + const result = {}; + for (const flow of flows) { + if (flow.type === "m.login.password") { + result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password, platform: this._platform}); + } + } + return result; + } + async queryLogin(homeServer) { homeServer = normalizeHomeserver(homeServer); const hsApi = new HomeServerApi({homeServer, request: this._platform.request}); - const response = hsApi.queryLogin(); + const response = await hsApi.queryLogin().response(); + return this.parseLoginOptions(response, homeServer); } - async startWithLogin(homeServer, username, password) { + async startWithLogin(loginMethod) { if (this._status.get() !== LoadStatus.NotLoading) { return; } await this._platform.logger.run("login", async log => { this._status.set(LoadStatus.Login); - homeServer = normalizeHomeserver(homeServer); const clock = this._platform.clock; let sessionInfo; try { const request = this._platform.request; + const homeServer = normalizeHomeserver(loginMethod.homeServer); const hsApi = new HomeServerApi({homeServer, request}); - const loginData = await hsApi.passwordLogin(username, password, "Hydrogen", {log}).response(); + const loginData = await loginMethod.login(hsApi, "Hydrogen", log); const sessionId = this.createNewSessionId(); sessionInfo = { id: sessionId, From acfe3f30db265af82cd7092ae6f5eeae2ac76b80 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 14:06:32 +0530 Subject: [PATCH 008/108] Make lint happy Signed-off-by: RMidhunSuresh --- src/matrix/LoginMethod.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/LoginMethod.js b/src/matrix/LoginMethod.js index 0d57499e..51f3754a 100644 --- a/src/matrix/LoginMethod.js +++ b/src/matrix/LoginMethod.js @@ -4,6 +4,7 @@ export class LoginMethod { this._platform = platform; } + // eslint-disable-next-line no-unused-vars async login(hsApi, deviceName) { throw("Not Implemented"); } From 72fb7f679b31c131fd535d3889312ffc2c7ba5f2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 14:07:18 +0530 Subject: [PATCH 009/108] Add license headers Signed-off-by: RMidhunSuresh --- src/matrix/LoginMethod.js | 16 ++++++++++++++++ src/matrix/PasswordLoginMethod.js | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/matrix/LoginMethod.js b/src/matrix/LoginMethod.js index 51f3754a..9ec51a90 100644 --- a/src/matrix/LoginMethod.js +++ b/src/matrix/LoginMethod.js @@ -1,3 +1,19 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + export class LoginMethod { constructor({homeServer, platform}) { this.homeServer = homeServer; diff --git a/src/matrix/PasswordLoginMethod.js b/src/matrix/PasswordLoginMethod.js index 81f2204f..155542a6 100644 --- a/src/matrix/PasswordLoginMethod.js +++ b/src/matrix/PasswordLoginMethod.js @@ -1,3 +1,19 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import { LoginMethod } from "./LoginMethod.js"; export class PasswordLoginMethod extends LoginMethod { From 46b7d9a3737d184767c7ab984a1d478dc4e040ab Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 14:11:00 +0530 Subject: [PATCH 010/108] Add explaining comment Signed-off-by: RMidhunSuresh --- src/matrix/LoginMethod.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/LoginMethod.js b/src/matrix/LoginMethod.js index 9ec51a90..2dbab85e 100644 --- a/src/matrix/LoginMethod.js +++ b/src/matrix/LoginMethod.js @@ -22,6 +22,10 @@ export class LoginMethod { // eslint-disable-next-line no-unused-vars async login(hsApi, deviceName) { + /* + Regardless of the login method, SessionContainer.startWithLogin() + can do SomeLoginMethod.login() + */ throw("Not Implemented"); } } From 730a6b2d0aaf1bf5836e8a7d9bf9556635f8a8d3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 23:23:52 +0530 Subject: [PATCH 011/108] Move files to separate directory Signed-off-by: RMidhunSuresh --- src/matrix/SessionContainer.js | 2 +- src/matrix/{ => login}/LoginMethod.js | 0 src/matrix/{ => login}/PasswordLoginMethod.js | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/matrix/{ => login}/LoginMethod.js (100%) rename src/matrix/{ => login}/PasswordLoginMethod.js (100%) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index cfc83255..4b112080 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -23,7 +23,7 @@ import {MediaRepository} from "./net/MediaRepository.js"; import {RequestScheduler} from "./net/RequestScheduler.js"; import {Sync, SyncStatus} from "./Sync.js"; import {Session} from "./Session.js"; -import {PasswordLoginMethod} from "./PasswordLoginMethod.js"; +import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js"; export const LoadStatus = createEnum( "NotLoading", diff --git a/src/matrix/LoginMethod.js b/src/matrix/login/LoginMethod.js similarity index 100% rename from src/matrix/LoginMethod.js rename to src/matrix/login/LoginMethod.js diff --git a/src/matrix/PasswordLoginMethod.js b/src/matrix/login/PasswordLoginMethod.js similarity index 100% rename from src/matrix/PasswordLoginMethod.js rename to src/matrix/login/PasswordLoginMethod.js From 9651817c5bef3699bfb4cd6dad6e36f7b764aa3c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 23:24:32 +0530 Subject: [PATCH 012/108] Formatting fix Signed-off-by: RMidhunSuresh --- src/matrix/login/PasswordLoginMethod.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/login/PasswordLoginMethod.js b/src/matrix/login/PasswordLoginMethod.js index 155542a6..abe9c23d 100644 --- a/src/matrix/login/PasswordLoginMethod.js +++ b/src/matrix/login/PasswordLoginMethod.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LoginMethod } from "./LoginMethod.js"; +import {LoginMethod} from "./LoginMethod.js"; export class PasswordLoginMethod extends LoginMethod { constructor(options) { From f3946fcdf37b2470871ffdcc569f34ececa71527 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 2 Aug 2021 15:30:21 +0530 Subject: [PATCH 013/108] Pass log as argument Signed-off-by: RMidhunSuresh --- src/matrix/SessionContainer.js | 2 +- src/matrix/login/LoginMethod.js | 5 ++--- src/matrix/login/PasswordLoginMethod.js | 6 ++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 4b112080..85aa979c 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -106,7 +106,7 @@ export class SessionContainer { const result = {}; for (const flow of flows) { if (flow.type === "m.login.password") { - result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password, platform: this._platform}); + result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password}); } } return result; diff --git a/src/matrix/login/LoginMethod.js b/src/matrix/login/LoginMethod.js index 2dbab85e..fee8e845 100644 --- a/src/matrix/login/LoginMethod.js +++ b/src/matrix/login/LoginMethod.js @@ -15,13 +15,12 @@ limitations under the License. */ export class LoginMethod { - constructor({homeServer, platform}) { + constructor({homeServer}) { this.homeServer = homeServer; - this._platform = platform; } // eslint-disable-next-line no-unused-vars - async login(hsApi, deviceName) { + async login(hsApi, deviceName, log) { /* Regardless of the login method, SessionContainer.startWithLogin() can do SomeLoginMethod.login() diff --git a/src/matrix/login/PasswordLoginMethod.js b/src/matrix/login/PasswordLoginMethod.js index abe9c23d..5c90ccf8 100644 --- a/src/matrix/login/PasswordLoginMethod.js +++ b/src/matrix/login/PasswordLoginMethod.js @@ -23,9 +23,7 @@ export class PasswordLoginMethod extends LoginMethod { this.password = options.password; } - async login(hsApi, deviceName) { - return this._platform.logger.run("passwordLogin", async log => - await hsApi.passwordLogin(this.username, this.password, deviceName, {log}).response() - ); + async login(hsApi, deviceName, log) { + return await hsApi.passwordLogin(this.username, this.password, deviceName, {log}).response(); } } From 18e1c305f5da4804ceac7008370a45a986729cec Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 22:51:54 +0530 Subject: [PATCH 014/108] Allow sso to be a root segment Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index dbac16ac..46839239 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -30,7 +30,7 @@ function allowsChild(parent, child) { switch (parent?.type) { case undefined: // allowed root segments - return type === "login" || type === "session"; + return type === "login" || type === "session" || type === "sso"; case "session": return type === "room" || type === "rooms" || type === "settings"; case "rooms": From 19664e54be7dddadf2516c32fcaea48a60b258b5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 22:52:40 +0530 Subject: [PATCH 015/108] Parse loginToken from query parameter Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 46839239..a3b587df 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -152,6 +152,10 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { const userId = iterator.next().value; if (!userId) { break; } pushRightPanelSegment(segments, type, userId); + } else if (type.includes("loginToken")) { + // Special case for SSO-login with query parameter loginToken= + const loginToken = type.split("=").pop(); + segments.push(new Segment("sso", loginToken)); } else { // might be undefined, which will be turned into true by Segment const value = iterator.next().value; From e2d2291d8d3ac7c093ae110b270ee8217463d417 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 23:01:44 +0530 Subject: [PATCH 016/108] Add test Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index a3b587df..868eec0e 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -232,6 +232,12 @@ export function tests() { const urlPath = stringifyPath(path); assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details"); }, + "Parse loginToken query parameter into SSO segment": assert => { + const segments = parseUrlPath("/?loginToken=a1232aSD123"); + assert.equal(segments.length, 1); + assert.equal(segments[0].type, "sso"); + assert.equal(segments[0].value, "a1232aSD123"); + }, "parse grid url path with focused empty tile": assert => { const segments = parseUrlPath("/session/1/rooms/a,b,c/3"); assert.equal(segments.length, 3); From 3efadcb72cf6920a940ef97e743e7148408d1a76 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 23:02:53 +0530 Subject: [PATCH 017/108] Add method that returns callback url Signed-off-by: RMidhunSuresh --- src/domain/navigation/URLRouter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.js index 28488129..b1b9950a 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.js @@ -120,4 +120,8 @@ export class URLRouter { const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`; return this._history.pathAsUrl(urlPath); } + + createSSOCallbackURL() { + return window.location.origin; + } } From 987a83b4cf4d28276e2a803a6df6d82f57848a45 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 30 Jul 2021 23:03:18 +0530 Subject: [PATCH 018/108] Add method to redirect to a specific URL Signed-off-by: RMidhunSuresh --- src/platform/web/Platform.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index f1410106..7e28f36f 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -240,6 +240,10 @@ export class Platform { return promise; } + openUrl(url) { + location.href = url; + } + parseHTML(html) { return parseHTML(html); } From 74f5e3048716acf6ae55e878d84bc4b29cafaa90 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 31 Jul 2021 17:10:22 +0530 Subject: [PATCH 019/108] Ignore sso segment in url Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 868eec0e..8aa3cf97 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -185,7 +185,8 @@ export function stringifyPath(path) { } break; case "right-panel": - // Ignore right-panel in url + case "sso": + // Do not put these segments in URL continue; default: urlPath += `/${segment.type}`; From bed01851860e5ea0bce1253eff2fc732c91210b8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 1 Aug 2021 13:54:16 +0530 Subject: [PATCH 020/108] Support loginToken query in History Signed-off-by: RMidhunSuresh --- src/platform/web/dom/History.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/platform/web/dom/History.js b/src/platform/web/dom/History.js index 92927d3f..68e4ef78 100644 --- a/src/platform/web/dom/History.js +++ b/src/platform/web/dom/History.js @@ -25,6 +25,14 @@ export class History extends BaseObservableValue { } get() { + /* + All URLS in Hydrogen will use /#/segment/value/... + But for SSO, we need to handle /?loginToken= + Handle that as a special case for now. + */ + if (document.location.search.includes("loginToken")) { + return document.location.search; + } return document.location.hash; } From b2613740b8c4beb21eac618c7765d711662201d2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 12:32:29 +0530 Subject: [PATCH 021/108] Add functionality to remove loginToken from URL Signed-off-by: RMidhunSuresh --- src/domain/navigation/URLRouter.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.js index b1b9950a..586eec8a 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.js @@ -124,4 +124,10 @@ export class URLRouter { createSSOCallbackURL() { return window.location.origin; } + + normalizeUrl() { + // Remove any queryParameters from the URL + // Gets rid of the loginToken after SSO + this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`); + } } From 75d71717d8d2d3a76f2fc7345bc38da07f7223f9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 31 Jul 2021 15:24:50 +0530 Subject: [PATCH 022/108] Show link for SSO login Signed-off-by: RMidhunSuresh --- src/domain/LoginViewModel.js | 45 +++++++++++++++++++++++--- src/platform/web/ui/login/LoginView.js | 4 ++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 4e9993aa..0cd474a9 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -17,6 +17,14 @@ limitations under the License. import {ViewModel} from "./ViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; +function normalizeHomeserver(homeServer) { + try { + return new URL(homeServer).origin; + } catch (err) { + return new URL(`https://${homeServer}`).origin; + } +} + export class LoginViewModel extends ViewModel { constructor(options) { super(options); @@ -27,6 +35,8 @@ export class LoginViewModel extends ViewModel { this._sessionContainer = null; this._loadViewModel = null; this._loadViewModelSubscription = null; + this._supportsSSOLogin = false; + this.queryLogin(); } get defaultHomeServer() { return this._defaultHomeServer; } @@ -41,6 +51,31 @@ export class LoginViewModel extends ViewModel { } } + async queryLogin(homeServer = this.defaultHomeServer) { + // See if we support SSO, if so shows SSO link + /* For this, we'd need to poll queryLogin before we do login() + */ + if (!this._sessionContainer) { + this._sessionContainer = this._createSessionContainer(); + } + const normalizedHS = normalizeHomeserver(homeServer); + try { + this.loginOptions = await this._sessionContainer.queryLogin(normalizedHS); + this._supportsSSOLogin = !!this.loginOptions.sso; + } + catch (e) { + // Something went wrong, assume SSO is not supported + this._supportsSSOLogin = false; + console.error("Could not query login methods supported by the homeserver"); + } + this.emitChange("supportsSSOLogin"); + } + + queryLoginFromInput() { + const homeServer = document.querySelector("#homeserver").value; + this.queryLogin(homeServer); + } + async login(username, password, homeserver) { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); if (this._loadViewModel) { @@ -48,10 +83,8 @@ export class LoginViewModel extends ViewModel { } this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ createAndStartSessionContainer: async () => { - this._sessionContainer = this._createSessionContainer(); - const loginOptions = await this._sessionContainer.queryLogin(homeserver); - if (loginOptions.password) { - this._sessionContainer.startWithLogin(loginOptions.password(username, password)); + if (this.loginOptions.password) { + this._sessionContainer.startWithLogin(this.loginOptions.password(username, password)); } return this._sessionContainer; }, @@ -76,6 +109,10 @@ export class LoginViewModel extends ViewModel { return this.urlCreator.urlForSegment("session"); } + get supportsSSOLogin() { + return this._supportsSSOLogin; + } + dispose() { super.dispose(); if (this._sessionContainer) { diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 683bf42d..f8ddcf4c 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -37,7 +37,8 @@ export class LoginView extends TemplateView { id: "homeserver", type: "text", placeholder: vm.i18n`Your matrix homeserver`, - value: vm.defaultHomeServer, + value: vm.defaultHomeServer, + onChange: () => vm.queryLoginFromInput(), disabled }); @@ -67,6 +68,7 @@ export class LoginView extends TemplateView { }, vm.i18n`Log In`), ]), ]), + t.if(vm => vm.supportsSSOLogin, () => t.p(t.a({className:"SSO", href:"#"}, "Login with SSO"))), // use t.mapView rather than t.if to create a new view when the view model changes too t.p(hydrogenGithubLink(t)) ]) From 4b87887a4fa885f4f848807cc082191f718a978d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 31 Jul 2021 17:07:44 +0530 Subject: [PATCH 023/108] Show completion view on sso segment Signed-off-by: RMidhunSuresh --- src/domain/RootViewModel.js | 11 ++++++++++- src/platform/web/ui/RootView.js | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index fca8d779..15b9d19e 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -35,12 +35,16 @@ export class RootViewModel extends ViewModel { async load() { this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); + this.track(this.navigation.observe("sso").subscribe(() => { + this._applyNavigation(); + })); this._applyNavigation(true); } async _applyNavigation(shouldRestoreLastUrl) { const isLogin = this.navigation.observe("login").get(); const sessionId = this.navigation.observe("session").get(); + const SSOSegment = this.navigation.path.get("sso"); if (isLogin) { if (this.activeSection !== "login") { this._showLogin(); @@ -65,7 +69,10 @@ export class RootViewModel extends ViewModel { this._showSessionLoader(sessionId); } } - } else { + } else if (SSOSegment) { + this._setSection(() => this.showCompletionView = true); + } + else { try { if (!(shouldRestoreLastUrl && this.urlCreator.tryRestoreLastUrl())) { const sessionInfos = await this.platform.sessionInfoStorage.getAll(); @@ -147,6 +154,8 @@ export class RootViewModel extends ViewModel { return "picker"; } else if (this._sessionLoadViewModel) { return "loading"; + } else if (this.showCompletionView) { + return "sso"; } else { return "redirecting"; } diff --git a/src/platform/web/ui/RootView.js b/src/platform/web/ui/RootView.js index f60bb984..55cdac14 100644 --- a/src/platform/web/ui/RootView.js +++ b/src/platform/web/ui/RootView.js @@ -20,6 +20,7 @@ import {SessionLoadView} from "./login/SessionLoadView.js"; import {SessionPickerView} from "./login/SessionPickerView.js"; import {TemplateView} from "./general/TemplateView.js"; import {StaticView} from "./general/StaticView.js"; +import {CompleteSSOView} from "../../../domain/CompleteSSOView.js"; export class RootView extends TemplateView { render(t, vm) { @@ -42,6 +43,8 @@ export class RootView extends TemplateView { return new StaticView(t => t.p("Redirecting...")); case "loading": return new SessionLoadView(vm.sessionLoadViewModel); + case "sso": + return new CompleteSSOView(); default: throw new Error(`Unknown section: ${vm.activeSection}`); } From 2103adfc03d3405e4d37f2b64ff9214bd64cd1be Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 31 Jul 2021 17:08:41 +0530 Subject: [PATCH 024/108] Add view Signed-off-by: RMidhunSuresh --- src/domain/CompleteSSOView.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/domain/CompleteSSOView.js diff --git a/src/domain/CompleteSSOView.js b/src/domain/CompleteSSOView.js new file mode 100644 index 00000000..7a7f63f1 --- /dev/null +++ b/src/domain/CompleteSSOView.js @@ -0,0 +1,23 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../platform/web/ui/general/TemplateView.js"; + +export class CompleteSSOView extends TemplateView { + render(t) { + return t.div({ className: "CompleteSSOView" }, "Finishing up SSO Login ..."); + } +} From 2c953e361d08ccebb41abe777cca69082addbd61 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 1 Aug 2021 20:21:04 +0530 Subject: [PATCH 025/108] Remove queryLoginFromInput() Signed-off-by: RMidhunSuresh --- src/domain/LoginViewModel.js | 5 ----- src/platform/web/ui/login/LoginView.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 0cd474a9..d75a715e 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -71,11 +71,6 @@ export class LoginViewModel extends ViewModel { this.emitChange("supportsSSOLogin"); } - queryLoginFromInput() { - const homeServer = document.querySelector("#homeserver").value; - this.queryLogin(homeServer); - } - async login(username, password, homeserver) { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); if (this._loadViewModel) { diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index f8ddcf4c..56dfa96b 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -38,7 +38,7 @@ export class LoginView extends TemplateView { type: "text", placeholder: vm.i18n`Your matrix homeserver`, value: vm.defaultHomeServer, - onChange: () => vm.queryLoginFromInput(), + onChange: () => vm.queryLogin(homeserver.value), disabled }); From 0af27fc8dd963a343bb6925a4c77d08274bf4ba4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 1 Aug 2021 20:35:04 +0530 Subject: [PATCH 026/108] Move normalizeHomeserver to LoginViewModel Signed-off-by: RMidhunSuresh --- src/domain/LoginViewModel.js | 1 + src/matrix/SessionContainer.js | 14 ++------------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index d75a715e..5e0702da 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -72,6 +72,7 @@ export class LoginViewModel extends ViewModel { } async login(username, password, homeserver) { + homeserver = normalizeHomeserver(homeserver); this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); if (this._loadViewModel) { this._loadViewModel = this.disposeTracked(this._loadViewModel); diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 85aa979c..4511d982 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -43,14 +43,6 @@ export const LoginFailure = createEnum( "Unknown", ); -function normalizeHomeserver(homeServer) { - try { - return new URL(homeServer).origin; - } catch (err) { - return new URL(`https://${homeServer}`).origin; - } -} - export class SessionContainer { constructor({platform, olmPromise, workerPromise}) { this._platform = platform; @@ -113,7 +105,6 @@ export class SessionContainer { } async queryLogin(homeServer) { - homeServer = normalizeHomeserver(homeServer); const hsApi = new HomeServerApi({homeServer, request: this._platform.request}); const response = await hsApi.queryLogin().response(); return this.parseLoginOptions(response, homeServer); @@ -129,15 +120,14 @@ export class SessionContainer { let sessionInfo; try { const request = this._platform.request; - const homeServer = normalizeHomeserver(loginMethod.homeServer); - const hsApi = new HomeServerApi({homeServer, request}); + const hsApi = new HomeServerApi({homeServer: loginMethod.homeServer, request}); const loginData = await loginMethod.login(hsApi, "Hydrogen", log); const sessionId = this.createNewSessionId(); sessionInfo = { id: sessionId, deviceId: loginData.device_id, userId: loginData.user_id, - homeServer: homeServer, + homeServer: loginMethod.homeServer, accessToken: loginData.access_token, lastUsed: clock.now() }; From ce5fdd465c83ebf70eacdc2ad267e7e53b7dc481 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 1 Aug 2021 20:41:57 +0530 Subject: [PATCH 027/108] Remove unnecessary braces Signed-off-by: RMidhunSuresh --- src/domain/RootViewModel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 15b9d19e..643655dc 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -35,9 +35,7 @@ export class RootViewModel extends ViewModel { async load() { this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); - this.track(this.navigation.observe("sso").subscribe(() => { - this._applyNavigation(); - })); + this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation())); this._applyNavigation(true); } From c82af5a0a373c223235c42ae6cf3c10615ed62d5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 11 Aug 2021 15:30:36 +0530 Subject: [PATCH 028/108] Replace link with button Signed-off-by: RMidhunSuresh --- src/platform/web/ui/login/LoginView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 56dfa96b..6ba9a575 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -68,7 +68,7 @@ export class LoginView extends TemplateView { }, vm.i18n`Log In`), ]), ]), - t.if(vm => vm.supportsSSOLogin, () => t.p(t.a({className:"SSO", href:"#"}, "Login with SSO"))), + t.if(vm => vm.supportsSSOLogin, () => t.button({className: "SSO"}, "Login with SSO")), // use t.mapView rather than t.if to create a new view when the view model changes too t.p(hydrogenGithubLink(t)) ]) From cabffd5e3fde7de02a9a47ba4d04566ea4a7a9fd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 11 Aug 2021 17:28:07 +0530 Subject: [PATCH 029/108] Move view to correct directory Signed-off-by: RMidhunSuresh --- src/platform/web/ui/RootView.js | 2 +- src/{domain => platform/web/ui/login}/CompleteSSOView.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{domain => platform/web/ui/login}/CompleteSSOView.js (91%) diff --git a/src/platform/web/ui/RootView.js b/src/platform/web/ui/RootView.js index 55cdac14..02fd4d7d 100644 --- a/src/platform/web/ui/RootView.js +++ b/src/platform/web/ui/RootView.js @@ -20,7 +20,7 @@ import {SessionLoadView} from "./login/SessionLoadView.js"; import {SessionPickerView} from "./login/SessionPickerView.js"; import {TemplateView} from "./general/TemplateView.js"; import {StaticView} from "./general/StaticView.js"; -import {CompleteSSOView} from "../../../domain/CompleteSSOView.js"; +import {CompleteSSOView} from "./login/CompleteSSOView.js"; export class RootView extends TemplateView { render(t, vm) { diff --git a/src/domain/CompleteSSOView.js b/src/platform/web/ui/login/CompleteSSOView.js similarity index 91% rename from src/domain/CompleteSSOView.js rename to src/platform/web/ui/login/CompleteSSOView.js index 7a7f63f1..2b6e9d02 100644 --- a/src/domain/CompleteSSOView.js +++ b/src/platform/web/ui/login/CompleteSSOView.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "../platform/web/ui/general/TemplateView.js"; +import {TemplateView} from "../general/TemplateView.js"; export class CompleteSSOView extends TemplateView { render(t) { From b8f0361157a255b0525714b64f7d0dd3aedaddc2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 16:21:00 +0530 Subject: [PATCH 030/108] Split login view into password and sso components Signed-off-by: RMidhunSuresh --- src/domain/LoginViewModel.js | 120 ------------------ src/domain/RootViewModel.js | 17 ++- src/domain/login/LoginViewModel.js | 105 +++++++++++++++ src/domain/login/PasswordLoginViewModel.js | 77 +++++++++++ src/domain/login/SSOLoginViewModel.js | 46 +++++++ src/domain/login/common.js | 23 ++++ src/platform/web/ui/RootView.js | 3 - src/platform/web/ui/login/CompleteSSOView.js | 8 +- src/platform/web/ui/login/LoginView.js | 76 ++++------- .../web/ui/login/PasswordLoginView.js | 71 +++++++++++ 10 files changed, 361 insertions(+), 185 deletions(-) delete mode 100644 src/domain/LoginViewModel.js create mode 100644 src/domain/login/LoginViewModel.js create mode 100644 src/domain/login/PasswordLoginViewModel.js create mode 100644 src/domain/login/SSOLoginViewModel.js create mode 100644 src/domain/login/common.js create mode 100644 src/platform/web/ui/login/PasswordLoginView.js diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js deleted file mode 100644 index 5e0702da..00000000 --- a/src/domain/LoginViewModel.js +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {ViewModel} from "./ViewModel.js"; -import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; - -function normalizeHomeserver(homeServer) { - try { - return new URL(homeServer).origin; - } catch (err) { - return new URL(`https://${homeServer}`).origin; - } -} - -export class LoginViewModel extends ViewModel { - constructor(options) { - super(options); - const {ready, defaultHomeServer, createSessionContainer} = options; - this._createSessionContainer = createSessionContainer; - this._ready = ready; - this._defaultHomeServer = defaultHomeServer; - this._sessionContainer = null; - this._loadViewModel = null; - this._loadViewModelSubscription = null; - this._supportsSSOLogin = false; - this.queryLogin(); - } - - get defaultHomeServer() { return this._defaultHomeServer; } - - get loadViewModel() {return this._loadViewModel; } - - get isBusy() { - if (!this._loadViewModel) { - return false; - } else { - return this._loadViewModel.loading; - } - } - - async queryLogin(homeServer = this.defaultHomeServer) { - // See if we support SSO, if so shows SSO link - /* For this, we'd need to poll queryLogin before we do login() - */ - if (!this._sessionContainer) { - this._sessionContainer = this._createSessionContainer(); - } - const normalizedHS = normalizeHomeserver(homeServer); - try { - this.loginOptions = await this._sessionContainer.queryLogin(normalizedHS); - this._supportsSSOLogin = !!this.loginOptions.sso; - } - catch (e) { - // Something went wrong, assume SSO is not supported - this._supportsSSOLogin = false; - console.error("Could not query login methods supported by the homeserver"); - } - this.emitChange("supportsSSOLogin"); - } - - async login(username, password, homeserver) { - homeserver = normalizeHomeserver(homeserver); - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - if (this._loadViewModel) { - this._loadViewModel = this.disposeTracked(this._loadViewModel); - } - this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ - createAndStartSessionContainer: async () => { - if (this.loginOptions.password) { - this._sessionContainer.startWithLogin(this.loginOptions.password(username, password)); - } - return this._sessionContainer; - }, - ready: sessionContainer => { - // make sure we don't delete the session in dispose when navigating away - this._sessionContainer = null; - this._ready(sessionContainer); - }, - homeserver, - }))); - this._loadViewModel.start(); - this.emitChange("loadViewModel"); - this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => { - if (!this._loadViewModel.loading) { - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - } - this.emitChange("isBusy"); - })); - } - - get cancelUrl() { - return this.urlCreator.urlForSegment("session"); - } - - get supportsSSOLogin() { - return this._supportsSSOLogin; - } - - dispose() { - super.dispose(); - if (this._sessionContainer) { - // if we move away before we're done with initial sync - // delete the session - this._sessionContainer.deleteSession(); - } - } -} diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 643655dc..c47762c5 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; -import {LoginViewModel} from "./LoginViewModel.js"; +import {LoginViewModel} from "./login/LoginViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {ViewModel} from "./ViewModel.js"; @@ -42,7 +42,8 @@ export class RootViewModel extends ViewModel { async _applyNavigation(shouldRestoreLastUrl) { const isLogin = this.navigation.observe("login").get(); const sessionId = this.navigation.observe("session").get(); - const SSOSegment = this.navigation.path.get("sso"); + // TODO: why not observe? + const ssoSegment = this.navigation.path.get("sso"); if (isLogin) { if (this.activeSection !== "login") { this._showLogin(); @@ -67,8 +68,11 @@ export class RootViewModel extends ViewModel { this._showSessionLoader(sessionId); } } - } else if (SSOSegment) { - this._setSection(() => this.showCompletionView = true); + } else if (ssoSegment) { + this.urlCreator.normalizeUrl(); + if (this.activeSection !== "login") { + this._showLogin({loginToken: ssoSegment.value}); + } } else { try { @@ -99,7 +103,7 @@ export class RootViewModel extends ViewModel { } } - _showLogin() { + _showLogin(options) { this._setSection(() => { this._loginViewModel = new LoginViewModel(this.childOptions({ defaultHomeServer: this.platform.config["defaultHomeServer"], @@ -116,6 +120,7 @@ export class RootViewModel extends ViewModel { this._pendingSessionContainer = sessionContainer; this.navigation.push("session", sessionContainer.sessionId); }, + ...options })); }); } @@ -152,8 +157,6 @@ export class RootViewModel extends ViewModel { return "picker"; } else if (this._sessionLoadViewModel) { return "loading"; - } else if (this.showCompletionView) { - return "sso"; } else { return "redirecting"; } diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js new file mode 100644 index 00000000..3d747024 --- /dev/null +++ b/src/domain/login/LoginViewModel.js @@ -0,0 +1,105 @@ +/* +Copyright 2020 Bruno Windels + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../ViewModel.js"; +import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; +import {SSOLoginViewModel} from "./SSOLoginViewModel.js"; +import {normalizeHomeserver} from "./common.js"; + +export class LoginViewModel extends ViewModel { + constructor(options) { + super(options); + const {ready, defaultHomeServer, createSessionContainer, loginToken} = options; + this._createSessionContainer = createSessionContainer; + this._ready = ready; + this._defaultHomeServer = defaultHomeServer; + this._loginToken = loginToken; + this._sessionContainer = this._createSessionContainer(); + this._loginOptions = null; + this._start(); + } + + get passwordLoginViewModel() { return this._passwordLoginViewModel; } + get ssoLoginViewModel() { return this._ssoLoginViewModel; } + get loadViewModel() {return this._loadViewModel; } + + async _start() { + if (this._loginToken) { + this._ssoLoginViewModel = this.track(new SSOLoginViewModel(this.childOptions({loginToken: this._loginToken}))); + this.emitChange("ssoLoginViewModel"); + } + else { + const defaultHomeServer = normalizeHomeserver(this._defaultHomeServer); + await this.queryLogin(defaultHomeServer); + this._showPasswordLogin(); + this._showSSOLogin(defaultHomeServer); + } + } + + _showPasswordLogin() { + this._passwordLoginViewModel = new PasswordLoginViewModel(this.childOptions({defaultHomeServer: this._defaultHomeServer})); + const observable = this._passwordLoginViewModel.homeserverObservable; + this.track(observable.subscribe(newHomeServer => this._onHomeServerChange(newHomeServer))); + this.emitChange("passwordLoginViewModel"); + } + + _showSSOLogin(homeserver) { + this._ssoLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); + this.emitChange("ssoLoginViewModel"); + if (this._loginOptions?.sso && !this._loginToken) { + this._ssoLoginViewModel = this.track(new SSOLoginViewModel(this.childOptions({homeserver}))); + this.emitChange("ssoLoginViewModel"); + } + } + + async queryLogin(homeserver) { + try { + this._loginOptions = await this._sessionContainer.queryLogin(homeserver); + } + catch (e) { + this._loginOptions = null; + console.error("Could not query login methods supported by the homeserver"); + } + } + + async _onHomeServerChange(homeserver) { + const normalizedHS = normalizeHomeserver(homeserver); + await this.queryLogin(normalizedHS); + this._showSSOLogin(normalizedHS); + } + + childOptions(options) { + return { + ...super.childOptions(options), + ready: sessionContainer => { + // make sure we don't delete the session in dispose when navigating away + this._sessionContainer = null; + this._ready(sessionContainer); + }, + sessionContainer: this._sessionContainer, + loginOptions: this._loginOptions + } + } + + dispose() { + super.dispose(); + if (this._sessionContainer) { + // if we move away before we're done with initial sync + // delete the session + this._sessionContainer.deleteSession(); + } + } +} diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js new file mode 100644 index 00000000..2b4700d3 --- /dev/null +++ b/src/domain/login/PasswordLoginViewModel.js @@ -0,0 +1,77 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../ViewModel.js"; +import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; +import {ObservableValue} from "../../observable/ObservableValue.js"; +import {normalizeHomeserver} from "./common.js"; + +export class PasswordLoginViewModel extends ViewModel { + constructor(options) { + super(options); + const {ready, defaultHomeServer, loginOptions, sessionContainer} = options; + this._ready = ready; + this._defaultHomeServer = defaultHomeServer; + this._sessionContainer = sessionContainer; + this._loadViewModel = null; + this._loadViewModelSubscription = null; + this._loginOptions = loginOptions; + this._homeserverObservable = new ObservableValue(this._defaultHomeServer); + } + + get defaultHomeServer() { return this._defaultHomeServer; } + get loadViewModel() {return this._loadViewModel; } + get homeserverObservable() { return this._homeserverObservable; } + get cancelUrl() { return this.urlCreator.urlForSegment("session"); } + + updateHomeServer(homeserver) { + this._homeserverObservable.set(homeserver); + } + + get isBusy() { + if (!this._loadViewModel) { + return false; + } else { + return this._loadViewModel.loading; + } + } + + async login(username, password, homeserver) { + homeserver = normalizeHomeserver(homeserver); + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + if (this._loadViewModel) { + this._loadViewModel = this.disposeTracked(this._loadViewModel); + } + this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ + createAndStartSessionContainer: async () => { + if (this._loginOptions.password) { + this._sessionContainer.startWithLogin(this._loginOptions.password(username, password)); + } + return this._sessionContainer; + }, + ready: this._ready, + homeserver, + }))); + this._loadViewModel.start(); + this.emitChange("loadViewModel"); + this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => { + if (!this._loadViewModel.loading) { + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + } + this.emitChange("isBusy"); + })); + } +} diff --git a/src/domain/login/SSOLoginViewModel.js b/src/domain/login/SSOLoginViewModel.js new file mode 100644 index 00000000..10691a37 --- /dev/null +++ b/src/domain/login/SSOLoginViewModel.js @@ -0,0 +1,46 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../ViewModel.js"; + +export class SSOLoginViewModel extends ViewModel{ + constructor(options) { + super(options); + const { + loginToken, + sessionContainer, + loginOptions, + ready, + homeserver + } = options; + this._loginToken = loginToken; + this._ready = ready; + this._sessionContainer = sessionContainer; + this._homeserver = homeserver; + this._loadViewModelSubscription = null; + this._loadViewModel = null; + this._loginOptions = loginOptions; + } + + get loadViewModel() { return this._loadViewModel; } + get supportsSSOLogin() { return this._supportsSSOLogin; } + get isSSOCompletion() { return !!this._loginToken; } + + + async startSSOLogin() { + console.log("Next PR"); + } +} diff --git a/src/domain/login/common.js b/src/domain/login/common.js new file mode 100644 index 00000000..8bc162e9 --- /dev/null +++ b/src/domain/login/common.js @@ -0,0 +1,23 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export function normalizeHomeserver(homeServer) { + try { + return new URL(homeServer).origin; + } catch (err) { + return new URL(`https://${homeServer}`).origin; + } +} diff --git a/src/platform/web/ui/RootView.js b/src/platform/web/ui/RootView.js index 02fd4d7d..f60bb984 100644 --- a/src/platform/web/ui/RootView.js +++ b/src/platform/web/ui/RootView.js @@ -20,7 +20,6 @@ import {SessionLoadView} from "./login/SessionLoadView.js"; import {SessionPickerView} from "./login/SessionPickerView.js"; import {TemplateView} from "./general/TemplateView.js"; import {StaticView} from "./general/StaticView.js"; -import {CompleteSSOView} from "./login/CompleteSSOView.js"; export class RootView extends TemplateView { render(t, vm) { @@ -43,8 +42,6 @@ export class RootView extends TemplateView { return new StaticView(t => t.p("Redirecting...")); case "loading": return new SessionLoadView(vm.sessionLoadViewModel); - case "sso": - return new CompleteSSOView(); default: throw new Error(`Unknown section: ${vm.activeSection}`); } diff --git a/src/platform/web/ui/login/CompleteSSOView.js b/src/platform/web/ui/login/CompleteSSOView.js index 2b6e9d02..a31072bb 100644 --- a/src/platform/web/ui/login/CompleteSSOView.js +++ b/src/platform/web/ui/login/CompleteSSOView.js @@ -15,9 +15,15 @@ limitations under the License. */ import {TemplateView} from "../general/TemplateView.js"; +import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; export class CompleteSSOView extends TemplateView { render(t) { - return t.div({ className: "CompleteSSOView" }, "Finishing up SSO Login ..."); + return t.div({ className: "CompleteSSOView" }, + [ + "Finishing up SSO Login ...", + t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null) + ] + ); } } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 6ba9a575..3e7a4fa6 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -16,63 +16,31 @@ limitations under the License. import {TemplateView} from "../general/TemplateView.js"; import {hydrogenGithubLink} from "./common.js"; -import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; +import {PasswordLoginView} from "./PasswordLoginView.js"; +import {CompleteSSOView} from "./CompleteSSOView.js"; export class LoginView extends TemplateView { - render(t, vm) { - const disabled = vm => !!vm.isBusy; - const username = t.input({ - id: "username", - type: "text", - placeholder: vm.i18n`Username`, - disabled - }); - const password = t.input({ - id: "password", - type: "password", - placeholder: vm.i18n`Password`, - disabled - }); - const homeserver = t.input({ - id: "homeserver", - type: "text", - placeholder: vm.i18n`Your matrix homeserver`, - value: vm.defaultHomeServer, - onChange: () => vm.queryLogin(homeserver.value), - disabled - }); - - return t.div({className: "PreSessionScreen"}, [ - t.div({className: "logo"}), - t.div({className: "LoginView form"}, [ - t.h1([vm.i18n`Sign In`]), - t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)), - t.form({ - onSubmit: evnt => { - evnt.preventDefault(); - vm.login(username.value, password.value, homeserver.value); - } - }, [ - t.div({className: "form-row"}, [t.label({for: "username"}, vm.i18n`Username`), username]), - t.div({className: "form-row"}, [t.label({for: "password"}, vm.i18n`Password`), password]), - t.div({className: "form-row"}, [t.label({for: "homeserver"}, vm.i18n`Homeserver`), homeserver]), - t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), - t.div({className: "button-row"}, [ - t.a({ - className: "button-action secondary", - href: vm.cancelUrl - }, [vm.i18n`Go Back`]), - t.button({ - className: "button-action primary", - type: "submit" - }, vm.i18n`Log In`), - ]), - ]), - t.if(vm => vm.supportsSSOLogin, () => t.button({className: "SSO"}, "Login with SSO")), - // use t.mapView rather than t.if to create a new view when the view model changes too - t.p(hydrogenGithubLink(t)) - ]) + render(t) { + return t.div({ className: "PreSessionScreen" }, [ + t.div({ className: "logo" }), + t.mapView(vm => vm.passwordLoginViewModel, vm => vm? new PasswordLoginView(vm): null), + t.mapView(vm => vm.ssoLoginViewModel, vm => { + if (vm?.isSSOCompletion) { + return new CompleteSSOView(vm); + } + else if (vm) { + return new SSOLoginView(vm); + } + return null; + } ), + // use t.mapView rather than t.if to create a new view when the view model changes too + t.p(hydrogenGithubLink(t)) ]); } } +class SSOLoginView extends TemplateView { + render(t, vm) { + return t.button({className: "SSO", type: "button", onClick: () => vm.startSSOLogin()}, "Login with SSO"); + } +} diff --git a/src/platform/web/ui/login/PasswordLoginView.js b/src/platform/web/ui/login/PasswordLoginView.js new file mode 100644 index 00000000..7a41f2b5 --- /dev/null +++ b/src/platform/web/ui/login/PasswordLoginView.js @@ -0,0 +1,71 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../general/TemplateView.js"; +import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; + +export class PasswordLoginView extends TemplateView { + render(t, vm) { + const disabled = vm => !!vm.isBusy; + const username = t.input({ + id: "username", + type: "text", + placeholder: vm.i18n`Username`, + disabled + }); + const password = t.input({ + id: "password", + type: "password", + placeholder: vm.i18n`Password`, + disabled + }); + const homeserver = t.input({ + id: "homeserver", + type: "text", + placeholder: vm.i18n`Your matrix homeserver`, + value: vm.defaultHomeServer, + onChange: () => vm.updateHomeServer(homeserver.value), + disabled + }); + + return t.div({className: "LoginView form"}, [ + t.h1([vm.i18n`Sign In`]), + t.if(vm => vm.error, t => t.div({ className: "error" }, vm => vm.error)), + t.form({ + onSubmit: evnt => { + evnt.preventDefault(); + vm.login(username.value, password.value, homeserver.value); + } + }, [ + t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]), + t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), + t.div({ className: "form-row" }, [t.label({ for: "homeserver" }, vm.i18n`Homeserver`), homeserver]), + t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), + t.div({ className: "button-row" }, [ + t.a({ + className: "button-action secondary", + href: vm.cancelUrl + }, [vm.i18n`Go Back`]), + t.button({ + className: "button-action primary", + type: "submit" + }, vm.i18n`Log In`), + ]), + ]) + ]); + } +} + From 93720f602556c6792c2ddc302f3698cc618a7298 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 17:37:53 +0530 Subject: [PATCH 031/108] Style sso button Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/login.css | 16 ++++++++++++++++ src/platform/web/ui/css/themes/element/theme.css | 10 ++++++++++ src/platform/web/ui/login/LoginView.js | 7 ++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css index aefdac42..3ca08a63 100644 --- a/src/platform/web/ui/css/login.css +++ b/src/platform/web/ui/css/login.css @@ -70,3 +70,19 @@ limitations under the License. .SessionLoadStatusView .spinner { --size: 20px; } + +.SSOLoginView { + display: flex; + flex-direction: column; +} + +.SSOLoginView_button { + flex: 1; + margin-top: 10px; +} + +.SSOLoginView_separator { + justify-content: center; + display: flex; + margin: 8px; +} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 08a872b8..5c182d09 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -223,6 +223,16 @@ a.button-action { padding-top: 16px; } +.SSOLoginView_button { + border: 1px solid #03B381; + border-radius: 8px; +} + +.SSOLoginView_separator { + font-weight: 500; + font-size: 1.5rem; +} + @media screen and (min-width: 600px) { .PreSessionScreen { box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1); diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 3e7a4fa6..34187154 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -41,6 +41,11 @@ export class LoginView extends TemplateView { class SSOLoginView extends TemplateView { render(t, vm) { - return t.button({className: "SSO", type: "button", onClick: () => vm.startSSOLogin()}, "Login with SSO"); + return t.div({ className: "SSOLoginView" }, + [ + t.p({ className: "SSOLoginView_separator" }, "or"), + t.button({ className: "SSOLoginView_button button-action secondary", type: "button", onClick: () => vm.startSSOLogin() }, "Log in with SSO") + ] + ); } } From 6c6c4c7dfdce14c690e8cb9fe1118a9d3eebd952 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 18:08:01 +0530 Subject: [PATCH 032/108] Style CompleteSSOLoginView Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/login.css | 9 +++++++-- src/platform/web/ui/css/themes/element/theme.css | 4 ++++ src/platform/web/ui/login/CompleteSSOView.js | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css index 3ca08a63..9a979024 100644 --- a/src/platform/web/ui/css/login.css +++ b/src/platform/web/ui/css/login.css @@ -36,7 +36,7 @@ limitations under the License. align-items: center; } -.SessionPickerView .session-info > :not(:first-child) { +.SessionPickerView .session-info> :not(:first-child) { margin-left: 8px; } @@ -58,7 +58,7 @@ limitations under the License. display: flex; } -.SessionLoadStatusView > :not(:first-child) { +.SessionLoadStatusView> :not(:first-child) { margin-left: 12px; } @@ -86,3 +86,8 @@ limitations under the License. display: flex; margin: 8px; } + +.CompleteSSOView_title { + display: flex; + justify-content: center; +} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 5c182d09..121f5e51 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -233,6 +233,10 @@ a.button-action { font-size: 1.5rem; } +.CompleteSSOView_title { + font-weight: 500; +} + @media screen and (min-width: 600px) { .PreSessionScreen { box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1); diff --git a/src/platform/web/ui/login/CompleteSSOView.js b/src/platform/web/ui/login/CompleteSSOView.js index a31072bb..cece7184 100644 --- a/src/platform/web/ui/login/CompleteSSOView.js +++ b/src/platform/web/ui/login/CompleteSSOView.js @@ -21,7 +21,7 @@ export class CompleteSSOView extends TemplateView { render(t) { return t.div({ className: "CompleteSSOView" }, [ - "Finishing up SSO Login ...", + t.p({ className: "CompleteSSOView_title" }, "Finishing up your SSO Login"), t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null) ] ); From e424293293692e781ec724b211dcb9ec7dd0cc64 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 11 Aug 2021 17:13:49 +0530 Subject: [PATCH 033/108] Save homeserver before redirecting Signed-off-by: RMidhunSuresh --- src/domain/LoginViewModel.js | 132 +++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/domain/LoginViewModel.js diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js new file mode 100644 index 00000000..a33b4c46 --- /dev/null +++ b/src/domain/LoginViewModel.js @@ -0,0 +1,132 @@ +/* +Copyright 2020 Bruno Windels + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "./ViewModel.js"; +import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; + +function normalizeHomeserver(homeServer) { + try { + return new URL(homeServer).origin; + } catch (err) { + return new URL(`https://${homeServer}`).origin; + } +} + +export class LoginViewModel extends ViewModel { + constructor(options) { + super(options); + const {ready, defaultHomeServer, createSessionContainer} = options; + this._createSessionContainer = createSessionContainer; + this._ready = ready; + this._defaultHomeServer = defaultHomeServer; + this._sessionContainer = null; + this._loadViewModel = null; + this._loadViewModelSubscription = null; + this._supportsSSOLogin = false; + this.queryLogin(); + } + + get defaultHomeServer() { return this._defaultHomeServer; } + + get loadViewModel() {return this._loadViewModel; } + + get isBusy() { + if (!this._loadViewModel) { + return false; + } else { + return this._loadViewModel.loading; + } + } + + async queryLogin(homeServer = this.defaultHomeServer) { + // See if we support SSO, if so shows SSO link + /* For this, we'd need to poll queryLogin before we do login() + */ + this._supportsSSOLogin = false; + this.emitChange("supportsSSOLogin"); + if (!this._sessionContainer) { + this._sessionContainer = this._createSessionContainer(); + } + const normalizedHS = normalizeHomeserver(homeServer); + try { + this.loginOptions = await this._sessionContainer.queryLogin(normalizedHS); + this._supportsSSOLogin = !!this.loginOptions.sso; + } + catch (e) { + // Something went wrong, assume SSO is not supported + console.error("Could not query login methods supported by the homeserver"); + } + this.emitChange("supportsSSOLogin"); + } + + async login(username, password, homeserver) { + homeserver = normalizeHomeserver(homeserver); + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + if (this._loadViewModel) { + this._loadViewModel = this.disposeTracked(this._loadViewModel); + } + this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ + createAndStartSessionContainer: async () => { + if (this.loginOptions.password) { + this._sessionContainer.startWithLogin(this.loginOptions.password(username, password)); + } + return this._sessionContainer; + }, + ready: sessionContainer => { + // make sure we don't delete the session in dispose when navigating away + this._sessionContainer = null; + this._ready(sessionContainer); + }, + homeserver, + }))); + this._loadViewModel.start(); + this.emitChange("loadViewModel"); + this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => { + if (!this._loadViewModel.loading) { + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + } + this.emitChange("isBusy"); + })); + } + + getSSOLink(homeserver) { + const link = `${homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${this.urlCreator.createSSOCallbackURL()}`; + return link; + } + + async startSSOLogin(homeserver) { + const hs = normalizeHomeserver(homeserver); + await this.platform.settingsStorage.setString("homeserver", hs); + this.platform.openUrl(this.getSSOLink(hs)); + } + + get cancelUrl() { + return this.urlCreator.urlForSegment("session"); + } + + get supportsSSOLogin() { + return this._supportsSSOLogin; + } + + dispose() { + super.dispose(); + if (this._sessionContainer) { + // if we move away before we're done with initial sync + // delete the session + this._sessionContainer.deleteSession(); + } + } +} From daeeaa2869bbfc3b3c084c611fb58fd61746db79 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 11 Aug 2021 17:14:17 +0530 Subject: [PATCH 034/108] Set and get strings Signed-off-by: RMidhunSuresh --- src/platform/web/dom/SettingsStorage.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/platform/web/dom/SettingsStorage.js b/src/platform/web/dom/SettingsStorage.js index 4e4c18c7..1590cec5 100644 --- a/src/platform/web/dom/SettingsStorage.js +++ b/src/platform/web/dom/SettingsStorage.js @@ -43,6 +43,14 @@ export class SettingsStorage { return defaultValue; } + async setString(key, value) { + this._set(key, value); + } + + async getString(key) { + return window.localStorage.getItem(`${this._prefix}${key}`); + } + async remove(key) { window.localStorage.removeItem(`${this._prefix}${key}`); } From 66f28b90fce04804cb57769be576b44c43844056 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 12:23:32 +0530 Subject: [PATCH 035/108] Implement token login Signed-off-by: RMidhunSuresh --- src/matrix/login/TokenLoginMethod.js | 29 ++++++++++++++++++++++++++++ src/matrix/net/HomeServerApi.js | 12 ++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/matrix/login/TokenLoginMethod.js diff --git a/src/matrix/login/TokenLoginMethod.js b/src/matrix/login/TokenLoginMethod.js new file mode 100644 index 00000000..e55cedcf --- /dev/null +++ b/src/matrix/login/TokenLoginMethod.js @@ -0,0 +1,29 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {LoginMethod} from "./LoginMethod.js"; +import {makeTxnId} from "../common.js"; + +export class TokenLoginMethod extends LoginMethod { + constructor(options) { + super(options); + this._loginToken = options.loginToken; + } + + async login(hsApi, deviceName, log) { + return await hsApi.tokenLogin(this._loginToken, makeTxnId(), deviceName, {log}).response(); + } +} diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 6dec6bd6..13baf00c 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -150,6 +150,18 @@ export class HomeServerApi { }, options); } + tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) { + return this._unauthedRequest("POST", this._url("/login"), null, { + "type": "m.login.token", + "identifier": { + "type": "m.id.user", + }, + "token": loginToken, + "txn_id": txnId, + "initial_device_display_name": initialDeviceDisplayName + }, options); + } + createFilter(userId, filter, options = null) { return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options); } From 3fa955e594b2672f0eb45ad78a64572cd6d50ac4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 12:24:37 +0530 Subject: [PATCH 036/108] Parse token/sso login in loginOptions Signed-off-by: RMidhunSuresh --- src/matrix/SessionContainer.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 4511d982..83696009 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -24,6 +24,7 @@ import {RequestScheduler} from "./net/RequestScheduler.js"; import {Sync, SyncStatus} from "./Sync.js"; import {Session} from "./Session.js"; import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js"; +import {TokenLoginMethod} from "./login/TokenLoginMethod.js"; export const LoadStatus = createEnum( "NotLoading", @@ -91,7 +92,8 @@ export class SessionContainer { } parseLoginOptions(options, homeServer) { - /* Take server response and return new object which has two props password and sso which + /* + Take server response and return new object which has two props password and sso which implements LoginMethod */ const flows = options.flows; @@ -100,6 +102,12 @@ export class SessionContainer { if (flow.type === "m.login.password") { result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password}); } + else if (flow.type === "m.login.sso" && flows.find(flow => flow.type === "m.login.token")) { + result.sso = loginToken => new TokenLoginMethod({homeServer, loginToken}); + } + else if (flow.type === "m.login.token") { + result.token = loginToken => new TokenLoginMethod({homeServer, loginToken}); + } } return result; } From 683d2c21eb7d4971050dc18314cc3e43dd7f659b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 12:26:45 +0530 Subject: [PATCH 037/108] Use generic language in session load status Signed-off-by: RMidhunSuresh --- src/domain/SessionLoadViewModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 83bf5966..80837677 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -114,15 +114,15 @@ export class SessionLoadViewModel extends ViewModel { case LoadStatus.NotLoading: return `Preparing…`; case LoadStatus.Login: - return `Checking your login and password…`; + return `Checking your credentials…`; case LoadStatus.LoginFailed: switch (sc.loginFailure) { case LoginFailure.LoginFailure: - return `Your username and/or password don't seem to be correct.`; + return `Your credentials don't seem to be correct.`; case LoginFailure.Connection: return `Can't connect to ${this._homeserver}.`; case LoginFailure.Unknown: - return `Something went wrong while checking your login and password.`; + return `Something went wrong while checking your credentials.`; } break; case LoadStatus.SessionSetup: From 474a4bb19a04b1403fc05557c477d5093c587dc9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 16:26:52 +0530 Subject: [PATCH 038/108] Remove Login vm Signed-off-by: RMidhunSuresh --- src/domain/LoginViewModel.js | 132 ----------------------------------- 1 file changed, 132 deletions(-) delete mode 100644 src/domain/LoginViewModel.js diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js deleted file mode 100644 index a33b4c46..00000000 --- a/src/domain/LoginViewModel.js +++ /dev/null @@ -1,132 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {ViewModel} from "./ViewModel.js"; -import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; - -function normalizeHomeserver(homeServer) { - try { - return new URL(homeServer).origin; - } catch (err) { - return new URL(`https://${homeServer}`).origin; - } -} - -export class LoginViewModel extends ViewModel { - constructor(options) { - super(options); - const {ready, defaultHomeServer, createSessionContainer} = options; - this._createSessionContainer = createSessionContainer; - this._ready = ready; - this._defaultHomeServer = defaultHomeServer; - this._sessionContainer = null; - this._loadViewModel = null; - this._loadViewModelSubscription = null; - this._supportsSSOLogin = false; - this.queryLogin(); - } - - get defaultHomeServer() { return this._defaultHomeServer; } - - get loadViewModel() {return this._loadViewModel; } - - get isBusy() { - if (!this._loadViewModel) { - return false; - } else { - return this._loadViewModel.loading; - } - } - - async queryLogin(homeServer = this.defaultHomeServer) { - // See if we support SSO, if so shows SSO link - /* For this, we'd need to poll queryLogin before we do login() - */ - this._supportsSSOLogin = false; - this.emitChange("supportsSSOLogin"); - if (!this._sessionContainer) { - this._sessionContainer = this._createSessionContainer(); - } - const normalizedHS = normalizeHomeserver(homeServer); - try { - this.loginOptions = await this._sessionContainer.queryLogin(normalizedHS); - this._supportsSSOLogin = !!this.loginOptions.sso; - } - catch (e) { - // Something went wrong, assume SSO is not supported - console.error("Could not query login methods supported by the homeserver"); - } - this.emitChange("supportsSSOLogin"); - } - - async login(username, password, homeserver) { - homeserver = normalizeHomeserver(homeserver); - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - if (this._loadViewModel) { - this._loadViewModel = this.disposeTracked(this._loadViewModel); - } - this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ - createAndStartSessionContainer: async () => { - if (this.loginOptions.password) { - this._sessionContainer.startWithLogin(this.loginOptions.password(username, password)); - } - return this._sessionContainer; - }, - ready: sessionContainer => { - // make sure we don't delete the session in dispose when navigating away - this._sessionContainer = null; - this._ready(sessionContainer); - }, - homeserver, - }))); - this._loadViewModel.start(); - this.emitChange("loadViewModel"); - this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => { - if (!this._loadViewModel.loading) { - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - } - this.emitChange("isBusy"); - })); - } - - getSSOLink(homeserver) { - const link = `${homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${this.urlCreator.createSSOCallbackURL()}`; - return link; - } - - async startSSOLogin(homeserver) { - const hs = normalizeHomeserver(homeserver); - await this.platform.settingsStorage.setString("homeserver", hs); - this.platform.openUrl(this.getSSOLink(hs)); - } - - get cancelUrl() { - return this.urlCreator.urlForSegment("session"); - } - - get supportsSSOLogin() { - return this._supportsSSOLogin; - } - - dispose() { - super.dispose(); - if (this._sessionContainer) { - // if we move away before we're done with initial sync - // delete the session - this._sessionContainer.deleteSession(); - } - } -} From 98f8f04c74b2eb5b25a53bc70cec96fab1a67391 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 15 Aug 2021 16:36:03 +0530 Subject: [PATCH 039/108] Implement SSO Signed-off-by: RMidhunSuresh --- src/domain/login/SSOLoginViewModel.js | 35 ++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/domain/login/SSOLoginViewModel.js b/src/domain/login/SSOLoginViewModel.js index 10691a37..d084727c 100644 --- a/src/domain/login/SSOLoginViewModel.js +++ b/src/domain/login/SSOLoginViewModel.js @@ -15,6 +15,7 @@ limitations under the License. */ import {ViewModel} from "../ViewModel.js"; +import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; export class SSOLoginViewModel extends ViewModel{ constructor(options) { @@ -33,14 +34,46 @@ export class SSOLoginViewModel extends ViewModel{ this._loadViewModelSubscription = null; this._loadViewModel = null; this._loginOptions = loginOptions; + this.performSSOLoginCompletion(); } get loadViewModel() { return this._loadViewModel; } get supportsSSOLogin() { return this._supportsSSOLogin; } get isSSOCompletion() { return !!this._loginToken; } + async performSSOLoginCompletion() { + if (!this._loginToken) { + return; + } + const homeserver = await this.platform.settingsStorage.getString("homeserver"); + const loginOptions = await this._sessionContainer.queryLogin(homeserver); + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + if (this._loadViewModel) { + this._loadViewModel = this.disposeTracked(this._loadViewModel); + } + this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ + createAndStartSessionContainer: async () => { + if (loginOptions.sso) { + this._sessionContainer.startWithLogin(loginOptions.sso(this._loginToken)); + } + return this._sessionContainer; + }, + ready: this._ready, + homeserver, + }))); + this._loadViewModel.start(); + this.emitChange("loadViewModel"); + this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => { + if (!this._loadViewModel.loading) { + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + } + this.emitChange("isBusy"); + })); + } async startSSOLogin() { - console.log("Next PR"); + await this.platform.settingsStorage.setString("homeserver", this._homeserver); + const link = `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${this.urlCreator.createSSOCallbackURL()}`; + this.platform.openUrl(link); } } From f8b0ef052f0dcdcc84bcba470be60a15208a4822 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 17 Aug 2021 14:20:48 +0530 Subject: [PATCH 040/108] Give sso homeserver storage key a better name Signed-off-by: RMidhunSuresh --- src/domain/login/SSOLoginViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/login/SSOLoginViewModel.js b/src/domain/login/SSOLoginViewModel.js index d084727c..8bd9cd84 100644 --- a/src/domain/login/SSOLoginViewModel.js +++ b/src/domain/login/SSOLoginViewModel.js @@ -45,7 +45,7 @@ export class SSOLoginViewModel extends ViewModel{ if (!this._loginToken) { return; } - const homeserver = await this.platform.settingsStorage.getString("homeserver"); + const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver"); const loginOptions = await this._sessionContainer.queryLogin(homeserver); this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); if (this._loadViewModel) { @@ -72,7 +72,7 @@ export class SSOLoginViewModel extends ViewModel{ } async startSSOLogin() { - await this.platform.settingsStorage.setString("homeserver", this._homeserver); + await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._homeserver); const link = `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${this.urlCreator.createSSOCallbackURL()}`; this.platform.openUrl(link); } From 4b72b64a2ea0da38cb51c81ccac367184329fb6c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 17 Aug 2021 14:59:38 +0530 Subject: [PATCH 041/108] Implement SSOLoginHelper Signed-off-by: RMidhunSuresh --- src/domain/login/SSOLoginViewModel.js | 4 ++-- src/matrix/SessionContainer.js | 3 ++- src/matrix/login/SSOLoginHelper.js | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 src/matrix/login/SSOLoginHelper.js diff --git a/src/domain/login/SSOLoginViewModel.js b/src/domain/login/SSOLoginViewModel.js index 8bd9cd84..baa478f1 100644 --- a/src/domain/login/SSOLoginViewModel.js +++ b/src/domain/login/SSOLoginViewModel.js @@ -54,7 +54,7 @@ export class SSOLoginViewModel extends ViewModel{ this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ createAndStartSessionContainer: async () => { if (loginOptions.sso) { - this._sessionContainer.startWithLogin(loginOptions.sso(this._loginToken)); + this._sessionContainer.startWithLogin(loginOptions.token(this._loginToken)); } return this._sessionContainer; }, @@ -73,7 +73,7 @@ export class SSOLoginViewModel extends ViewModel{ async startSSOLogin() { await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._homeserver); - const link = `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${this.urlCreator.createSSOCallbackURL()}`; + const link = this._loginOptions.sso.ssoEndpointLink(this.urlCreator.createSSOCallbackURL()); this.platform.openUrl(link); } } diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 83696009..7492d0c3 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -25,6 +25,7 @@ import {Sync, SyncStatus} from "./Sync.js"; import {Session} from "./Session.js"; import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js"; import {TokenLoginMethod} from "./login/TokenLoginMethod.js"; +import {SSOLoginHelper} from "./login/SSOLoginHelper.js"; export const LoadStatus = createEnum( "NotLoading", @@ -103,7 +104,7 @@ export class SessionContainer { result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password}); } else if (flow.type === "m.login.sso" && flows.find(flow => flow.type === "m.login.token")) { - result.sso = loginToken => new TokenLoginMethod({homeServer, loginToken}); + result.sso = new SSOLoginHelper(homeServer); } else if (flow.type === "m.login.token") { result.token = loginToken => new TokenLoginMethod({homeServer, loginToken}); diff --git a/src/matrix/login/SSOLoginHelper.js b/src/matrix/login/SSOLoginHelper.js new file mode 100644 index 00000000..701d00b4 --- /dev/null +++ b/src/matrix/login/SSOLoginHelper.js @@ -0,0 +1,25 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export class SSOLoginHelper{ + constructor(homeserver) { + this._homeserver = homeserver; + } + + ssoEndpointLink(redirectURL) { + return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${redirectURL}`; + } +} From c4e7dc3b5aad7becaf013189f4e59fd47d2e2c8f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 17 Aug 2021 17:01:03 +0530 Subject: [PATCH 042/108] Split SSOLoginViewModel Signed-off-by: RMidhunSuresh --- ...wModel.js => CompleteSSOLoginViewModel.js} | 14 +------- src/domain/login/LoginViewModel.js | 22 +++++++------ src/domain/login/StartSSOLoginViewModel.js | 32 +++++++++++++++++++ src/platform/web/ui/css/login.css | 6 ++-- .../web/ui/css/themes/element/theme.css | 4 +-- src/platform/web/ui/login/LoginView.js | 19 ++++------- 6 files changed, 57 insertions(+), 40 deletions(-) rename src/domain/login/{SSOLoginViewModel.js => CompleteSSOLoginViewModel.js} (81%) create mode 100644 src/domain/login/StartSSOLoginViewModel.js diff --git a/src/domain/login/SSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js similarity index 81% rename from src/domain/login/SSOLoginViewModel.js rename to src/domain/login/CompleteSSOLoginViewModel.js index baa478f1..f64c5b94 100644 --- a/src/domain/login/SSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -17,29 +17,23 @@ limitations under the License. import {ViewModel} from "../ViewModel.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; -export class SSOLoginViewModel extends ViewModel{ +export class CompleteSSOLoginViewModel extends ViewModel { constructor(options) { super(options); const { loginToken, sessionContainer, - loginOptions, ready, - homeserver } = options; this._loginToken = loginToken; this._ready = ready; this._sessionContainer = sessionContainer; - this._homeserver = homeserver; this._loadViewModelSubscription = null; this._loadViewModel = null; - this._loginOptions = loginOptions; this.performSSOLoginCompletion(); } get loadViewModel() { return this._loadViewModel; } - get supportsSSOLogin() { return this._supportsSSOLogin; } - get isSSOCompletion() { return !!this._loginToken; } async performSSOLoginCompletion() { if (!this._loginToken) { @@ -70,10 +64,4 @@ export class SSOLoginViewModel extends ViewModel{ this.emitChange("isBusy"); })); } - - async startSSOLogin() { - await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._homeserver); - const link = this._loginOptions.sso.ssoEndpointLink(this.urlCreator.createSSOCallbackURL()); - this.platform.openUrl(link); - } } diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 3d747024..ba86baf5 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -16,8 +16,9 @@ limitations under the License. import {ViewModel} from "../ViewModel.js"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; -import {SSOLoginViewModel} from "./SSOLoginViewModel.js"; +import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {normalizeHomeserver} from "./common.js"; +import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; export class LoginViewModel extends ViewModel { constructor(options) { @@ -29,17 +30,20 @@ export class LoginViewModel extends ViewModel { this._loginToken = loginToken; this._sessionContainer = this._createSessionContainer(); this._loginOptions = null; + this._passwordLoginViewModel = null; + this._startSSOLoginViewModel = null; + this._completeSSOLoginViewModel = null; this._start(); } get passwordLoginViewModel() { return this._passwordLoginViewModel; } - get ssoLoginViewModel() { return this._ssoLoginViewModel; } - get loadViewModel() {return this._loadViewModel; } + get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } + get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } async _start() { if (this._loginToken) { - this._ssoLoginViewModel = this.track(new SSOLoginViewModel(this.childOptions({loginToken: this._loginToken}))); - this.emitChange("ssoLoginViewModel"); + this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel(this.childOptions({loginToken: this._loginToken}))); + this.emitChange("completeSSOLoginViewModel"); } else { const defaultHomeServer = normalizeHomeserver(this._defaultHomeServer); @@ -57,11 +61,11 @@ export class LoginViewModel extends ViewModel { } _showSSOLogin(homeserver) { - this._ssoLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); - this.emitChange("ssoLoginViewModel"); + this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); + this.emitChange("startSSOLoginViewModel"); if (this._loginOptions?.sso && !this._loginToken) { - this._ssoLoginViewModel = this.track(new SSOLoginViewModel(this.childOptions({homeserver}))); - this.emitChange("ssoLoginViewModel"); + this._startSSOLoginViewModel = this.track(new StartSSOLoginViewModel(this.childOptions({homeserver}))); + this.emitChange("startSSOLoginViewModel"); } } diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js new file mode 100644 index 00000000..315106ce --- /dev/null +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -0,0 +1,32 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../ViewModel.js"; + +export class StartSSOLoginViewModel extends ViewModel{ + constructor(options) { + super(options); + const {loginOptions, homeserver} = options; + this._sso = loginOptions.sso; + this._homeserver = homeserver; + } + + async startSSOLogin() { + await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._homeserver); + const link = this._sso.ssoEndpointLink(this.urlCreator.createSSOCallbackURL()); + this.platform.openUrl(link); + } +} diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css index 9a979024..b6f474dd 100644 --- a/src/platform/web/ui/css/login.css +++ b/src/platform/web/ui/css/login.css @@ -71,17 +71,17 @@ limitations under the License. --size: 20px; } -.SSOLoginView { +.StartSSOLoginView { display: flex; flex-direction: column; } -.SSOLoginView_button { +.StartSSOLoginView_button { flex: 1; margin-top: 10px; } -.SSOLoginView_separator { +.StartSSOLoginView_separator { justify-content: center; display: flex; margin: 8px; diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 121f5e51..74a1f5f8 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -223,12 +223,12 @@ a.button-action { padding-top: 16px; } -.SSOLoginView_button { +.StartSSOLoginView_button { border: 1px solid #03B381; border-radius: 8px; } -.SSOLoginView_separator { +.StartSSOLoginView_separator { font-weight: 500; font-size: 1.5rem; } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 34187154..45c773bd 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -23,28 +23,21 @@ export class LoginView extends TemplateView { render(t) { return t.div({ className: "PreSessionScreen" }, [ t.div({ className: "logo" }), + t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm? new CompleteSSOView(vm): null), t.mapView(vm => vm.passwordLoginViewModel, vm => vm? new PasswordLoginView(vm): null), - t.mapView(vm => vm.ssoLoginViewModel, vm => { - if (vm?.isSSOCompletion) { - return new CompleteSSOView(vm); - } - else if (vm) { - return new SSOLoginView(vm); - } - return null; - } ), + t.mapView(vm => vm.startSSOLoginViewModel, vm => vm? new StartSSOLoginView(vm): null), // use t.mapView rather than t.if to create a new view when the view model changes too t.p(hydrogenGithubLink(t)) ]); } } -class SSOLoginView extends TemplateView { +class StartSSOLoginView extends TemplateView { render(t, vm) { - return t.div({ className: "SSOLoginView" }, + return t.div({ className: "StartSSOLoginView" }, [ - t.p({ className: "SSOLoginView_separator" }, "or"), - t.button({ className: "SSOLoginView_button button-action secondary", type: "button", onClick: () => vm.startSSOLogin() }, "Log in with SSO") + t.p({ className: "StartSSOLoginView_separator" }, "or"), + t.button({ className: "StartSSOLoginView_button button-action secondary", type: "button", onClick: () => vm.startSSOLogin() }, "Log in with SSO") ] ); } From 7b9ec5516a004070b5f23dfb0be715cb030f8128 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 17 Aug 2021 17:31:14 +0530 Subject: [PATCH 043/108] Move normalizeHomeserver into session container Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 11 ++++------- src/domain/login/PasswordLoginViewModel.js | 2 -- src/domain/login/common.js | 23 ---------------------- src/matrix/SessionContainer.js | 13 ++++++++++-- 4 files changed, 15 insertions(+), 34 deletions(-) delete mode 100644 src/domain/login/common.js diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index ba86baf5..4f3b3cbd 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -17,7 +17,6 @@ limitations under the License. import {ViewModel} from "../ViewModel.js"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; -import {normalizeHomeserver} from "./common.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; export class LoginViewModel extends ViewModel { @@ -46,10 +45,9 @@ export class LoginViewModel extends ViewModel { this.emitChange("completeSSOLoginViewModel"); } else { - const defaultHomeServer = normalizeHomeserver(this._defaultHomeServer); - await this.queryLogin(defaultHomeServer); + await this.queryLogin(this._defaultHomeServer); this._showPasswordLogin(); - this._showSSOLogin(defaultHomeServer); + this._showSSOLogin(this._defaultHomeServer); } } @@ -80,9 +78,8 @@ export class LoginViewModel extends ViewModel { } async _onHomeServerChange(homeserver) { - const normalizedHS = normalizeHomeserver(homeserver); - await this.queryLogin(normalizedHS); - this._showSSOLogin(normalizedHS); + await this.queryLogin(homeserver); + this._showSSOLogin(homeserver); } childOptions(options) { diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 2b4700d3..496d431d 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -17,7 +17,6 @@ limitations under the License. import {ViewModel} from "../ViewModel.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; import {ObservableValue} from "../../observable/ObservableValue.js"; -import {normalizeHomeserver} from "./common.js"; export class PasswordLoginViewModel extends ViewModel { constructor(options) { @@ -50,7 +49,6 @@ export class PasswordLoginViewModel extends ViewModel { } async login(username, password, homeserver) { - homeserver = normalizeHomeserver(homeserver); this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); if (this._loadViewModel) { this._loadViewModel = this.disposeTracked(this._loadViewModel); diff --git a/src/domain/login/common.js b/src/domain/login/common.js deleted file mode 100644 index 8bc162e9..00000000 --- a/src/domain/login/common.js +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -export function normalizeHomeserver(homeServer) { - try { - return new URL(homeServer).origin; - } catch (err) { - return new URL(`https://${homeServer}`).origin; - } -} diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 7492d0c3..3aaa782b 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -27,6 +27,14 @@ import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js"; import {TokenLoginMethod} from "./login/TokenLoginMethod.js"; import {SSOLoginHelper} from "./login/SSOLoginHelper.js"; +function normalizeHomeserver(homeServer) { + try { + return new URL(homeServer).origin; + } catch (err) { + return new URL(`https://${homeServer}`).origin; + } +} + export const LoadStatus = createEnum( "NotLoading", "Login", @@ -114,9 +122,10 @@ export class SessionContainer { } async queryLogin(homeServer) { - const hsApi = new HomeServerApi({homeServer, request: this._platform.request}); + const normalizedHS = normalizeHomeserver(homeServer); + const hsApi = new HomeServerApi({homeServer: normalizedHS, request: this._platform.request}); const response = await hsApi.queryLogin().response(); - return this.parseLoginOptions(response, homeServer); + return this.parseLoginOptions(response, normalizedHS); } async startWithLogin(loginMethod) { From db3fd3d1aeccf273eb963b013c45aaa5e73a78cc Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 17 Aug 2021 17:48:06 +0530 Subject: [PATCH 044/108] Fix test Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 8aa3cf97..d21bcad4 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -234,7 +234,7 @@ export function tests() { assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details"); }, "Parse loginToken query parameter into SSO segment": assert => { - const segments = parseUrlPath("/?loginToken=a1232aSD123"); + const segments = parseUrlPath("?loginToken=a1232aSD123"); assert.equal(segments.length, 1); assert.equal(segments[0].type, "sso"); assert.equal(segments[0].value, "a1232aSD123"); From d2c94b0d3eb68a36db7210bab3fd32e77ca9f4ba Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 17 Aug 2021 17:53:59 +0530 Subject: [PATCH 045/108] Give argument better name Signed-off-by: RMidhunSuresh --- src/domain/RootViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index c47762c5..24982a71 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -103,7 +103,7 @@ export class RootViewModel extends ViewModel { } } - _showLogin(options) { + _showLogin(loginToken) { this._setSection(() => { this._loginViewModel = new LoginViewModel(this.childOptions({ defaultHomeServer: this.platform.config["defaultHomeServer"], @@ -120,7 +120,7 @@ export class RootViewModel extends ViewModel { this._pendingSessionContainer = sessionContainer; this.navigation.push("session", sessionContainer.sessionId); }, - ...options + ...loginToken })); }); } From 83f4095d880ffd64cda24ecc07793c2a06fbef13 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 17 Aug 2021 17:59:35 +0530 Subject: [PATCH 046/108] rename queryLogin to getLoginFlows Signed-off-by: RMidhunSuresh --- src/matrix/SessionContainer.js | 2 +- src/matrix/net/HomeServerApi.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 3aaa782b..edbe0f28 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -124,7 +124,7 @@ export class SessionContainer { async queryLogin(homeServer) { const normalizedHS = normalizeHomeserver(homeServer); const hsApi = new HomeServerApi({homeServer: normalizedHS, request: this._platform.request}); - const response = await hsApi.queryLogin().response(); + const response = await hsApi.getLoginFlows().response(); return this.parseLoginOptions(response, normalizedHS); } diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 13baf00c..8641e374 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -134,7 +134,7 @@ export class HomeServerApi { return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); } - queryLogin() { + getLoginFlows() { return this._unauthedRequest("GET", this._url("/login"), null, null, null); } From 13cb8979ace2637d7bf44fc13f21d4181c9de104 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 17 Aug 2021 20:54:24 +0530 Subject: [PATCH 047/108] Check correct login method early Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 9 ++++++--- src/domain/login/PasswordLoginViewModel.js | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index f64c5b94..bf98cefc 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -41,15 +41,18 @@ export class CompleteSSOLoginViewModel extends ViewModel { } const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver"); const loginOptions = await this._sessionContainer.queryLogin(homeserver); + if (!loginOptions.token) { + const path = this.navigation.pathFrom([this.navigation.segment("session")]); + this.navigation.applyPath(path); + return; + } this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); if (this._loadViewModel) { this._loadViewModel = this.disposeTracked(this._loadViewModel); } this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ createAndStartSessionContainer: async () => { - if (loginOptions.sso) { - this._sessionContainer.startWithLogin(loginOptions.token(this._loginToken)); - } + this._sessionContainer.startWithLogin(loginOptions.token(this._loginToken)); return this._sessionContainer; }, ready: this._ready, diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 496d431d..5feee9b5 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -49,15 +49,18 @@ export class PasswordLoginViewModel extends ViewModel { } async login(username, password, homeserver) { + if (!this._loginOptions.password) { + const path = this.navigation.pathFrom([this.navigation.segment("session")]); + this.navigation.applyPath(path); + return; + } this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); if (this._loadViewModel) { this._loadViewModel = this.disposeTracked(this._loadViewModel); } this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ createAndStartSessionContainer: async () => { - if (this._loginOptions.password) { - this._sessionContainer.startWithLogin(this._loginOptions.password(username, password)); - } + this._sessionContainer.startWithLogin(this._loginOptions.password(username, password)); return this._sessionContainer; }, ready: this._ready, From 10a6aca477505bf871737d7e13f64f394415b376 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 18 Aug 2021 13:05:05 +0530 Subject: [PATCH 048/108] Move homeserver input into LoginView Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 59 ++++++++++++------- src/domain/login/PasswordLoginViewModel.js | 15 ++--- src/platform/web/ui/css/login.css | 14 ++++- .../web/ui/css/themes/element/theme.css | 2 +- src/platform/web/ui/login/LoginView.js | 34 +++++++---- .../web/ui/login/PasswordLoginView.js | 14 +---- 6 files changed, 81 insertions(+), 57 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 4f3b3cbd..42be2ae1 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -32,39 +32,51 @@ export class LoginViewModel extends ViewModel { this._passwordLoginViewModel = null; this._startSSOLoginViewModel = null; this._completeSSOLoginViewModel = null; - this._start(); + this._homeserver = null; + this._errorMessage = ""; + this._start(this._defaultHomeServer); } get passwordLoginViewModel() { return this._passwordLoginViewModel; } get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } + get defaultHomeServer() { return this._defaultHomeServer; } + get errorMessage() { return this._errorMessage; } - async _start() { + async _start(homeserver) { if (this._loginToken) { this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel(this.childOptions({loginToken: this._loginToken}))); this.emitChange("completeSSOLoginViewModel"); } else { - await this.queryLogin(this._defaultHomeServer); - this._showPasswordLogin(); - this._showSSOLogin(this._defaultHomeServer); + this._errorMessage = ""; + await this.queryLogin(homeserver); + if (this._loginOptions) { + if (this._loginOptions.sso) { this._showSSOLogin(); } + if (this._loginOptions.password) { this._showPasswordLogin(); } + if (!this._loginOptions.sso && !this._loginOptions.password) { + this._showError("This homeserver neither supports SSO nor Password based login flows"); + } + } + else { + this._showError("Could not query login methods supported by the homeserver"); + } } } _showPasswordLogin() { - this._passwordLoginViewModel = new PasswordLoginViewModel(this.childOptions({defaultHomeServer: this._defaultHomeServer})); - const observable = this._passwordLoginViewModel.homeserverObservable; - this.track(observable.subscribe(newHomeServer => this._onHomeServerChange(newHomeServer))); + this._passwordLoginViewModel = this.track(new PasswordLoginViewModel(this.childOptions())); this.emitChange("passwordLoginViewModel"); } - _showSSOLogin(homeserver) { - this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); + _showSSOLogin() { + this._startSSOLoginViewModel = this.track(new StartSSOLoginViewModel(this.childOptions())); this.emitChange("startSSOLoginViewModel"); - if (this._loginOptions?.sso && !this._loginToken) { - this._startSSOLoginViewModel = this.track(new StartSSOLoginViewModel(this.childOptions({homeserver}))); - this.emitChange("startSSOLoginViewModel"); - } + } + + _showError(message) { + this._errorMessage = message; + this.emitChange("errorMessage"); } async queryLogin(homeserver) { @@ -73,16 +85,22 @@ export class LoginViewModel extends ViewModel { } catch (e) { this._loginOptions = null; - console.error("Could not query login methods supported by the homeserver"); } } - async _onHomeServerChange(homeserver) { - await this.queryLogin(homeserver); - this._showSSOLogin(homeserver); + _disposeViewModels() { + this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); + this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); + this.emitChange("disposeViewModels"); } - childOptions(options) { + updateHomeServer(newHomeserver) { + this._homeserver = newHomeserver; + this._disposeViewModels(); + this._start(newHomeserver); + } + + childOptions(options = {}) { return { ...super.childOptions(options), ready: sessionContainer => { @@ -91,7 +109,8 @@ export class LoginViewModel extends ViewModel { this._ready(sessionContainer); }, sessionContainer: this._sessionContainer, - loginOptions: this._loginOptions + loginOptions: this._loginOptions, + homeserver: this._homeserver ?? this._defaultHomeServer } } diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 5feee9b5..32aef2dd 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -16,30 +16,22 @@ limitations under the License. import {ViewModel} from "../ViewModel.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; -import {ObservableValue} from "../../observable/ObservableValue.js"; export class PasswordLoginViewModel extends ViewModel { constructor(options) { super(options); - const {ready, defaultHomeServer, loginOptions, sessionContainer} = options; + const {ready, loginOptions, sessionContainer, homeserver} = options; this._ready = ready; - this._defaultHomeServer = defaultHomeServer; this._sessionContainer = sessionContainer; this._loadViewModel = null; this._loadViewModelSubscription = null; this._loginOptions = loginOptions; - this._homeserverObservable = new ObservableValue(this._defaultHomeServer); + this._homeserver = homeserver; } - get defaultHomeServer() { return this._defaultHomeServer; } get loadViewModel() {return this._loadViewModel; } - get homeserverObservable() { return this._homeserverObservable; } get cancelUrl() { return this.urlCreator.urlForSegment("session"); } - updateHomeServer(homeserver) { - this._homeserverObservable.set(homeserver); - } - get isBusy() { if (!this._loadViewModel) { return false; @@ -48,7 +40,8 @@ export class PasswordLoginViewModel extends ViewModel { } } - async login(username, password, homeserver) { + async login(username, password) { + const homeserver = this._homeserver; if (!this._loginOptions.password) { const path = this.navigation.pathFrom([this.navigation.segment("session")]); this.navigation.applyPath(path); diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css index b6f474dd..2b65f82c 100644 --- a/src/platform/web/ui/css/login.css +++ b/src/platform/web/ui/css/login.css @@ -50,8 +50,8 @@ limitations under the License. margin: 0 20px; } -.LoginView { - padding: 0.4em; +.PasswordLoginView { + padding: 0 0.4em 0.4em; } .SessionLoadStatusView { @@ -81,7 +81,7 @@ limitations under the License. margin-top: 10px; } -.StartSSOLoginView_separator { +.LoginView_separator { justify-content: center; display: flex; margin: 8px; @@ -91,3 +91,11 @@ limitations under the License. display: flex; justify-content: center; } + +.LoginView_sso { + padding: 0.4em 0.4em 0; +} + +.LoginView_error { + padding: 0.4em +} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 74a1f5f8..facbefc8 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -228,7 +228,7 @@ a.button-action { border-radius: 8px; } -.StartSSOLoginView_separator { +.LoginView_separator { font-weight: 500; font-size: 1.5rem; } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 45c773bd..f260314d 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -20,12 +20,25 @@ import {PasswordLoginView} from "./PasswordLoginView.js"; import {CompleteSSOView} from "./CompleteSSOView.js"; export class LoginView extends TemplateView { - render(t) { - return t.div({ className: "PreSessionScreen" }, [ - t.div({ className: "logo" }), - t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm? new CompleteSSOView(vm): null), - t.mapView(vm => vm.passwordLoginViewModel, vm => vm? new PasswordLoginView(vm): null), - t.mapView(vm => vm.startSSOLoginViewModel, vm => vm? new StartSSOLoginView(vm): null), + render(t, vm) { + const homeserver = t.input({ + id: "homeserver", + type: "text", + placeholder: vm.i18n`Your matrix homeserver`, + value: vm.defaultHomeServer, + onChange: () => vm.updateHomeServer(homeserver.value), + }); + + return t.div({className: "PreSessionScreen"}, [ + t.div({className: "logo"}), + t.h1([vm.i18n`Sign In`]), + t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), + t.if(vm => !vm.completeSSOLoginViewModel, + (t, vm) => t.div({ className: "LoginView_sso form form-row" }, [t.label({ for: "homeserver" }, vm.i18n`Homeserver`), homeserver])), + t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null), + t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)), + t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null), + t.if(vm => vm.errorMessage, (t, vm) => t.h5({className: "LoginView_error"}, vm.i18n(vm.errorMessage))), // use t.mapView rather than t.if to create a new view when the view model changes too t.p(hydrogenGithubLink(t)) ]); @@ -35,10 +48,11 @@ export class LoginView extends TemplateView { class StartSSOLoginView extends TemplateView { render(t, vm) { return t.div({ className: "StartSSOLoginView" }, - [ - t.p({ className: "StartSSOLoginView_separator" }, "or"), - t.button({ className: "StartSSOLoginView_button button-action secondary", type: "button", onClick: () => vm.startSSOLogin() }, "Log in with SSO") - ] + t.button({ + className: "StartSSOLoginView_button button-action secondary", + type: "button", + onClick: () => vm.startSSOLogin() + }, vm.i18n`Log in with SSO`) ); } } diff --git a/src/platform/web/ui/login/PasswordLoginView.js b/src/platform/web/ui/login/PasswordLoginView.js index 7a41f2b5..b19daa2a 100644 --- a/src/platform/web/ui/login/PasswordLoginView.js +++ b/src/platform/web/ui/login/PasswordLoginView.js @@ -32,27 +32,17 @@ export class PasswordLoginView extends TemplateView { placeholder: vm.i18n`Password`, disabled }); - const homeserver = t.input({ - id: "homeserver", - type: "text", - placeholder: vm.i18n`Your matrix homeserver`, - value: vm.defaultHomeServer, - onChange: () => vm.updateHomeServer(homeserver.value), - disabled - }); - return t.div({className: "LoginView form"}, [ - t.h1([vm.i18n`Sign In`]), + return t.div({className: "PasswordLoginView form"}, [ t.if(vm => vm.error, t => t.div({ className: "error" }, vm => vm.error)), t.form({ onSubmit: evnt => { evnt.preventDefault(); - vm.login(username.value, password.value, homeserver.value); + vm.login(username.value, password.value); } }, [ t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]), t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), - t.div({ className: "form-row" }, [t.label({ for: "homeserver" }, vm.i18n`Homeserver`), homeserver]), t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), t.div({ className: "button-row" }, [ t.a({ From 073743927d166c0ed31a72ddc043ed71ebcf9326 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Aug 2021 18:52:06 +0200 Subject: [PATCH 049/108] only enable promise related rules ... I think? --- .ts-eslintrc.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js index 43392aef..1974e07b 100644 --- a/.ts-eslintrc.js +++ b/.ts-eslintrc.js @@ -5,8 +5,8 @@ module.exports = { "es6": true }, extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", + // "plugin:@typescript-eslint/recommended", + // "plugin:@typescript-eslint/recommended-requiring-type-checking", ], parser: '@typescript-eslint/parser', parserOptions: { @@ -17,5 +17,8 @@ module.exports = { plugins: [ '@typescript-eslint', ], - rules: {} + rules: { + "@typescript-eslint/no-floating-promises": 2, + "@typescript-eslint/no-misused-promises": 2 + } }; From c99c5e62baf2dd87edc06331b9ebf869c5522672 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Aug 2021 18:52:19 +0200 Subject: [PATCH 050/108] disable ts lint on CI --- .github/workflows/codechecks.js.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/codechecks.js.yml b/.github/workflows/codechecks.js.yml index 733e8b55..6ad2f42e 100644 --- a/.github/workflows/codechecks.js.yml +++ b/.github/workflows/codechecks.js.yml @@ -45,5 +45,3 @@ jobs: run: yarn run lint-ci - name: Typescript run: yarn run tsc - - name: Typescript lint - run: yarn run lint-ts From 17f1d6b16a6ed217b496a1a228d09c3a84b5f7a4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 20:13:34 +0530 Subject: [PATCH 051/108] Remove defaultHomeServer prop Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 42be2ae1..6460fbf7 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -25,22 +25,21 @@ export class LoginViewModel extends ViewModel { const {ready, defaultHomeServer, createSessionContainer, loginToken} = options; this._createSessionContainer = createSessionContainer; this._ready = ready; - this._defaultHomeServer = defaultHomeServer; this._loginToken = loginToken; this._sessionContainer = this._createSessionContainer(); this._loginOptions = null; this._passwordLoginViewModel = null; this._startSSOLoginViewModel = null; this._completeSSOLoginViewModel = null; - this._homeserver = null; + this._homeserver = defaultHomeServer; this._errorMessage = ""; - this._start(this._defaultHomeServer); + this._start(this._homeserver); } get passwordLoginViewModel() { return this._passwordLoginViewModel; } get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } - get defaultHomeServer() { return this._defaultHomeServer; } + get defaultHomeServer() { return this._homeserver; } get errorMessage() { return this._errorMessage; } async _start(homeserver) { @@ -110,7 +109,7 @@ export class LoginViewModel extends ViewModel { }, sessionContainer: this._sessionContainer, loginOptions: this._loginOptions, - homeserver: this._homeserver ?? this._defaultHomeServer + homeserver: this._homeserver } } From 738603e8909589a15232d32ef0759ed309a0c54b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 20:25:54 +0530 Subject: [PATCH 052/108] Rename start to createViewModels Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 6460fbf7..0f4a5915 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -33,7 +33,7 @@ export class LoginViewModel extends ViewModel { this._completeSSOLoginViewModel = null; this._homeserver = defaultHomeServer; this._errorMessage = ""; - this._start(this._homeserver); + this._createViewModels(this._homeserver); } get passwordLoginViewModel() { return this._passwordLoginViewModel; } @@ -42,7 +42,7 @@ export class LoginViewModel extends ViewModel { get defaultHomeServer() { return this._homeserver; } get errorMessage() { return this._errorMessage; } - async _start(homeserver) { + async _createViewModels(homeserver) { if (this._loginToken) { this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel(this.childOptions({loginToken: this._loginToken}))); this.emitChange("completeSSOLoginViewModel"); @@ -96,7 +96,7 @@ export class LoginViewModel extends ViewModel { updateHomeServer(newHomeserver) { this._homeserver = newHomeserver; this._disposeViewModels(); - this._start(newHomeserver); + this._createViewModels(newHomeserver); } childOptions(options = {}) { From 068fba3616c4f9937632bc9c95f6b7fa50442785 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 20:30:14 +0530 Subject: [PATCH 053/108] Inline method Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 0f4a5915..76876a9b 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -49,7 +49,12 @@ export class LoginViewModel extends ViewModel { } else { this._errorMessage = ""; - await this.queryLogin(homeserver); + try { + this._loginOptions = await this._sessionContainer.queryLogin(homeserver); + } + catch (e) { + this._loginOptions = null; + } if (this._loginOptions) { if (this._loginOptions.sso) { this._showSSOLogin(); } if (this._loginOptions.password) { this._showPasswordLogin(); } @@ -78,15 +83,6 @@ export class LoginViewModel extends ViewModel { this.emitChange("errorMessage"); } - async queryLogin(homeserver) { - try { - this._loginOptions = await this._sessionContainer.queryLogin(homeserver); - } - catch (e) { - this._loginOptions = null; - } - } - _disposeViewModels() { this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); From daf7af17b10200b5651682ad0a383737c2d9e78f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 20:35:07 +0530 Subject: [PATCH 054/108] Move logic to vm Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 1 + src/platform/web/ui/login/LoginView.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 76876a9b..73547957 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -41,6 +41,7 @@ export class LoginViewModel extends ViewModel { get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } get defaultHomeServer() { return this._homeserver; } get errorMessage() { return this._errorMessage; } + get showHomeserver() { return !this._completeSSOLoginViewModel; } async _createViewModels(homeserver) { if (this._loginToken) { diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index f260314d..ebcac433 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -33,7 +33,7 @@ export class LoginView extends TemplateView { t.div({className: "logo"}), t.h1([vm.i18n`Sign In`]), t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), - t.if(vm => !vm.completeSSOLoginViewModel, + t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" }, [t.label({ for: "homeserver" }, vm.i18n`Homeserver`), homeserver])), t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null), t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)), From 2d842c71743078cd49f2705edab716b8862e8c25 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 20:39:20 +0530 Subject: [PATCH 055/108] rename ssoEndpointLink to createSSORedirectURL Signed-off-by: RMidhunSuresh --- src/domain/login/StartSSOLoginViewModel.js | 2 +- src/matrix/login/SSOLoginHelper.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js index 315106ce..9a54d2c4 100644 --- a/src/domain/login/StartSSOLoginViewModel.js +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -26,7 +26,7 @@ export class StartSSOLoginViewModel extends ViewModel{ async startSSOLogin() { await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._homeserver); - const link = this._sso.ssoEndpointLink(this.urlCreator.createSSOCallbackURL()); + const link = this._sso.createSSORedirectURL(this.urlCreator.createSSOCallbackURL()); this.platform.openUrl(link); } } diff --git a/src/matrix/login/SSOLoginHelper.js b/src/matrix/login/SSOLoginHelper.js index 701d00b4..376e81ba 100644 --- a/src/matrix/login/SSOLoginHelper.js +++ b/src/matrix/login/SSOLoginHelper.js @@ -19,7 +19,7 @@ export class SSOLoginHelper{ this._homeserver = homeserver; } - ssoEndpointLink(redirectURL) { - return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${redirectURL}`; + createSSORedirectURL(returnURL) { + return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${returnURL}`; } } From ed278e3e5a4e9d038a01720231232ff0d872057c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 20:41:24 +0530 Subject: [PATCH 056/108] Remove unwanted check Signed-off-by: RMidhunSuresh --- src/domain/login/PasswordLoginViewModel.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 32aef2dd..00707a0a 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -42,11 +42,6 @@ export class PasswordLoginViewModel extends ViewModel { async login(username, password) { const homeserver = this._homeserver; - if (!this._loginOptions.password) { - const path = this.navigation.pathFrom([this.navigation.segment("session")]); - this.navigation.applyPath(path); - return; - } this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); if (this._loadViewModel) { this._loadViewModel = this.disposeTracked(this._loadViewModel); From 3af2ae3bddee60b53b07718a68f25be59d1fbbcf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 20:43:03 +0530 Subject: [PATCH 057/108] make method private Signed-off-by: RMidhunSuresh --- src/matrix/SessionContainer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index edbe0f28..40bc1be0 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -100,7 +100,7 @@ export class SessionContainer { }); } - parseLoginOptions(options, homeServer) { + _parseLoginOptions(options, homeServer) { /* Take server response and return new object which has two props password and sso which implements LoginMethod @@ -125,7 +125,7 @@ export class SessionContainer { const normalizedHS = normalizeHomeserver(homeServer); const hsApi = new HomeServerApi({homeServer: normalizedHS, request: this._platform.request}); const response = await hsApi.getLoginFlows().response(); - return this.parseLoginOptions(response, normalizedHS); + return this._parseLoginOptions(response, normalizedHS); } async startWithLogin(loginMethod) { From 5ab405fc30211b525ae39204a30419eb58c1105f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 21:37:26 +0530 Subject: [PATCH 058/108] Move back-button to login view Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 1 + src/domain/login/PasswordLoginViewModel.js | 1 - src/platform/web/ui/css/login.css | 3 ++- src/platform/web/ui/css/themes/element/theme.css | 5 +++++ src/platform/web/ui/login/LoginView.js | 1 + src/platform/web/ui/login/PasswordLoginView.js | 4 ---- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 73547957..d6cd6eed 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -42,6 +42,7 @@ export class LoginViewModel extends ViewModel { get defaultHomeServer() { return this._homeserver; } get errorMessage() { return this._errorMessage; } get showHomeserver() { return !this._completeSSOLoginViewModel; } + get cancelUrl() { return this.urlCreator.urlForSegment("session"); } async _createViewModels(homeserver) { if (this._loginToken) { diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 00707a0a..f53869ec 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -30,7 +30,6 @@ export class PasswordLoginViewModel extends ViewModel { } get loadViewModel() {return this._loadViewModel; } - get cancelUrl() { return this.urlCreator.urlForSegment("session"); } get isBusy() { if (!this._loadViewModel) { diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css index 2b65f82c..01b6bc6e 100644 --- a/src/platform/web/ui/css/login.css +++ b/src/platform/web/ui/css/login.css @@ -74,11 +74,12 @@ limitations under the License. .StartSSOLoginView { display: flex; flex-direction: column; + padding: 0 0.4em 0; } .StartSSOLoginView_button { flex: 1; - margin-top: 10px; + margin-top: 12px; } .LoginView_separator { diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index facbefc8..08f1df06 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -228,6 +228,11 @@ a.button-action { border-radius: 8px; } +.LoginView_back { + background-image: url("./icons/chevron-left.svg"); + background-color: transparent; +} + .LoginView_separator { font-weight: 500; font-size: 1.5rem; diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index ebcac433..c15d69ba 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -30,6 +30,7 @@ export class LoginView extends TemplateView { }); return t.div({className: "PreSessionScreen"}, [ + t.a({className: "button-utility LoginView_back", href: vm.cancelUrl}), t.div({className: "logo"}), t.h1([vm.i18n`Sign In`]), t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), diff --git a/src/platform/web/ui/login/PasswordLoginView.js b/src/platform/web/ui/login/PasswordLoginView.js index b19daa2a..0f330993 100644 --- a/src/platform/web/ui/login/PasswordLoginView.js +++ b/src/platform/web/ui/login/PasswordLoginView.js @@ -45,10 +45,6 @@ export class PasswordLoginView extends TemplateView { t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), t.div({ className: "button-row" }, [ - t.a({ - className: "button-action secondary", - href: vm.cancelUrl - }, [vm.i18n`Go Back`]), t.button({ className: "button-action primary", type: "submit" From 80ea48e8a19f2633418a2bb338eba6d5c05083fc Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 19 Aug 2021 21:43:44 +0530 Subject: [PATCH 059/108] Move input into t.if Signed-off-by: RMidhunSuresh --- src/platform/web/ui/login/LoginView.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index c15d69ba..bd087dd2 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -21,21 +21,23 @@ import {CompleteSSOView} from "./CompleteSSOView.js"; export class LoginView extends TemplateView { render(t, vm) { - const homeserver = t.input({ - id: "homeserver", - type: "text", - placeholder: vm.i18n`Your matrix homeserver`, - value: vm.defaultHomeServer, - onChange: () => vm.updateHomeServer(homeserver.value), - }); - return t.div({className: "PreSessionScreen"}, [ t.a({className: "button-utility LoginView_back", href: vm.cancelUrl}), t.div({className: "logo"}), t.h1([vm.i18n`Sign In`]), t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), - t.if(vm => vm.showHomeserver, - (t, vm) => t.div({ className: "LoginView_sso form form-row" }, [t.label({ for: "homeserver" }, vm.i18n`Homeserver`), homeserver])), + t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" }, + [ + t.label({for: "homeserver"}, vm.i18n`Homeserver`), + t.input({ + id: "homeserver", + type: "text", + placeholder: vm.i18n`Your matrix homeserver`, + value: vm.defaultHomeServer, + onChange: event => vm.updateHomeServer(event.target.value), + }) + ] + )), t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null), t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)), t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null), From bdc860eb798fdae49bd08c0e6983e6aa31791bea Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 15:19:42 +0530 Subject: [PATCH 060/108] Refactor to pull loadvm into login vm Signed-off-by: RMidhunSuresh --- src/domain/RootViewModel.js | 8 +-- src/domain/SessionLoadViewModel.js | 21 +------ src/domain/login/CompleteSSOLoginViewModel.js | 45 +++++++------ src/domain/login/LoginViewModel.js | 63 +++++++++++++++++-- src/domain/login/PasswordLoginViewModel.js | 58 ++++++++--------- src/matrix/SessionContainer.js | 10 +++ src/platform/web/ui/login/LoginView.js | 5 +- .../web/ui/login/PasswordLoginView.js | 5 +- 8 files changed, 128 insertions(+), 87 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 24982a71..4a0a969e 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -133,13 +133,11 @@ export class RootViewModel extends ViewModel { } _showSessionLoader(sessionId) { + const sessionContainer = this._createSessionContainer(); + sessionContainer.startWithExistingSession(sessionId); this._setSection(() => { this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({ - createAndStartSessionContainer: () => { - const sessionContainer = this._createSessionContainer(); - sessionContainer.startWithExistingSession(sessionId); - return sessionContainer; - }, + sessionContainer, ready: sessionContainer => this._showSession(sessionContainer) })); this._sessionLoadViewModel.start(); diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 80837677..956ba3a4 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; +import {LoadStatus} from "../matrix/SessionContainer.js"; import {SyncStatus} from "../matrix/Sync.js"; import {ViewModel} from "./ViewModel.js"; export class SessionLoadViewModel extends ViewModel { constructor(options) { super(options); - const {createAndStartSessionContainer, ready, homeserver, deleteSessionOnCancel} = options; - this._createAndStartSessionContainer = createAndStartSessionContainer; + const {sessionContainer, ready, homeserver, deleteSessionOnCancel} = options; + this._sessionContainer = sessionContainer; this._ready = ready; this._homeserver = homeserver; this._deleteSessionOnCancel = deleteSessionOnCancel; @@ -38,7 +38,6 @@ export class SessionLoadViewModel extends ViewModel { try { this._loading = true; this.emitChange("loading"); - this._sessionContainer = await this._createAndStartSessionContainer(); this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { this.emitChange("loadLabel"); // wait for initial sync, but not catchup sync @@ -111,20 +110,6 @@ export class SessionLoadViewModel extends ViewModel { if (sc) { switch (sc.loadStatus.get()) { - case LoadStatus.NotLoading: - return `Preparing…`; - case LoadStatus.Login: - return `Checking your credentials…`; - case LoadStatus.LoginFailed: - switch (sc.loginFailure) { - case LoginFailure.LoginFailure: - return `Your credentials don't seem to be correct.`; - case LoginFailure.Connection: - return `Can't connect to ${this._homeserver}.`; - case LoginFailure.Unknown: - return `Something went wrong while checking your credentials.`; - } - break; case LoadStatus.SessionSetup: return `Setting up your encryption keys…`; case LoadStatus.Loading: diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index bf98cefc..90d25fda 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel} from "../ViewModel.js"; -import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; +import {LoginFailure} from "../../matrix/SessionContainer.js"; export class CompleteSSOLoginViewModel extends ViewModel { constructor(options) { @@ -24,17 +24,17 @@ export class CompleteSSOLoginViewModel extends ViewModel { loginToken, sessionContainer, ready, + attemptLogin, + showError, } = options; this._loginToken = loginToken; this._ready = ready; this._sessionContainer = sessionContainer; - this._loadViewModelSubscription = null; - this._loadViewModel = null; + this._attemptLogin = attemptLogin; + this._showError = showError; this.performSSOLoginCompletion(); } - get loadViewModel() { return this._loadViewModel; } - async performSSOLoginCompletion() { if (!this._loginToken) { return; @@ -46,25 +46,22 @@ export class CompleteSSOLoginViewModel extends ViewModel { this.navigation.applyPath(path); return; } - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - if (this._loadViewModel) { - this._loadViewModel = this.disposeTracked(this._loadViewModel); + const status = await this._attemptLogin(loginOptions.token(this._loginToken)); + let error = ""; + switch (status) { + case LoginFailure.Credentials: + error = `Your login-token is invalid.`; + break; + case LoginFailure.Connection: + error = `Can't connect to ${this._homeserver}.`; + break; + case LoginFailure.Unknown: + error = `Something went wrong while checking your login-token.`; + break; + } + if (error) { + this._showError(error); + this._sessionContainer.resetStatus(); } - this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ - createAndStartSessionContainer: async () => { - this._sessionContainer.startWithLogin(loginOptions.token(this._loginToken)); - return this._sessionContainer; - }, - ready: this._ready, - homeserver, - }))); - this._loadViewModel.start(); - this.emitChange("loadViewModel"); - this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => { - if (!this._loadViewModel.loading) { - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - } - this.emitChange("isBusy"); - })); } } diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index d6cd6eed..63c4d253 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -18,6 +18,8 @@ import {ViewModel} from "../ViewModel.js"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; +import {LoadStatus} from "../../matrix/SessionContainer.js"; +import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; export class LoginViewModel extends ViewModel { constructor(options) { @@ -31,8 +33,12 @@ export class LoginViewModel extends ViewModel { this._passwordLoginViewModel = null; this._startSSOLoginViewModel = null; this._completeSSOLoginViewModel = null; + this._loadViewModel = null; + this._loadViewModelSubscription = null; this._homeserver = defaultHomeServer; this._errorMessage = ""; + this._hideHomeserver = false; + this._isBusy = false; this._createViewModels(this._homeserver); } @@ -41,11 +47,14 @@ export class LoginViewModel extends ViewModel { get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } get defaultHomeServer() { return this._homeserver; } get errorMessage() { return this._errorMessage; } - get showHomeserver() { return !this._completeSSOLoginViewModel; } + get showHomeserver() { return !this._hideHomeserver; } get cancelUrl() { return this.urlCreator.urlForSegment("session"); } + get loadViewModel() {return this._loadViewModel; } + get isBusy() { return this._isBusy; } async _createViewModels(homeserver) { if (this._loginToken) { + this._hideHomeserver = true; this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel(this.childOptions({loginToken: this._loginToken}))); this.emitChange("completeSSOLoginViewModel"); } @@ -61,11 +70,11 @@ export class LoginViewModel extends ViewModel { if (this._loginOptions.sso) { this._showSSOLogin(); } if (this._loginOptions.password) { this._showPasswordLogin(); } if (!this._loginOptions.sso && !this._loginOptions.password) { - this._showError("This homeserver neither supports SSO nor Password based login flows"); + this.showError("This homeserver neither supports SSO nor Password based login flows"); } } else { - this._showError("Could not query login methods supported by the homeserver"); + this.showError("Could not query login methods supported by the homeserver"); } } } @@ -80,14 +89,56 @@ export class LoginViewModel extends ViewModel { this.emitChange("startSSOLoginViewModel"); } - _showError(message) { + showError(message) { this._errorMessage = message; this.emitChange("errorMessage"); + this._errorMessage = ""; + } + + _toggleBusy(status) { + this._isBusy = status; + this.emitChange("isBusy"); + } + + async attemptLogin(loginMethod) { + this._toggleBusy(true); + this._sessionContainer.startWithLogin(loginMethod); + const loadStatus = this._sessionContainer.loadStatus; + const handle = loadStatus.waitFor(status => status !== LoadStatus.Login); + await handle.promise; + this._toggleBusy(false); + const status = loadStatus.get(); + if (status === LoadStatus.LoginFailed) { + return this._sessionContainer.loginFailure; + } + this._hideHomeserver = true; + this._disposeViewModels(); + this._createLoadViewModel(); + return null; + } + + _createLoadViewModel() { + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + if (this._loadViewModel) { + this._loadViewModel = this.disposeTracked(this._loadViewModel); + } + this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions())); + this._loadViewModel.start(); + this.emitChange("loadViewModel"); + this._loadViewModelSubscription = this.track( + this._loadViewModel.disposableOn("change", () => { + if (!this._loadViewModel.loading) { + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + } + this.emitChange("isBusy"); + }) + ); } _disposeViewModels() { this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); + this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel); this.emitChange("disposeViewModels"); } @@ -107,7 +158,9 @@ export class LoginViewModel extends ViewModel { }, sessionContainer: this._sessionContainer, loginOptions: this._loginOptions, - homeserver: this._homeserver + homeserver: this._homeserver, + attemptLogin: loginMethod => this.attemptLogin(loginMethod), + showError: message => this.showError(message) } } diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index f53869ec..99f54826 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -15,51 +15,47 @@ limitations under the License. */ import {ViewModel} from "../ViewModel.js"; -import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; +import {LoginFailure} from "../../matrix/SessionContainer.js"; export class PasswordLoginViewModel extends ViewModel { constructor(options) { super(options); - const {ready, loginOptions, sessionContainer, homeserver} = options; + const {ready, loginOptions, sessionContainer, homeserver, attemptLogin, showError} = options; this._ready = ready; this._sessionContainer = sessionContainer; - this._loadViewModel = null; - this._loadViewModelSubscription = null; this._loginOptions = loginOptions; + this._attemptLogin = attemptLogin; + this._showError = showError; this._homeserver = homeserver; + this._isBusy = false; } - get loadViewModel() {return this._loadViewModel; } + get isBusy() { return this._isBusy; } - get isBusy() { - if (!this._loadViewModel) { - return false; - } else { - return this._loadViewModel.loading; - } + _toggleBusy(state) { + this._isBusy = state; + this.emitChange("isBusy"); } async login(username, password) { - const homeserver = this._homeserver; - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - if (this._loadViewModel) { - this._loadViewModel = this.disposeTracked(this._loadViewModel); + this._toggleBusy(true); + const status = await this._attemptLogin(this._loginOptions.password(username, password)); + this._toggleBusy(false); + let error = ""; + switch (status) { + case LoginFailure.Credentials: + error = `Your credentials don't seem to be correct.`; + break; + case LoginFailure.Connection: + error = `Can't connect to ${this._homeserver}.`; + break; + case LoginFailure.Unknown: + error = `Something went wrong while checking your credentials.`; + break; + } + if (error) { + this._showError(error); + this._sessionContainer.resetStatus(); } - this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ - createAndStartSessionContainer: async () => { - this._sessionContainer.startWithLogin(this._loginOptions.password(username, password)); - return this._sessionContainer; - }, - ready: this._ready, - homeserver, - }))); - this._loadViewModel.start(); - this.emitChange("loadViewModel"); - this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => { - if (!this._loadViewModel.loading) { - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - } - this.emitChange("isBusy"); - })); } } diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 40bc1be0..c078f95f 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -300,6 +300,10 @@ export class SessionContainer { return this._error; } + get loginFailure() { + return this._loginFailure; + } + /** only set at loadStatus InitialSync, CatchupSync or Ready */ get sync() { return this._sync; @@ -349,4 +353,10 @@ export class SessionContainer { this._sessionId = null; } } + + resetStatus() { + this._status.set(LoadStatus.NotLoading); + this._error = null; + this._loginFailure = null; + } } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index bd087dd2..ad7f72a4 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -18,6 +18,7 @@ import {TemplateView} from "../general/TemplateView.js"; import {hydrogenGithubLink} from "./common.js"; import {PasswordLoginView} from "./PasswordLoginView.js"; import {CompleteSSOView} from "./CompleteSSOView.js"; +import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; export class LoginView extends TemplateView { render(t, vm) { @@ -34,6 +35,7 @@ export class LoginView extends TemplateView { type: "text", placeholder: vm.i18n`Your matrix homeserver`, value: vm.defaultHomeServer, + disabled: vm => vm.isBusy, onChange: event => vm.updateHomeServer(event.target.value), }) ] @@ -41,7 +43,8 @@ export class LoginView extends TemplateView { t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null), t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)), t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null), - t.if(vm => vm.errorMessage, (t, vm) => t.h5({className: "LoginView_error"}, vm.i18n(vm.errorMessage))), + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "LoginView_error"}, vm.i18n(vm.errorMessage))), + t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), // use t.mapView rather than t.if to create a new view when the view model changes too t.p(hydrogenGithubLink(t)) ]); diff --git a/src/platform/web/ui/login/PasswordLoginView.js b/src/platform/web/ui/login/PasswordLoginView.js index 0f330993..ca26d1cc 100644 --- a/src/platform/web/ui/login/PasswordLoginView.js +++ b/src/platform/web/ui/login/PasswordLoginView.js @@ -15,7 +15,6 @@ limitations under the License. */ import {TemplateView} from "../general/TemplateView.js"; -import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; export class PasswordLoginView extends TemplateView { render(t, vm) { @@ -43,11 +42,11 @@ export class PasswordLoginView extends TemplateView { }, [ t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]), t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), - t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), t.div({ className: "button-row" }, [ t.button({ className: "button-action primary", - type: "submit" + type: "submit", + disabled }, vm.i18n`Log In`), ]), ]) From dadeb7f3e53a850bd806f3058e7729f0d9501f7e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 15:42:43 +0530 Subject: [PATCH 061/108] Do not override childOptions Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 4 +- src/domain/login/LoginViewModel.js | 54 ++++++++++++------- src/domain/login/PasswordLoginViewModel.js | 3 +- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index 90d25fda..9434f84a 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -23,12 +23,10 @@ export class CompleteSSOLoginViewModel extends ViewModel { const { loginToken, sessionContainer, - ready, attemptLogin, showError, } = options; this._loginToken = loginToken; - this._ready = ready; this._sessionContainer = sessionContainer; this._attemptLogin = attemptLogin; this._showError = showError; @@ -53,7 +51,7 @@ export class CompleteSSOLoginViewModel extends ViewModel { error = `Your login-token is invalid.`; break; case LoginFailure.Connection: - error = `Can't connect to ${this._homeserver}.`; + error = `Can't connect to ${homeserver}.`; break; case LoginFailure.Unknown: error = `Something went wrong while checking your login-token.`; diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 63c4d253..df779ec0 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -55,7 +55,14 @@ export class LoginViewModel extends ViewModel { async _createViewModels(homeserver) { if (this._loginToken) { this._hideHomeserver = true; - this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel(this.childOptions({loginToken: this._loginToken}))); + this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel( + this.childOptions( + { + sessionContainer: this._sessionContainer, + attemptLogin: loginMethod => this.attemptLogin(loginMethod), + showError: message => this.showError(message), + loginToken: this._loginToken + }))); this.emitChange("completeSSOLoginViewModel"); } else { @@ -80,12 +87,23 @@ export class LoginViewModel extends ViewModel { } _showPasswordLogin() { - this._passwordLoginViewModel = this.track(new PasswordLoginViewModel(this.childOptions())); + this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( + this.childOptions({ + sessionContainer: this._sessionContainer, + loginOptions: this._loginOptions, + homeserver: this._homeserver, + attemptLogin: loginMethod => this.attemptLogin(loginMethod), + showError: message => this.showError(message) + }))); this.emitChange("passwordLoginViewModel"); } _showSSOLogin() { - this._startSSOLoginViewModel = this.track(new StartSSOLoginViewModel(this.childOptions())); + this._startSSOLoginViewModel = this.track( + new StartSSOLoginViewModel( + this.childOptions({ loginOptions: this._loginOptions, homeserver: this._homeserver }) + ) + ); this.emitChange("startSSOLoginViewModel"); } @@ -122,7 +140,19 @@ export class LoginViewModel extends ViewModel { if (this._loadViewModel) { this._loadViewModel = this.disposeTracked(this._loadViewModel); } - this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions())); + this._loadViewModel = this.track( + new SessionLoadViewModel( + this.childOptions({ + ready: (sessionContainer) => { + // make sure we don't delete the session in dispose when navigating away + this._sessionContainer = null; + this._ready(sessionContainer); + }, + sessionContainer: this._sessionContainer, + homeserver: this._homeserver + }) + ) + ); this._loadViewModel.start(); this.emitChange("loadViewModel"); this._loadViewModelSubscription = this.track( @@ -148,22 +178,6 @@ export class LoginViewModel extends ViewModel { this._createViewModels(newHomeserver); } - childOptions(options = {}) { - return { - ...super.childOptions(options), - ready: sessionContainer => { - // make sure we don't delete the session in dispose when navigating away - this._sessionContainer = null; - this._ready(sessionContainer); - }, - sessionContainer: this._sessionContainer, - loginOptions: this._loginOptions, - homeserver: this._homeserver, - attemptLogin: loginMethod => this.attemptLogin(loginMethod), - showError: message => this.showError(message) - } - } - dispose() { super.dispose(); if (this._sessionContainer) { diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 99f54826..d08b8312 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -20,8 +20,7 @@ import {LoginFailure} from "../../matrix/SessionContainer.js"; export class PasswordLoginViewModel extends ViewModel { constructor(options) { super(options); - const {ready, loginOptions, sessionContainer, homeserver, attemptLogin, showError} = options; - this._ready = ready; + const {loginOptions, sessionContainer, homeserver, attemptLogin, showError} = options; this._sessionContainer = sessionContainer; this._loginOptions = loginOptions; this._attemptLogin = attemptLogin; From 2468bc3e9fa2d2e1ae5444544e86e996acd0db09 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 15:47:30 +0530 Subject: [PATCH 062/108] Remove homeserver prop Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 4 +--- src/domain/login/StartSSOLoginViewModel.js | 6 ++---- src/matrix/login/SSOLoginHelper.js | 2 ++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index df779ec0..78457be0 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -100,9 +100,7 @@ export class LoginViewModel extends ViewModel { _showSSOLogin() { this._startSSOLoginViewModel = this.track( - new StartSSOLoginViewModel( - this.childOptions({ loginOptions: this._loginOptions, homeserver: this._homeserver }) - ) + new StartSSOLoginViewModel(this.childOptions({loginOptions: this._loginOptions})) ); this.emitChange("startSSOLoginViewModel"); } diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js index 9a54d2c4..41f6dbeb 100644 --- a/src/domain/login/StartSSOLoginViewModel.js +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -19,13 +19,11 @@ import {ViewModel} from "../ViewModel.js"; export class StartSSOLoginViewModel extends ViewModel{ constructor(options) { super(options); - const {loginOptions, homeserver} = options; - this._sso = loginOptions.sso; - this._homeserver = homeserver; + this._sso = options.loginOptions.sso; } async startSSOLogin() { - await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._homeserver); + await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._sso.homeserver); const link = this._sso.createSSORedirectURL(this.urlCreator.createSSOCallbackURL()); this.platform.openUrl(link); } diff --git a/src/matrix/login/SSOLoginHelper.js b/src/matrix/login/SSOLoginHelper.js index 376e81ba..a15c8ef9 100644 --- a/src/matrix/login/SSOLoginHelper.js +++ b/src/matrix/login/SSOLoginHelper.js @@ -19,6 +19,8 @@ export class SSOLoginHelper{ this._homeserver = homeserver; } + get homeserver() { return this._homeserver; } + createSSORedirectURL(returnURL) { return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${returnURL}`; } From 9482998b15c0533abb636541a9e0f49ba0811c83 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 19:08:47 +0530 Subject: [PATCH 063/108] Internationalize and remove dash Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index 9434f84a..2ec2f371 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -48,13 +48,13 @@ export class CompleteSSOLoginViewModel extends ViewModel { let error = ""; switch (status) { case LoginFailure.Credentials: - error = `Your login-token is invalid.`; + error = this.i18n`Your logintoken is invalid.`; break; case LoginFailure.Connection: - error = `Can't connect to ${homeserver}.`; + error = this.i18n`Can't connect to ${homeserver}.`; break; case LoginFailure.Unknown: - error = `Something went wrong while checking your login-token.`; + error = this.i18n`Something went wrong while checking your logintoken.`; break; } if (error) { From c650b358319e38d5f9ce6021f6fd44ee13297af7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 20:05:36 +0530 Subject: [PATCH 064/108] resetStatus from within startLogin Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 1 - src/domain/login/LoginViewModel.js | 1 - src/domain/login/PasswordLoginViewModel.js | 4 +--- src/matrix/SessionContainer.js | 6 ++++-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index 2ec2f371..1fed0b41 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -59,7 +59,6 @@ export class CompleteSSOLoginViewModel extends ViewModel { } if (error) { this._showError(error); - this._sessionContainer.resetStatus(); } } } diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 78457be0..9e8ed146 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -89,7 +89,6 @@ export class LoginViewModel extends ViewModel { _showPasswordLogin() { this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( this.childOptions({ - sessionContainer: this._sessionContainer, loginOptions: this._loginOptions, homeserver: this._homeserver, attemptLogin: loginMethod => this.attemptLogin(loginMethod), diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index d08b8312..76814806 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -20,8 +20,7 @@ import {LoginFailure} from "../../matrix/SessionContainer.js"; export class PasswordLoginViewModel extends ViewModel { constructor(options) { super(options); - const {loginOptions, sessionContainer, homeserver, attemptLogin, showError} = options; - this._sessionContainer = sessionContainer; + const {loginOptions, homeserver, attemptLogin, showError} = options; this._loginOptions = loginOptions; this._attemptLogin = attemptLogin; this._showError = showError; @@ -54,7 +53,6 @@ export class PasswordLoginViewModel extends ViewModel { } if (error) { this._showError(error); - this._sessionContainer.resetStatus(); } } } diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index c078f95f..ca8379bc 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -129,9 +129,11 @@ export class SessionContainer { } async startWithLogin(loginMethod) { - if (this._status.get() !== LoadStatus.NotLoading) { + const currentStatus = this._status.get(); + if (currentStatus !== LoadStatus.LoginFailed && currentStatus !== LoadStatus.NotLoading) { return; } + this._resetStatus(); await this._platform.logger.run("login", async log => { this._status.set(LoadStatus.Login); const clock = this._platform.clock; @@ -354,7 +356,7 @@ export class SessionContainer { } } - resetStatus() { + _resetStatus() { this._status.set(LoadStatus.NotLoading); this._error = null; this._loginFailure = null; From 5ca732341a0e2756d0c75e2fb1ad7b9863065df6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 20:09:12 +0530 Subject: [PATCH 065/108] Rename defaultHomeserver to homeserver Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 2 +- src/platform/web/ui/login/LoginView.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 9e8ed146..61fd3967 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -45,7 +45,7 @@ export class LoginViewModel extends ViewModel { get passwordLoginViewModel() { return this._passwordLoginViewModel; } get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } - get defaultHomeServer() { return this._homeserver; } + get homeserver() { return this._homeserver; } get errorMessage() { return this._errorMessage; } get showHomeserver() { return !this._hideHomeserver; } get cancelUrl() { return this.urlCreator.urlForSegment("session"); } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index ad7f72a4..1a146051 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -34,7 +34,7 @@ export class LoginView extends TemplateView { id: "homeserver", type: "text", placeholder: vm.i18n`Your matrix homeserver`, - value: vm.defaultHomeServer, + value: vm.homeserver, disabled: vm => vm.isBusy, onChange: event => vm.updateHomeServer(event.target.value), }) From 0e7a9e224c0cd3e0925faecbc4487796ae843863 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 20:10:31 +0530 Subject: [PATCH 066/108] Remove unwanted if Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 61fd3967..e7d7bf49 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -134,9 +134,7 @@ export class LoginViewModel extends ViewModel { _createLoadViewModel() { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - if (this._loadViewModel) { - this._loadViewModel = this.disposeTracked(this._loadViewModel); - } + this._loadViewModel = this.disposeTracked(this._loadViewModel); this._loadViewModel = this.track( new SessionLoadViewModel( this.childOptions({ From 784b06d5009e368bdc6def2a3051fb7a275c041d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 20:12:25 +0530 Subject: [PATCH 067/108] Fix emit for isBusy Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index e7d7bf49..26d0ca88 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -155,7 +155,7 @@ export class LoginViewModel extends ViewModel { if (!this._loadViewModel.loading) { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); } - this.emitChange("isBusy"); + this._toggleBusy(false); }) ); } From ff8417dfe250c5cc0914c1ef1044d14def777ea4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 20:31:22 +0530 Subject: [PATCH 068/108] Set busy state from login vm Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 4 ++++ src/domain/login/PasswordLoginViewModel.js | 4 +--- src/domain/login/StartSSOLoginViewModel.js | 8 ++++++++ src/platform/web/ui/login/LoginView.js | 3 ++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 26d0ca88..381dc260 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -117,11 +117,15 @@ export class LoginViewModel extends ViewModel { async attemptLogin(loginMethod) { this._toggleBusy(true); + this._passwordLoginViewModel?.toggleBusy(true); + this._startSSOLoginViewModel?.toggleBusy(true); this._sessionContainer.startWithLogin(loginMethod); const loadStatus = this._sessionContainer.loadStatus; const handle = loadStatus.waitFor(status => status !== LoadStatus.Login); await handle.promise; this._toggleBusy(false); + this._passwordLoginViewModel?.toggleBusy(false); + this._startSSOLoginViewModel?.toggleBusy(false); const status = loadStatus.get(); if (status === LoadStatus.LoginFailed) { return this._sessionContainer.loginFailure; diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 76814806..64a48f37 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -30,15 +30,13 @@ export class PasswordLoginViewModel extends ViewModel { get isBusy() { return this._isBusy; } - _toggleBusy(state) { + toggleBusy(state) { this._isBusy = state; this.emitChange("isBusy"); } async login(username, password) { - this._toggleBusy(true); const status = await this._attemptLogin(this._loginOptions.password(username, password)); - this._toggleBusy(false); let error = ""; switch (status) { case LoginFailure.Credentials: diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js index 41f6dbeb..e4c0cf92 100644 --- a/src/domain/login/StartSSOLoginViewModel.js +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -20,6 +20,14 @@ export class StartSSOLoginViewModel extends ViewModel{ constructor(options) { super(options); this._sso = options.loginOptions.sso; + this._isBusy = false; + } + + get isBusy() { return this._isBusy; } + + toggleBusy(state) { + this._isBusy = state; + this.emitChange("isBusy"); } async startSSOLogin() { diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 1a146051..7672a719 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -57,7 +57,8 @@ class StartSSOLoginView extends TemplateView { t.button({ className: "StartSSOLoginView_button button-action secondary", type: "button", - onClick: () => vm.startSSOLogin() + onClick: () => vm.startSSOLogin(), + disabled: vm => vm.isBusy }, vm.i18n`Log in with SSO`) ); } From 355468b6372d662f04beeb5e6a1c8cbfbd5ab351 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 20:36:33 +0530 Subject: [PATCH 069/108] Internationalize + add back old message Signed-off-by: RMidhunSuresh --- src/domain/login/PasswordLoginViewModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 64a48f37..d23cda4a 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -40,13 +40,13 @@ export class PasswordLoginViewModel extends ViewModel { let error = ""; switch (status) { case LoginFailure.Credentials: - error = `Your credentials don't seem to be correct.`; + error = this.i18n`Your username and/or password don't seem to be correct.`; break; case LoginFailure.Connection: - error = `Can't connect to ${this._homeserver}.`; + error = this.i18n`Can't connect to ${this._homeserver}.`; break; case LoginFailure.Unknown: - error = `Something went wrong while checking your credentials.`; + error = this.i18n`Something went wrong while checking your login and password.`; break; } if (error) { From a2677a64006c52dab0a18efe110d73274a5547a5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 21:22:12 +0530 Subject: [PATCH 070/108] Separate errors for each vm Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 11 +++++++++-- src/domain/login/PasswordLoginViewModel.js | 11 +++++++++-- src/platform/web/ui/login/CompleteSSOView.js | 3 ++- src/platform/web/ui/login/LoginView.js | 2 +- src/platform/web/ui/login/PasswordLoginView.js | 1 + 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index 1fed0b41..9bdb0509 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -24,15 +24,22 @@ export class CompleteSSOLoginViewModel extends ViewModel { loginToken, sessionContainer, attemptLogin, - showError, } = options; this._loginToken = loginToken; this._sessionContainer = sessionContainer; this._attemptLogin = attemptLogin; - this._showError = showError; + this._errorMessage = ""; this.performSSOLoginCompletion(); } + get errorMessage() { return this._errorMessage; } + + _showError(message) { + this._errorMessage = message; + this.emitChange("errorMessage"); + this._errorMessage = ""; + } + async performSSOLoginCompletion() { if (!this._loginToken) { return; diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index d23cda4a..3524779e 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -20,21 +20,28 @@ import {LoginFailure} from "../../matrix/SessionContainer.js"; export class PasswordLoginViewModel extends ViewModel { constructor(options) { super(options); - const {loginOptions, homeserver, attemptLogin, showError} = options; + const {loginOptions, homeserver, attemptLogin} = options; this._loginOptions = loginOptions; this._attemptLogin = attemptLogin; - this._showError = showError; this._homeserver = homeserver; this._isBusy = false; + this._errorMessage = ""; } get isBusy() { return this._isBusy; } + get errorMessage() { return this._errorMessage; } toggleBusy(state) { this._isBusy = state; this.emitChange("isBusy"); } + _showError(message) { + this._errorMessage = message; + this.emitChange("errorMessage"); + this._errorMessage = ""; + } + async login(username, password) { const status = await this._attemptLogin(this._loginOptions.password(username, password)); let error = ""; diff --git a/src/platform/web/ui/login/CompleteSSOView.js b/src/platform/web/ui/login/CompleteSSOView.js index cece7184..8b0654c6 100644 --- a/src/platform/web/ui/login/CompleteSSOView.js +++ b/src/platform/web/ui/login/CompleteSSOView.js @@ -22,7 +22,8 @@ export class CompleteSSOView extends TemplateView { return t.div({ className: "CompleteSSOView" }, [ t.p({ className: "CompleteSSOView_title" }, "Finishing up your SSO Login"), - t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null) + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "CompleteSSOView_error"}, vm.i18n(vm.errorMessage))), + t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), ] ); } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 7672a719..1dcbde3d 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -29,6 +29,7 @@ export class LoginView extends TemplateView { t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" }, [ + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "LoginView_error"}, vm.i18n(vm.errorMessage))), t.label({for: "homeserver"}, vm.i18n`Homeserver`), t.input({ id: "homeserver", @@ -43,7 +44,6 @@ export class LoginView extends TemplateView { t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null), t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)), t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null), - t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "LoginView_error"}, vm.i18n(vm.errorMessage))), t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), // use t.mapView rather than t.if to create a new view when the view model changes too t.p(hydrogenGithubLink(t)) diff --git a/src/platform/web/ui/login/PasswordLoginView.js b/src/platform/web/ui/login/PasswordLoginView.js index ca26d1cc..7fd7d534 100644 --- a/src/platform/web/ui/login/PasswordLoginView.js +++ b/src/platform/web/ui/login/PasswordLoginView.js @@ -40,6 +40,7 @@ export class PasswordLoginView extends TemplateView { vm.login(username.value, password.value); } }, [ + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "PasswordLoginView_error"}, vm.i18n(vm.errorMessage))), t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]), t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), t.div({ className: "button-row" }, [ From 55da58893b86f7771e494bf8480d0d32aa6fa654 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 21:41:42 +0530 Subject: [PATCH 071/108] Red + bold error Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/login.css | 4 ---- src/platform/web/ui/css/themes/element/theme.css | 5 +++++ src/platform/web/ui/login/CompleteSSOView.js | 2 +- src/platform/web/ui/login/LoginView.js | 2 +- src/platform/web/ui/login/PasswordLoginView.js | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css index 01b6bc6e..59a25799 100644 --- a/src/platform/web/ui/css/login.css +++ b/src/platform/web/ui/css/login.css @@ -96,7 +96,3 @@ limitations under the License. .LoginView_sso { padding: 0.4em 0.4em 0; } - -.LoginView_error { - padding: 0.4em -} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 08f1df06..4b7dab4d 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -161,6 +161,11 @@ a.button-action { background-image: url('icons/disable-grid.svg'); } +.error { + color: #f00; + font-weight: 600; +} + .FilterField { background-image: url('icons/search.svg'); background-repeat: no-repeat; diff --git a/src/platform/web/ui/login/CompleteSSOView.js b/src/platform/web/ui/login/CompleteSSOView.js index 8b0654c6..63614acf 100644 --- a/src/platform/web/ui/login/CompleteSSOView.js +++ b/src/platform/web/ui/login/CompleteSSOView.js @@ -22,7 +22,7 @@ export class CompleteSSOView extends TemplateView { return t.div({ className: "CompleteSSOView" }, [ t.p({ className: "CompleteSSOView_title" }, "Finishing up your SSO Login"), - t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "CompleteSSOView_error"}, vm.i18n(vm.errorMessage))), + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), ] ); diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 1dcbde3d..b44a67df 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -29,7 +29,7 @@ export class LoginView extends TemplateView { t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" }, [ - t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "LoginView_error"}, vm.i18n(vm.errorMessage))), + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), t.label({for: "homeserver"}, vm.i18n`Homeserver`), t.input({ id: "homeserver", diff --git a/src/platform/web/ui/login/PasswordLoginView.js b/src/platform/web/ui/login/PasswordLoginView.js index 7fd7d534..130f30ae 100644 --- a/src/platform/web/ui/login/PasswordLoginView.js +++ b/src/platform/web/ui/login/PasswordLoginView.js @@ -40,7 +40,7 @@ export class PasswordLoginView extends TemplateView { vm.login(username.value, password.value); } }, [ - t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "PasswordLoginView_error"}, vm.i18n(vm.errorMessage))), + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]), t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), t.div({ className: "button-row" }, [ From 82067ca6f57f568aaff9f450cbe5ca650cc17ba5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 21:43:42 +0530 Subject: [PATCH 072/108] No need to pass showError Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 381dc260..74a8d67d 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -60,7 +60,6 @@ export class LoginViewModel extends ViewModel { { sessionContainer: this._sessionContainer, attemptLogin: loginMethod => this.attemptLogin(loginMethod), - showError: message => this.showError(message), loginToken: this._loginToken }))); this.emitChange("completeSSOLoginViewModel"); @@ -77,11 +76,11 @@ export class LoginViewModel extends ViewModel { if (this._loginOptions.sso) { this._showSSOLogin(); } if (this._loginOptions.password) { this._showPasswordLogin(); } if (!this._loginOptions.sso && !this._loginOptions.password) { - this.showError("This homeserver neither supports SSO nor Password based login flows"); + this._showError("This homeserver neither supports SSO nor Password based login flows"); } } else { - this.showError("Could not query login methods supported by the homeserver"); + this._showError("Could not query login methods supported by the homeserver"); } } } @@ -92,7 +91,6 @@ export class LoginViewModel extends ViewModel { loginOptions: this._loginOptions, homeserver: this._homeserver, attemptLogin: loginMethod => this.attemptLogin(loginMethod), - showError: message => this.showError(message) }))); this.emitChange("passwordLoginViewModel"); } @@ -104,7 +102,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("startSSOLoginViewModel"); } - showError(message) { + _showError(message) { this._errorMessage = message; this.emitChange("errorMessage"); this._errorMessage = ""; From a5985cba2a15106ed6d7e3a9130a8f44ce606229 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 22:18:24 +0530 Subject: [PATCH 073/108] Add spinner Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 6 ++++++ src/platform/web/ui/css/login.css | 6 +++--- src/platform/web/ui/login/LoginView.js | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 74a8d67d..9a1b75df 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -39,6 +39,7 @@ export class LoginViewModel extends ViewModel { this._errorMessage = ""; this._hideHomeserver = false; this._isBusy = false; + this._isFetchingLoginOptions = false; this._createViewModels(this._homeserver); } @@ -51,6 +52,7 @@ export class LoginViewModel extends ViewModel { get cancelUrl() { return this.urlCreator.urlForSegment("session"); } get loadViewModel() {return this._loadViewModel; } get isBusy() { return this._isBusy; } + get isFetchingLoginOptions() { return this._isFetchingLoginOptions; } async _createViewModels(homeserver) { if (this._loginToken) { @@ -67,11 +69,15 @@ export class LoginViewModel extends ViewModel { else { this._errorMessage = ""; try { + this._isFetchingLoginOptions = true; + this.emitChange("isFetchingLoginOptions"); this._loginOptions = await this._sessionContainer.queryLogin(homeserver); } catch (e) { this._loginOptions = null; } + this._isFetchingLoginOptions = false; + this.emitChange("isFetchingLoginOptions"); if (this._loginOptions) { if (this._loginOptions.sso) { this._showSSOLogin(); } if (this._loginOptions.password) { this._showPasswordLogin(); } diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css index 59a25799..ca376dee 100644 --- a/src/platform/web/ui/css/login.css +++ b/src/platform/web/ui/css/login.css @@ -54,15 +54,15 @@ limitations under the License. padding: 0 0.4em 0.4em; } -.SessionLoadStatusView { +.SessionLoadStatusView, .LoginView_query-spinner { display: flex; } -.SessionLoadStatusView> :not(:first-child) { +.SessionLoadStatusView> :not(:first-child), .LoginView_query-spinner> :not(:first-child) { margin-left: 12px; } -.SessionLoadStatusView p { +.SessionLoadStatusView p, .LoginView_query-spinner p { flex: 1; margin: 0; } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index b44a67df..4b507567 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -19,6 +19,7 @@ import {hydrogenGithubLink} from "./common.js"; import {PasswordLoginView} from "./PasswordLoginView.js"; import {CompleteSSOView} from "./CompleteSSOView.js"; import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; +import {spinner} from "../common.js"; export class LoginView extends TemplateView { render(t, vm) { @@ -41,6 +42,7 @@ export class LoginView extends TemplateView { }) ] )), + t.if(vm => vm.isFetchingLoginOptions, t => t.div({className: "LoginView_query-spinner"}, [spinner(t), t.p("Fetching available login options...")])), t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null), t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)), t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null), From 0630452571c507b8215d8e059fcdb4013f809aa2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 22:53:15 +0530 Subject: [PATCH 074/108] No need to observe Signed-off-by: RMidhunSuresh --- src/domain/RootViewModel.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 4a0a969e..d6fdcfa0 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -40,10 +40,9 @@ export class RootViewModel extends ViewModel { } async _applyNavigation(shouldRestoreLastUrl) { - const isLogin = this.navigation.observe("login").get(); - const sessionId = this.navigation.observe("session").get(); - // TODO: why not observe? - const ssoSegment = this.navigation.path.get("sso"); + const isLogin = this.navigation.path.get("login") + const sessionId = this.navigation.path.get("session")?.value; + const loginToken = this.navigation.path.get("sso")?.value; if (isLogin) { if (this.activeSection !== "login") { this._showLogin(); @@ -68,10 +67,10 @@ export class RootViewModel extends ViewModel { this._showSessionLoader(sessionId); } } - } else if (ssoSegment) { + } else if (loginToken) { this.urlCreator.normalizeUrl(); if (this.activeSection !== "login") { - this._showLogin({loginToken: ssoSegment.value}); + this._showLogin(loginToken); } } else { @@ -120,7 +119,7 @@ export class RootViewModel extends ViewModel { this._pendingSessionContainer = sessionContainer; this.navigation.push("session", sessionContainer.sessionId); }, - ...loginToken + loginToken })); }); } From 94ba93acb5272cee7c4bb94e6c9653c4b2e5055a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 22:57:13 +0530 Subject: [PATCH 075/108] Add explaining comment Signed-off-by: RMidhunSuresh --- src/domain/SessionLoadViewModel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 956ba3a4..10cbb851 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -108,6 +108,7 @@ export class SessionLoadViewModel extends ViewModel { return `Something went wrong: ${error && error.message}.`; } + // Statuses related to login are handled by respective login view models if (sc) { switch (sc.loadStatus.get()) { case LoadStatus.SessionSetup: From 0e6139d5e3e453261d9ece6b4162a96281f1942f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 23:02:06 +0530 Subject: [PATCH 076/108] Use homeserver from login method Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 3 +-- src/domain/login/PasswordLoginViewModel.js | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 9a1b75df..129b5913 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -95,8 +95,7 @@ export class LoginViewModel extends ViewModel { this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( this.childOptions({ loginOptions: this._loginOptions, - homeserver: this._homeserver, - attemptLogin: loginMethod => this.attemptLogin(loginMethod), + attemptLogin: loginMethod => this.attemptLogin(loginMethod) }))); this.emitChange("passwordLoginViewModel"); } diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 3524779e..8cdef3c5 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -20,10 +20,9 @@ import {LoginFailure} from "../../matrix/SessionContainer.js"; export class PasswordLoginViewModel extends ViewModel { constructor(options) { super(options); - const {loginOptions, homeserver, attemptLogin} = options; + const {loginOptions, attemptLogin} = options; this._loginOptions = loginOptions; this._attemptLogin = attemptLogin; - this._homeserver = homeserver; this._isBusy = false; this._errorMessage = ""; } @@ -43,14 +42,15 @@ export class PasswordLoginViewModel extends ViewModel { } async login(username, password) { - const status = await this._attemptLogin(this._loginOptions.password(username, password)); + const loginMethod = this._loginOptions.password(username, password); + const status = await this._attemptLogin(loginMethod); let error = ""; switch (status) { case LoginFailure.Credentials: error = this.i18n`Your username and/or password don't seem to be correct.`; break; case LoginFailure.Connection: - error = this.i18n`Can't connect to ${this._homeserver}.`; + error = this.i18n`Can't connect to ${loginMethod.homeServer}.`; break; case LoginFailure.Unknown: error = this.i18n`Something went wrong while checking your login and password.`; From d47e126370683cda318cabc49d209d6a2abbb842 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 23:03:51 +0530 Subject: [PATCH 077/108] add missing emit Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 129b5913..b080c76e 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -134,6 +134,7 @@ export class LoginViewModel extends ViewModel { return this._sessionContainer.loginFailure; } this._hideHomeserver = true; + this.emitChange("hideHomeserver"); this._disposeViewModels(); this._createLoadViewModel(); return null; From 04806a1425e67847d38402a7e36610467ef612a6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 20 Aug 2021 23:16:03 +0530 Subject: [PATCH 078/108] Convert link to button Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 6 +++++- src/platform/web/ui/login/LoginView.js | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index b080c76e..27e45d1f 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -49,11 +49,15 @@ export class LoginViewModel extends ViewModel { get homeserver() { return this._homeserver; } get errorMessage() { return this._errorMessage; } get showHomeserver() { return !this._hideHomeserver; } - get cancelUrl() { return this.urlCreator.urlForSegment("session"); } get loadViewModel() {return this._loadViewModel; } get isBusy() { return this._isBusy; } get isFetchingLoginOptions() { return this._isFetchingLoginOptions; } + goBack() { + const path = this.navigation.pathFrom([this.navigation.segment("session")]); + this.navigation.applyPath(path); + } + async _createViewModels(homeserver) { if (this._loginToken) { this._hideHomeserver = true; diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 4b507567..3d8e8470 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -23,8 +23,14 @@ import {spinner} from "../common.js"; export class LoginView extends TemplateView { render(t, vm) { + const disabled = vm => vm.isBusy; + return t.div({className: "PreSessionScreen"}, [ - t.a({className: "button-utility LoginView_back", href: vm.cancelUrl}), + t.button({ + className: "button-utility LoginView_back", + onClick: () => vm.goBack(), + disabled + }), t.div({className: "logo"}), t.h1([vm.i18n`Sign In`]), t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), @@ -37,7 +43,7 @@ export class LoginView extends TemplateView { type: "text", placeholder: vm.i18n`Your matrix homeserver`, value: vm.homeserver, - disabled: vm => vm.isBusy, + disabled, onChange: event => vm.updateHomeServer(event.target.value), }) ] From 84fd2861402c5dbfe3caf0725ba8175ac9dbd64c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 15:06:16 +0530 Subject: [PATCH 079/108] Split logintoken into two words Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index 9bdb0509..ded09cd9 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -55,13 +55,13 @@ export class CompleteSSOLoginViewModel extends ViewModel { let error = ""; switch (status) { case LoginFailure.Credentials: - error = this.i18n`Your logintoken is invalid.`; + error = this.i18n`Your login token is invalid.`; break; case LoginFailure.Connection: error = this.i18n`Can't connect to ${homeserver}.`; break; case LoginFailure.Unknown: - error = this.i18n`Something went wrong while checking your logintoken.`; + error = this.i18n`Something went wrong while checking your login token.`; break; } if (error) { From c9fbafb909fd64b81cbe2f4a26227c18d20376f1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 15:12:40 +0530 Subject: [PATCH 080/108] Also check LoadStatus.Error Signed-off-by: RMidhunSuresh --- src/matrix/SessionContainer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index ca8379bc..27776d37 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -130,7 +130,9 @@ export class SessionContainer { async startWithLogin(loginMethod) { const currentStatus = this._status.get(); - if (currentStatus !== LoadStatus.LoginFailed && currentStatus !== LoadStatus.NotLoading) { + if (currentStatus !== LoadStatus.LoginFailed && + currentStatus !== LoadStatus.NotLoading && + currentStatus !== LoadStatus.Error) { return; } this._resetStatus(); From ecfdc314d5a8b9b0c4bee1c1538176c32414ad3b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 15:28:44 +0530 Subject: [PATCH 081/108] Do not set error message to empty string Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 1 - src/domain/login/LoginViewModel.js | 3 ++- src/domain/login/PasswordLoginViewModel.js | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index ded09cd9..c06b3ffd 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -37,7 +37,6 @@ export class CompleteSSOLoginViewModel extends ViewModel { _showError(message) { this._errorMessage = message; this.emitChange("errorMessage"); - this._errorMessage = ""; } async performSSOLoginCompletion() { diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 27e45d1f..13087581 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -114,7 +114,6 @@ export class LoginViewModel extends ViewModel { _showError(message) { this._errorMessage = message; this.emitChange("errorMessage"); - this._errorMessage = ""; } _toggleBusy(status) { @@ -180,6 +179,8 @@ export class LoginViewModel extends ViewModel { } updateHomeServer(newHomeserver) { + this._errorMessage = ""; + this.emitChange("errorMessage"); this._homeserver = newHomeserver; this._disposeViewModels(); this._createViewModels(newHomeserver); diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 8cdef3c5..0d534b90 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -38,10 +38,11 @@ export class PasswordLoginViewModel extends ViewModel { _showError(message) { this._errorMessage = message; this.emitChange("errorMessage"); - this._errorMessage = ""; } async login(username, password) { + this._errorMessage = ""; + this.emitChange("errorMessage"); const loginMethod = this._loginOptions.password(username, password); const status = await this._attemptLogin(loginMethod); let error = ""; From 69478b81b2c45689d7e16d794801d32ddace6e64 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 15:48:29 +0530 Subject: [PATCH 082/108] Fix toggleBusy Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 14 ++++++-------- src/domain/login/PasswordLoginViewModel.js | 4 ++-- src/domain/login/StartSSOLoginViewModel.js | 4 ++-- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 13087581..97534a56 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -116,22 +116,20 @@ export class LoginViewModel extends ViewModel { this.emitChange("errorMessage"); } - _toggleBusy(status) { - this._isBusy = status; + _toggleBusy() { + this._isBusy = !this._isBusy; + this._passwordLoginViewModel?.toggleBusy(); + this._startSSOLoginViewModel?.toggleBusy(); this.emitChange("isBusy"); } async attemptLogin(loginMethod) { - this._toggleBusy(true); - this._passwordLoginViewModel?.toggleBusy(true); - this._startSSOLoginViewModel?.toggleBusy(true); + this._toggleBusy(); this._sessionContainer.startWithLogin(loginMethod); const loadStatus = this._sessionContainer.loadStatus; const handle = loadStatus.waitFor(status => status !== LoadStatus.Login); await handle.promise; - this._toggleBusy(false); - this._passwordLoginViewModel?.toggleBusy(false); - this._startSSOLoginViewModel?.toggleBusy(false); + this._toggleBusy(); const status = loadStatus.get(); if (status === LoadStatus.LoginFailed) { return this._sessionContainer.loginFailure; diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 0d534b90..7ba75c18 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -30,8 +30,8 @@ export class PasswordLoginViewModel extends ViewModel { get isBusy() { return this._isBusy; } get errorMessage() { return this._errorMessage; } - toggleBusy(state) { - this._isBusy = state; + toggleBusy() { + this._isBusy = !this._isBusy; this.emitChange("isBusy"); } diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js index e4c0cf92..bd236788 100644 --- a/src/domain/login/StartSSOLoginViewModel.js +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -25,8 +25,8 @@ export class StartSSOLoginViewModel extends ViewModel{ get isBusy() { return this._isBusy; } - toggleBusy(state) { - this._isBusy = state; + toggleBusy() { + this._isBusy = !this._isBusy; this.emitChange("isBusy"); } From e80667c93547eaa944991448ca4e232c21214c64 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 15:50:22 +0530 Subject: [PATCH 083/108] Remove duplicate style Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/themes/element/theme.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 4b7dab4d..08f1df06 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -161,11 +161,6 @@ a.button-action { background-image: url('icons/disable-grid.svg'); } -.error { - color: #f00; - font-weight: 600; -} - .FilterField { background-image: url('icons/search.svg'); background-repeat: no-repeat; From b0db7e03441edf851f977329ebc3a1be6e3c4f1b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 15:55:07 +0530 Subject: [PATCH 084/108] More simpler navigation Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 97534a56..832d2916 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -54,8 +54,7 @@ export class LoginViewModel extends ViewModel { get isFetchingLoginOptions() { return this._isFetchingLoginOptions; } goBack() { - const path = this.navigation.pathFrom([this.navigation.segment("session")]); - this.navigation.applyPath(path); + this.navigation.push("session"); } async _createViewModels(homeserver) { From fc169af10f0b50f6de02892eb87140c7d889de2a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 16:09:40 +0530 Subject: [PATCH 085/108] Rename toggle to set Signed-off-by: RMidhunSuresh --- src/domain/login/LoginViewModel.js | 14 +++++++------- src/domain/login/PasswordLoginViewModel.js | 4 ++-- src/domain/login/StartSSOLoginViewModel.js | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 832d2916..85dddb79 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -115,20 +115,20 @@ export class LoginViewModel extends ViewModel { this.emitChange("errorMessage"); } - _toggleBusy() { - this._isBusy = !this._isBusy; - this._passwordLoginViewModel?.toggleBusy(); - this._startSSOLoginViewModel?.toggleBusy(); + _setBusy(status) { + this._isBusy = status; + this._passwordLoginViewModel?.setBusy(status); + this._startSSOLoginViewModel?.setBusy(status); this.emitChange("isBusy"); } async attemptLogin(loginMethod) { - this._toggleBusy(); + this._setBusy(true); this._sessionContainer.startWithLogin(loginMethod); const loadStatus = this._sessionContainer.loadStatus; const handle = loadStatus.waitFor(status => status !== LoadStatus.Login); await handle.promise; - this._toggleBusy(); + this._setBusy(false); const status = loadStatus.get(); if (status === LoadStatus.LoginFailed) { return this._sessionContainer.loginFailure; @@ -163,7 +163,7 @@ export class LoginViewModel extends ViewModel { if (!this._loadViewModel.loading) { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); } - this._toggleBusy(false); + this._setBusy(false); }) ); } diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 7ba75c18..f8d354a4 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -30,8 +30,8 @@ export class PasswordLoginViewModel extends ViewModel { get isBusy() { return this._isBusy; } get errorMessage() { return this._errorMessage; } - toggleBusy() { - this._isBusy = !this._isBusy; + setBusy(status) { + this._isBusy = status; this.emitChange("isBusy"); } diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js index bd236788..54218d22 100644 --- a/src/domain/login/StartSSOLoginViewModel.js +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -25,8 +25,8 @@ export class StartSSOLoginViewModel extends ViewModel{ get isBusy() { return this._isBusy; } - toggleBusy() { - this._isBusy = !this._isBusy; + setBusy(status) { + this._isBusy = status; this.emitChange("isBusy"); } From c9319c7c3880abae669ffd6e657d2ea9a09891db Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 16:58:54 +0530 Subject: [PATCH 086/108] Catch any error from queryLogin Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index c06b3ffd..c1f6223c 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -44,7 +44,14 @@ export class CompleteSSOLoginViewModel extends ViewModel { return; } const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver"); - const loginOptions = await this._sessionContainer.queryLogin(homeserver); + let loginOptions; + try { + loginOptions = await this._sessionContainer.queryLogin(homeserver); + } + catch (err) { + this._showError(err.message); + return; + } if (!loginOptions.token) { const path = this.navigation.pathFrom([this.navigation.segment("session")]); this.navigation.applyPath(path); From ef4db4ababb9db97c7d06ebbf3f10cb835a238e1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 23 Aug 2021 17:00:41 +0530 Subject: [PATCH 087/108] Make navigation simpler Signed-off-by: RMidhunSuresh --- src/domain/login/CompleteSSOLoginViewModel.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index c1f6223c..b7e04cc8 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -53,8 +53,7 @@ export class CompleteSSOLoginViewModel extends ViewModel { return; } if (!loginOptions.token) { - const path = this.navigation.pathFrom([this.navigation.segment("session")]); - this.navigation.applyPath(path); + this.navigation.push("session"); return; } const status = await this._attemptLogin(loginOptions.token(this._loginToken)); From 577c3168e69712f0ed1ef2b0bf795a70663ef5be Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 15:54:06 +0200 Subject: [PATCH 088/108] make queryLogin abortable --- src/matrix/SessionContainer.js | 10 ++++++--- src/utils/AbortableOperation.ts | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/utils/AbortableOperation.ts diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 27776d37..ab20a4eb 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -15,6 +15,7 @@ limitations under the License. */ import {createEnum} from "../utils/enum.js"; +import {AbortableOperation} from "../utils/AbortableOperation"; import {ObservableValue} from "../observable/ObservableValue.js"; import {HomeServerApi} from "./net/HomeServerApi.js"; import {Reconnector, ConnectionStatus} from "./net/Reconnector.js"; @@ -53,6 +54,7 @@ export const LoginFailure = createEnum( "Unknown", ); + export class SessionContainer { constructor({platform, olmPromise, workerPromise}) { this._platform = platform; @@ -121,11 +123,13 @@ export class SessionContainer { return result; } - async queryLogin(homeServer) { + queryLogin(homeServer) { const normalizedHS = normalizeHomeserver(homeServer); const hsApi = new HomeServerApi({homeServer: normalizedHS, request: this._platform.request}); - const response = await hsApi.getLoginFlows().response(); - return this._parseLoginOptions(response, normalizedHS); + return new AbortableOperation(async setAbortable => { + const response = await setAbortable(hsApi.getLoginFlows()).response(); + return this._parseLoginOptions(response, normalizedHS); + }); } async startWithLogin(loginMethod) { diff --git a/src/utils/AbortableOperation.ts b/src/utils/AbortableOperation.ts new file mode 100644 index 00000000..0cc49e10 --- /dev/null +++ b/src/utils/AbortableOperation.ts @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +interface IAbortable { + abort(); +} + +type RunFn = (setAbortable: (a: IAbortable) => typeof a) => T; + +export class AbortableOperation { + public readonly result: T; + private _abortable: IAbortable | null; + + constructor(run: RunFn) { + this._abortable = null; + const setAbortable = abortable => { + this._abortable = abortable; + return abortable; + }; + this.result = run(setAbortable); + } + + abort() { + this._abortable?.abort(); + this._abortable = null; + } +} From 8eab9ab28b33884da9ba0d18a3b18e848056e55c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 15:54:40 +0200 Subject: [PATCH 089/108] add 2s timeout on input of homeserver to also query the homeserver, in addition to change event --- src/domain/login/LoginViewModel.js | 84 +++++++++++++++++--------- src/platform/web/ui/login/LoginView.js | 7 ++- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 85dddb79..8827a511 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -39,8 +39,9 @@ export class LoginViewModel extends ViewModel { this._errorMessage = ""; this._hideHomeserver = false; this._isBusy = false; - this._isFetchingLoginOptions = false; - this._createViewModels(this._homeserver); + this._abortHomeserverQueryTimeout = null; + this._abortQueryOperation = null; + this._initViewModels(); } get passwordLoginViewModel() { return this._passwordLoginViewModel; } @@ -51,13 +52,13 @@ export class LoginViewModel extends ViewModel { get showHomeserver() { return !this._hideHomeserver; } get loadViewModel() {return this._loadViewModel; } get isBusy() { return this._isBusy; } - get isFetchingLoginOptions() { return this._isFetchingLoginOptions; } + get isFetchingLoginOptions() { return !!this._abortQueryOperation; } goBack() { this.navigation.push("session"); } - async _createViewModels(homeserver) { + async _initViewModels() { if (this._loginToken) { this._hideHomeserver = true; this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel( @@ -70,27 +71,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("completeSSOLoginViewModel"); } else { - this._errorMessage = ""; - try { - this._isFetchingLoginOptions = true; - this.emitChange("isFetchingLoginOptions"); - this._loginOptions = await this._sessionContainer.queryLogin(homeserver); - } - catch (e) { - this._loginOptions = null; - } - this._isFetchingLoginOptions = false; - this.emitChange("isFetchingLoginOptions"); - if (this._loginOptions) { - if (this._loginOptions.sso) { this._showSSOLogin(); } - if (this._loginOptions.password) { this._showPasswordLogin(); } - if (!this._loginOptions.sso && !this._loginOptions.password) { - this._showError("This homeserver neither supports SSO nor Password based login flows"); - } - } - else { - this._showError("Could not query login methods supported by the homeserver"); - } + await this.queryHomeServer(); } } @@ -175,12 +156,59 @@ export class LoginViewModel extends ViewModel { this.emitChange("disposeViewModels"); } - updateHomeServer(newHomeserver) { + async setHomeServerInput(newHomeserver) { + this._homeserver = newHomeserver; + // abort ongoing query, if any + this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); + this.emitChange("isFetchingLoginOptions"); + this.disposeTracked(this._abortHomeserverQueryTimeout); + const timeout = this.clock.createTimeout(2000); + this._abortHomeserverQueryTimeout = this.track(() => timeout.abort()); + try { + await timeout.elapsed(); + } catch (err) { + if (err.name === "AbortError") { + return; // still typing, don't query + } else { + throw err; + } + } + this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); + this.queryHomeServer(); + } + + async queryHomeServer() { this._errorMessage = ""; this.emitChange("errorMessage"); - this._homeserver = newHomeserver; + this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); this._disposeViewModels(); - this._createViewModels(newHomeserver); + try { + const queryOperation = this._sessionContainer.queryLogin(this._homeserver); + this._abortQueryOperation = this.track(() => queryOperation.abort()); + this.emitChange("isFetchingLoginOptions"); + this._loginOptions = await queryOperation.result; + } + catch (e) { + console.log("error", e); + if (e.name === "AbortError") { + return; //aborted, bail out + } else { + this._loginOptions = null; + } + } finally { + this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); + this.emitChange("isFetchingLoginOptions"); + } + if (this._loginOptions) { + if (this._loginOptions.sso) { this._showSSOLogin(); } + if (this._loginOptions.password) { this._showPasswordLogin(); } + if (!this._loginOptions.sso && !this._loginOptions.password) { + this._showError("This homeserver supports neither SSO nor password based login flows"); + } + } + else { + this._showError("Could not query login methods supported by the homeserver"); + } } dispose() { diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 3d8e8470..df92cb67 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -36,7 +36,6 @@ export class LoginView extends TemplateView { t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" }, [ - t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), t.label({for: "homeserver"}, vm.i18n`Homeserver`), t.input({ id: "homeserver", @@ -44,8 +43,10 @@ export class LoginView extends TemplateView { placeholder: vm.i18n`Your matrix homeserver`, value: vm.homeserver, disabled, - onChange: event => vm.updateHomeServer(event.target.value), - }) + onInput: () => vm.setHomeServerInput(event.target.value), + onChange: event => vm.queryHomeServer(), + }), + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] )), t.if(vm => vm.isFetchingLoginOptions, t => t.div({className: "LoginView_query-spinner"}, [spinner(t), t.p("Fetching available login options...")])), From d1301fa642ebb2d7a9b8dcccd20cfd017cf09bcb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 15:57:16 +0200 Subject: [PATCH 090/108] input is not needed in the name here --- src/domain/login/LoginViewModel.js | 2 +- src/platform/web/ui/login/LoginView.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 8827a511..27dd47c3 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -156,7 +156,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("disposeViewModels"); } - async setHomeServerInput(newHomeserver) { + async setHomeServer(newHomeserver) { this._homeserver = newHomeserver; // abort ongoing query, if any this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index df92cb67..498cb574 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -43,7 +43,7 @@ export class LoginView extends TemplateView { placeholder: vm.i18n`Your matrix homeserver`, value: vm.homeserver, disabled, - onInput: () => vm.setHomeServerInput(event.target.value), + onInput: () => vm.setHomeServer(event.target.value), onChange: event => vm.queryHomeServer(), }), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), From 993bc36096543b5770b5305bc5c2f57a0621c0cb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 16:01:39 +0200 Subject: [PATCH 091/108] dont query 2nd time after losing focus --- src/domain/login/LoginViewModel.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 27dd47c3..d065b139 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -180,6 +180,9 @@ export class LoginViewModel extends ViewModel { async queryHomeServer() { this._errorMessage = ""; this.emitChange("errorMessage"); + // if query is called before the typing timeout hits (e.g. field lost focus), cancel the timeout so we don't query again. + this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); + // cancel ongoing query operation, if any this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); this._disposeViewModels(); try { From 9760a4540e7894961e4094e82f9523dd44d4dc9a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 16:04:00 +0200 Subject: [PATCH 092/108] remove debug log --- src/domain/login/LoginViewModel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index d065b139..cd75ca87 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -192,7 +192,6 @@ export class LoginViewModel extends ViewModel { this._loginOptions = await queryOperation.result; } catch (e) { - console.log("error", e); if (e.name === "AbortError") { return; //aborted, bail out } else { From 3a5b7c1d0ed4939e9e6b23b621da7c83a3140f6d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 18:47:36 +0200 Subject: [PATCH 093/108] support well-known lookup --- src/matrix/SessionContainer.js | 22 +++++++++--- src/matrix/well-known.js | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/matrix/well-known.js diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index ab20a4eb..7233eeb0 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,6 +16,7 @@ limitations under the License. */ import {createEnum} from "../utils/enum.js"; +import {lookupHomeServer} from "./well-known.js"; import {AbortableOperation} from "../utils/AbortableOperation"; import {ObservableValue} from "../observable/ObservableValue.js"; import {HomeServerApi} from "./net/HomeServerApi.js"; @@ -36,6 +38,16 @@ function normalizeHomeserver(homeServer) { } } +function getRetryHomeServer(homeServer) { + const url = new URL(homeServer); + const {host} = url; + const dotCount = host.split(".").length - 1; + if (dotCount === 1) { + url.host = `www.${host}`; + return url.origin; + } +} + export const LoadStatus = createEnum( "NotLoading", "Login", @@ -108,7 +120,7 @@ export class SessionContainer { implements LoginMethod */ const flows = options.flows; - const result = {}; + const result = {homeServer}; for (const flow of flows) { if (flow.type === "m.login.password") { result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password}); @@ -124,11 +136,13 @@ export class SessionContainer { } queryLogin(homeServer) { - const normalizedHS = normalizeHomeserver(homeServer); - const hsApi = new HomeServerApi({homeServer: normalizedHS, request: this._platform.request}); return new AbortableOperation(async setAbortable => { + homeServer = await lookupHomeServer(homeServer, (url, options) => { + return setAbortable(this._platform.request(url, options)); + }); + const hsApi = new HomeServerApi({homeServer, request: this._platform.request}); const response = await setAbortable(hsApi.getLoginFlows()).response(); - return this._parseLoginOptions(response, normalizedHS); + return this._parseLoginOptions(response, homeServer); }); } diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js new file mode 100644 index 00000000..a33a734a --- /dev/null +++ b/src/matrix/well-known.js @@ -0,0 +1,64 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +function normalizeHomeserver(homeServer) { + try { + return new URL(homeServer).origin; + } catch (err) { + return new URL(`https://${homeServer}`).origin; + } +} + +function getRetryHomeServer(homeServer) { + const url = new URL(homeServer); + const {host} = url; + const dotCount = host.split(".").length - 1; + if (dotCount === 1) { + url.host = `www.${host}`; + return url.origin; + } +} + +export async function lookupHomeServer(homeServer, request) { + homeServer = normalizeHomeserver(homeServer); + const requestOptions = {format: "json", timeout: 30000, method: "GET"}; + let wellKnownResponse = null; + while (!wellKnownResponse) { + try { + const wellKnownUrl = `${homeServer}/.well-known/matrix/client`; + wellKnownResponse = await request(wellKnownUrl, requestOptions).response(); + } catch (err) { + if (err.name === "ConnectionError") { + const retryHS = getRetryHomeServer(homeServer); + if (retryHS) { + homeServer = retryHS; + } else { + throw err; + } + } else { + throw err; + } + } + } + if (wellKnownResponse.status === 200) { + const {body} = wellKnownResponse; + const wellKnownHomeServer = body["m.homeserver"]?.["base_url"]; + if (typeof wellKnownHomeServer === "string") { + homeServer = normalizeHomeserver(wellKnownHomeServer); + } + } + return homeServer +} From c7b47bb8d66d23568fef2390983ef199763e621d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:26:39 +0200 Subject: [PATCH 094/108] fix homeServer misspelling to homeserver across the project --- src/domain/RootViewModel.js | 2 +- src/domain/login/LoginViewModel.js | 12 ++-- src/domain/login/PasswordLoginViewModel.js | 5 +- .../session/settings/SettingsViewModel.js | 2 +- src/matrix/Session.js | 4 +- src/matrix/SessionContainer.js | 50 ++++++----------- src/matrix/login/LoginMethod.js | 4 +- src/matrix/net/HomeServerApi.js | 6 +- src/matrix/net/MediaRepository.js | 8 +-- src/matrix/well-known.js | 56 +++++++++---------- src/platform/web/ui/login/LoginView.js | 4 +- src/platform/web/ui/view-gallery.html | 4 +- 12 files changed, 69 insertions(+), 88 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index d6fdcfa0..d9949c77 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -105,7 +105,7 @@ export class RootViewModel extends ViewModel { _showLogin(loginToken) { this._setSection(() => { this._loginViewModel = new LoginViewModel(this.childOptions({ - defaultHomeServer: this.platform.config["defaultHomeServer"], + defaultHomeserver: this.platform.config["defaultHomeServer"], createSessionContainer: this._createSessionContainer, ready: sessionContainer => { // we don't want to load the session container again, diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index cd75ca87..d0cf9664 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -24,7 +24,7 @@ import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; export class LoginViewModel extends ViewModel { constructor(options) { super(options); - const {ready, defaultHomeServer, createSessionContainer, loginToken} = options; + const {ready, defaultHomeserver, createSessionContainer, loginToken} = options; this._createSessionContainer = createSessionContainer; this._ready = ready; this._loginToken = loginToken; @@ -35,7 +35,7 @@ export class LoginViewModel extends ViewModel { this._completeSSOLoginViewModel = null; this._loadViewModel = null; this._loadViewModelSubscription = null; - this._homeserver = defaultHomeServer; + this._homeserver = defaultHomeserver; this._errorMessage = ""; this._hideHomeserver = false; this._isBusy = false; @@ -71,7 +71,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("completeSSOLoginViewModel"); } else { - await this.queryHomeServer(); + await this.queryHomeserver(); } } @@ -156,7 +156,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("disposeViewModels"); } - async setHomeServer(newHomeserver) { + async setHomeserver(newHomeserver) { this._homeserver = newHomeserver; // abort ongoing query, if any this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); @@ -174,13 +174,13 @@ export class LoginViewModel extends ViewModel { } } this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); - this.queryHomeServer(); + this.queryHomeserver(); } - async queryHomeServer() { this._errorMessage = ""; this.emitChange("errorMessage"); // if query is called before the typing timeout hits (e.g. field lost focus), cancel the timeout so we don't query again. + async queryHomeserver() { this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); // cancel ongoing query operation, if any this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index f8d354a4..7ea239e6 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -43,15 +43,14 @@ export class PasswordLoginViewModel extends ViewModel { async login(username, password) { this._errorMessage = ""; this.emitChange("errorMessage"); - const loginMethod = this._loginOptions.password(username, password); - const status = await this._attemptLogin(loginMethod); + const status = await this._attemptLogin(this._loginOptions.password(username, password)); let error = ""; switch (status) { case LoginFailure.Credentials: error = this.i18n`Your username and/or password don't seem to be correct.`; break; case LoginFailure.Connection: - error = this.i18n`Can't connect to ${loginMethod.homeServer}.`; + error = this.i18n`Can't connect to ${this._loginOptions.homeserver}.`; break; case LoginFailure.Unknown: error = this.i18n`Something went wrong while checking your login and password.`; diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 221e39da..7120f5fb 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -151,7 +151,7 @@ export class SettingsViewModel extends ViewModel { this.pushNotifications.enabledOnServer = null; this.pushNotifications.serverError = null; try { - this.pushNotifications.enabledOnServer = await this._session.checkPusherEnabledOnHomeServer(); + this.pushNotifications.enabledOnServer = await this._session.checkPusherEnabledOnHomeserver(); this.emitChange("pushNotifications.enabledOnServer"); } catch (err) { this.pushNotifications.serverError = err; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 5a046117..63aece42 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -46,7 +46,7 @@ const PICKLE_KEY = "DEFAULT_KEY"; const PUSHER_KEY = "pusher"; export class Session { - // sessionInfo contains deviceId, userId and homeServer + // sessionInfo contains deviceId, userId and homeserver constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) { this._platform = platform; this._storage = storage; @@ -636,7 +636,7 @@ export class Session { return !!pusherData; } - async checkPusherEnabledOnHomeServer() { + async checkPusherEnabledOnHomeserver() { const readTxn = await this._storage.readTxn([this._storage.storeNames.session]); const pusherData = await readTxn.session.get(PUSHER_KEY); if (!pusherData) { diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 7233eeb0..f375fdd7 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -16,7 +16,7 @@ limitations under the License. */ import {createEnum} from "../utils/enum.js"; -import {lookupHomeServer} from "./well-known.js"; +import {lookupHomeserver} from "./well-known.js"; import {AbortableOperation} from "../utils/AbortableOperation"; import {ObservableValue} from "../observable/ObservableValue.js"; import {HomeServerApi} from "./net/HomeServerApi.js"; @@ -30,24 +30,6 @@ import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js"; import {TokenLoginMethod} from "./login/TokenLoginMethod.js"; import {SSOLoginHelper} from "./login/SSOLoginHelper.js"; -function normalizeHomeserver(homeServer) { - try { - return new URL(homeServer).origin; - } catch (err) { - return new URL(`https://${homeServer}`).origin; - } -} - -function getRetryHomeServer(homeServer) { - const url = new URL(homeServer); - const {host} = url; - const dotCount = host.split(".").length - 1; - if (dotCount === 1) { - url.host = `www.${host}`; - return url.origin; - } -} - export const LoadStatus = createEnum( "NotLoading", "Login", @@ -66,7 +48,6 @@ export const LoginFailure = createEnum( "Unknown", ); - export class SessionContainer { constructor({platform, olmPromise, workerPromise}) { this._platform = platform; @@ -114,35 +95,35 @@ export class SessionContainer { }); } - _parseLoginOptions(options, homeServer) { + _parseLoginOptions(options, homeserver) { /* Take server response and return new object which has two props password and sso which implements LoginMethod */ const flows = options.flows; - const result = {homeServer}; + const result = {homeserver}; for (const flow of flows) { if (flow.type === "m.login.password") { - result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password}); + result.password = (username, password) => new PasswordLoginMethod({homeserver, username, password}); } else if (flow.type === "m.login.sso" && flows.find(flow => flow.type === "m.login.token")) { - result.sso = new SSOLoginHelper(homeServer); + result.sso = new SSOLoginHelper(homeserver); } else if (flow.type === "m.login.token") { - result.token = loginToken => new TokenLoginMethod({homeServer, loginToken}); + result.token = loginToken => new TokenLoginMethod({homeserver, loginToken}); } } return result; } - queryLogin(homeServer) { + queryLogin(homeserver) { return new AbortableOperation(async setAbortable => { - homeServer = await lookupHomeServer(homeServer, (url, options) => { + homeserver = await lookupHomeserver(homeserver, (url, options) => { return setAbortable(this._platform.request(url, options)); }); - const hsApi = new HomeServerApi({homeServer, request: this._platform.request}); + const hsApi = new HomeServerApi({homeserver, request: this._platform.request}); const response = await setAbortable(hsApi.getLoginFlows()).response(); - return this._parseLoginOptions(response, homeServer); + return this._parseLoginOptions(response, homeserver); }); } @@ -160,14 +141,15 @@ export class SessionContainer { let sessionInfo; try { const request = this._platform.request; - const hsApi = new HomeServerApi({homeServer: loginMethod.homeServer, request}); + const hsApi = new HomeServerApi({homeserver: loginMethod.homeserver, request}); const loginData = await loginMethod.login(hsApi, "Hydrogen", log); const sessionId = this.createNewSessionId(); sessionInfo = { id: sessionId, deviceId: loginData.device_id, userId: loginData.user_id, - homeServer: loginMethod.homeServer, + homeServer: loginMethod.homeserver, // deprecate this over time + homeserver: loginMethod.homeserver, accessToken: loginData.access_token, lastUsed: clock.now() }; @@ -216,7 +198,7 @@ export class SessionContainer { createMeasure: clock.createMeasure }); const hsApi = new HomeServerApi({ - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, accessToken: sessionInfo.accessToken, request: this._platform.request, reconnector: this._reconnector, @@ -228,7 +210,7 @@ export class SessionContainer { id: sessionInfo.id, deviceId: sessionInfo.deviceId, userId: sessionInfo.userId, - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, }; const olm = await this._olmPromise; let olmWorker = null; @@ -238,7 +220,7 @@ export class SessionContainer { this._requestScheduler = new RequestScheduler({hsApi, clock}); this._requestScheduler.start(); const mediaRepository = new MediaRepository({ - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, platform: this._platform, }); this._session = new Session({ diff --git a/src/matrix/login/LoginMethod.js b/src/matrix/login/LoginMethod.js index fee8e845..ece18871 100644 --- a/src/matrix/login/LoginMethod.js +++ b/src/matrix/login/LoginMethod.js @@ -15,8 +15,8 @@ limitations under the License. */ export class LoginMethod { - constructor({homeServer}) { - this.homeServer = homeServer; + constructor({homeserver}) { + this.homeserver = homeserver; } // eslint-disable-next-line no-unused-vars diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 8641e374..4b53b28b 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -19,10 +19,10 @@ import {encodeQueryParams, encodeBody} from "./common.js"; import {HomeServerRequest} from "./HomeServerRequest.js"; export class HomeServerApi { - constructor({homeServer, accessToken, request, reconnector}) { + constructor({homeserver, accessToken, request, reconnector}) { // store these both in a closure somehow so it's harder to get at in case of XSS? // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write - this._homeserver = homeServer; + this._homeserver = homeserver; this._accessToken = accessToken; this._requestFn = request; this._reconnector = reconnector; @@ -234,7 +234,7 @@ export function tests() { "superficial happy path for GET": async assert => { const hsApi = new HomeServerApi({ request: () => new MockRequest().respond(200, 42), - homeServer: "https://hs.tld" + homeserver: "https://hs.tld" }); const result = await hsApi._get("foo", null, null, null).response(); assert.strictEqual(result, 42); diff --git a/src/matrix/net/MediaRepository.js b/src/matrix/net/MediaRepository.js index f7e47cfd..3f718c85 100644 --- a/src/matrix/net/MediaRepository.js +++ b/src/matrix/net/MediaRepository.js @@ -18,8 +18,8 @@ import {encodeQueryParams} from "./common.js"; import {decryptAttachment} from "../e2ee/attachment.js"; export class MediaRepository { - constructor({homeServer, platform}) { - this._homeServer = homeServer; + constructor({homeserver, platform}) { + this._homeserver = homeserver; this._platform = platform; } @@ -27,7 +27,7 @@ export class MediaRepository { const parts = this._parseMxcUrl(url); if (parts) { const [serverName, mediaId] = parts; - const httpUrl = `${this._homeServer}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; + const httpUrl = `${this._homeserver}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; return httpUrl + "?" + encodeQueryParams({width: Math.round(width), height: Math.round(height), method}); } return null; @@ -37,7 +37,7 @@ export class MediaRepository { const parts = this._parseMxcUrl(url); if (parts) { const [serverName, mediaId] = parts; - return `${this._homeServer}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; + return `${this._homeserver}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; } else { return null; } diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js index a33a734a..1217c43e 100644 --- a/src/matrix/well-known.js +++ b/src/matrix/well-known.js @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -function normalizeHomeserver(homeServer) { +function normalizeHomeserver(homeserver) { try { - return new URL(homeServer).origin; + return new URL(homeserver).origin; } catch (err) { - return new URL(`https://${homeServer}`).origin; + return new URL(`https://${homeserver}`).origin; } } -function getRetryHomeServer(homeServer) { - const url = new URL(homeServer); +function getRetryHomeserver(homeserver) { + const url = new URL(homeserver); const {host} = url; const dotCount = host.split(".").length - 1; if (dotCount === 1) { @@ -32,33 +32,33 @@ function getRetryHomeServer(homeServer) { } } -export async function lookupHomeServer(homeServer, request) { - homeServer = normalizeHomeserver(homeServer); - const requestOptions = {format: "json", timeout: 30000, method: "GET"}; - let wellKnownResponse = null; - while (!wellKnownResponse) { - try { - const wellKnownUrl = `${homeServer}/.well-known/matrix/client`; - wellKnownResponse = await request(wellKnownUrl, requestOptions).response(); - } catch (err) { - if (err.name === "ConnectionError") { - const retryHS = getRetryHomeServer(homeServer); - if (retryHS) { - homeServer = retryHS; - } else { - throw err; - } +export async function lookupHomeserver(homeserver, request) { + homeserver = normalizeHomeserver(homeserver); + const requestOptions = {format: "json", timeout: 30000, method: "GET"}; + let wellKnownResponse = null; + while (!wellKnownResponse) { + try { + const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; + wellKnownResponse = await request(wellKnownUrl, requestOptions).response(); + } catch (err) { + if (err.name === "ConnectionError") { + const retryHS = getRetryHomeserver(homeserver); + if (retryHS) { + homeserver = retryHS; } else { throw err; } + } else { + throw err; } } - if (wellKnownResponse.status === 200) { - const {body} = wellKnownResponse; - const wellKnownHomeServer = body["m.homeserver"]?.["base_url"]; - if (typeof wellKnownHomeServer === "string") { - homeServer = normalizeHomeserver(wellKnownHomeServer); - } + } + if (wellKnownResponse.status === 200) { + const {body} = wellKnownResponse; + const wellKnownHomeserver = body["m.homeserver"]?.["base_url"]; + if (typeof wellKnownHomeserver === "string") { + homeserver = normalizeHomeserver(wellKnownHomeserver); } - return homeServer + } + return homeserver; } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 498cb574..4e4f1061 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -43,8 +43,8 @@ export class LoginView extends TemplateView { placeholder: vm.i18n`Your matrix homeserver`, value: vm.homeserver, disabled, - onInput: () => vm.setHomeServer(event.target.value), - onChange: event => vm.queryHomeServer(), + onInput: event => vm.setHomeserver(event.target.value), + onChange: () => vm.queryHomeserver(), }), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] diff --git a/src/platform/web/ui/view-gallery.html b/src/platform/web/ui/view-gallery.html index 7887d44f..675b2b5b 100644 --- a/src/platform/web/ui/view-gallery.html +++ b/src/platform/web/ui/view-gallery.html @@ -43,7 +43,7 @@ From e944dc5cac453facf7291d0db87927084acc56dd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:27:24 +0200 Subject: [PATCH 095/108] decrease typing timeout to 1s --- src/domain/login/LoginViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index d0cf9664..c634f7db 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -162,7 +162,7 @@ export class LoginViewModel extends ViewModel { this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); this.emitChange("isFetchingLoginOptions"); this.disposeTracked(this._abortHomeserverQueryTimeout); - const timeout = this.clock.createTimeout(2000); + const timeout = this.clock.createTimeout(1000); this._abortHomeserverQueryTimeout = this.track(() => timeout.abort()); try { await timeout.elapsed(); From e0d53e57badb4f407967a468e938818ced87d5b8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:28:21 +0200 Subject: [PATCH 096/108] clear everything when typing in homeserver field as it's not relevant anymore --- src/domain/login/LoginViewModel.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index c634f7db..72cde571 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -158,9 +158,14 @@ export class LoginViewModel extends ViewModel { async setHomeserver(newHomeserver) { this._homeserver = newHomeserver; - // abort ongoing query, if any + // clear everything set by queryHomeserver + this._loginOptions = null; + this._queriedHomeserver = null; + this._showError(""); + this._disposeViewModels(); this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); - this.emitChange("isFetchingLoginOptions"); + this.emitChange(); // multiple fields changing + // also clear the timeout if it is still running this.disposeTracked(this._abortHomeserverQueryTimeout); const timeout = this.clock.createTimeout(1000); this._abortHomeserverQueryTimeout = this.track(() => timeout.abort()); @@ -177,14 +182,16 @@ export class LoginViewModel extends ViewModel { this.queryHomeserver(); } - this._errorMessage = ""; - this.emitChange("errorMessage"); - // if query is called before the typing timeout hits (e.g. field lost focus), cancel the timeout so we don't query again. async queryHomeserver() { + // given that setHomeserver already clears everything set here, + // and that is the only way to change the homeserver, + // we don't need to reset things again here. + // However, clear things set by setHomeserver: + // if query is called before the typing timeout hits (e.g. field lost focus), + // cancel the timeout so we don't query again. this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); // cancel ongoing query operation, if any this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); - this._disposeViewModels(); try { const queryOperation = this._sessionContainer.queryLogin(this._homeserver); this._abortQueryOperation = this.track(() => queryOperation.abort()); @@ -209,7 +216,7 @@ export class LoginViewModel extends ViewModel { } } else { - this._showError("Could not query login methods supported by the homeserver"); + this._showError(`Could not query login methods supported by ${this.homeserver}`); } } From 3dbffdb417ab32695624563cb4a3d1fb79418723 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:28:43 +0200 Subject: [PATCH 097/108] don't requery the same homeserver we just did --- src/domain/login/LoginViewModel.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 72cde571..d629241d 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -36,6 +36,7 @@ export class LoginViewModel extends ViewModel { this._loadViewModel = null; this._loadViewModelSubscription = null; this._homeserver = defaultHomeserver; + this._queriedHomeserver = null; this._errorMessage = ""; this._hideHomeserver = false; this._isBusy = false; @@ -183,6 +184,11 @@ export class LoginViewModel extends ViewModel { } async queryHomeserver() { + // don't repeat a query we've just done + if (this._homeserver === this._queriedHomeserver || this._homeserver === "") { + return; + } + this._queriedHomeserver = this._homeserver; // given that setHomeserver already clears everything set here, // and that is the only way to change the homeserver, // we don't need to reset things again here. From c0d3c950b02527d9f5d8c310d31cf2c7b08d342e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:28:59 +0200 Subject: [PATCH 098/108] show the homeserver after lookup --- src/domain/login/LoginViewModel.js | 2 ++ src/platform/web/ui/css/themes/element/theme.css | 6 ++++++ src/platform/web/ui/login/LoginView.js | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index d629241d..f2e9f0a5 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -49,6 +49,7 @@ export class LoginViewModel extends ViewModel { get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } get homeserver() { return this._homeserver; } + get forwardedHomeserver() { return this._loginOptions?.homeserver; } get errorMessage() { return this._errorMessage; } get showHomeserver() { return !this._hideHomeserver; } get loadViewModel() {return this._loadViewModel; } @@ -203,6 +204,7 @@ export class LoginViewModel extends ViewModel { this._abortQueryOperation = this.track(() => queryOperation.abort()); this.emitChange("isFetchingLoginOptions"); this._loginOptions = await queryOperation.result; + this.emitChange("forwardedHomeserver"); } catch (e) { if (e.name === "AbortError") { diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 08f1df06..1b0bc9e4 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -238,6 +238,12 @@ a.button-action { font-size: 1.5rem; } +.LoginView_forwardInfo { + font-size: 0.9em; + margin-left: 1em; + color: #777; +} + .CompleteSSOView_title { font-weight: 500; } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 4e4f1061..fc2bf686 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -46,6 +46,10 @@ export class LoginView extends TemplateView { onInput: event => vm.setHomeserver(event.target.value), onChange: () => vm.queryHomeserver(), }), + t.p({className: { + LoginView_forwardInfo: true, + hidden: vm => !vm.forwardedHomeserver + }}, vm => vm.i18n`${vm.homeserver} forwards to ${vm.forwardedHomeserver}.`), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] )), From 59605a2a57041ec6061a6ec86225a6b4ca9a0569 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:53:33 +0200 Subject: [PATCH 099/108] don't fail login on missing cors on well-known --- src/matrix/well-known.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js index 1217c43e..c29939c2 100644 --- a/src/matrix/well-known.js +++ b/src/matrix/well-known.js @@ -32,28 +32,35 @@ function getRetryHomeserver(homeserver) { } } -export async function lookupHomeserver(homeserver, request) { - homeserver = normalizeHomeserver(homeserver); +async function getWellKnownResponse(homeserver, request) { const requestOptions = {format: "json", timeout: 30000, method: "GET"}; let wellKnownResponse = null; while (!wellKnownResponse) { try { const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; - wellKnownResponse = await request(wellKnownUrl, requestOptions).response(); + return await request(wellKnownUrl, requestOptions).response(); } catch (err) { if (err.name === "ConnectionError") { const retryHS = getRetryHomeserver(homeserver); if (retryHS) { homeserver = retryHS; } else { - throw err; + // don't fail lookup on a ConnectionError, + // there might be a missing CORS header on a 404 response or something, + // which won't be a problem necessarily with homeserver requests later on ... + return null; } } else { throw err; } } } - if (wellKnownResponse.status === 200) { +} + +export async function lookupHomeserver(homeserver, request) { + homeserver = normalizeHomeserver(homeserver); + const wellKnownResponse = await getWellKnownResponse(homeserver, request); + if (wellKnownResponse && wellKnownResponse.status === 200) { const {body} = wellKnownResponse; const wellKnownHomeserver = body["m.homeserver"]?.["base_url"]; if (typeof wellKnownHomeserver === "string") { From 160ae0b76762d24c645598f65d4eed87c7916ab0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:53:52 +0200 Subject: [PATCH 100/108] adjust to AbortableOperation api change --- src/domain/login/CompleteSSOLoginViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index b7e04cc8..7821313a 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -46,7 +46,7 @@ export class CompleteSSOLoginViewModel extends ViewModel { const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver"); let loginOptions; try { - loginOptions = await this._sessionContainer.queryLogin(homeserver); + loginOptions = await this._sessionContainer.queryLogin(homeserver).result; } catch (err) { this._showError(err.message); From d1412e1f425922a65ce73b17430669329ed9639a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:05:42 +0200 Subject: [PATCH 101/108] don't retry with www.{host}, as it's a minor security issue if www.host gets hacked (e.g. a bad wordpress plugin), it could spread to the matrix server running on a different host. --- src/matrix/well-known.js | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js index c29939c2..00c91f27 100644 --- a/src/matrix/well-known.js +++ b/src/matrix/well-known.js @@ -22,37 +22,19 @@ function normalizeHomeserver(homeserver) { } } -function getRetryHomeserver(homeserver) { - const url = new URL(homeserver); - const {host} = url; - const dotCount = host.split(".").length - 1; - if (dotCount === 1) { - url.host = `www.${host}`; - return url.origin; - } -} - async function getWellKnownResponse(homeserver, request) { const requestOptions = {format: "json", timeout: 30000, method: "GET"}; - let wellKnownResponse = null; - while (!wellKnownResponse) { - try { - const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; - return await request(wellKnownUrl, requestOptions).response(); - } catch (err) { - if (err.name === "ConnectionError") { - const retryHS = getRetryHomeserver(homeserver); - if (retryHS) { - homeserver = retryHS; - } else { - // don't fail lookup on a ConnectionError, - // there might be a missing CORS header on a 404 response or something, - // which won't be a problem necessarily with homeserver requests later on ... - return null; - } - } else { - throw err; - } + try { + const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; + return await request(wellKnownUrl, requestOptions).response(); + } catch (err) { + if (err.name === "ConnectionError") { + // don't fail lookup on a ConnectionError, + // there might be a missing CORS header on a 404 response or something, + // which won't be a problem necessarily with homeserver requests later on ... + return null; + } else { + throw err; } } } From d31e442c168b6af4a07181bbe872c74fec497941 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:06:52 +0200 Subject: [PATCH 102/108] better text --- src/platform/web/ui/login/LoginView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index fc2bf686..8ed91357 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -49,7 +49,7 @@ export class LoginView extends TemplateView { t.p({className: { LoginView_forwardInfo: true, hidden: vm => !vm.forwardedHomeserver - }}, vm => vm.i18n`${vm.homeserver} forwards to ${vm.forwardedHomeserver}.`), + }}, vm => vm.i18n`You will connect to ${vm.forwardedHomeserver}.`), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] )), From 65bd892d8f55bdc537d31a657359a5e26896d339 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:09:45 +0200 Subject: [PATCH 103/108] better naming --- src/domain/login/LoginViewModel.js | 4 ++-- src/platform/web/ui/login/LoginView.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index f2e9f0a5..9cdf9290 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -49,7 +49,7 @@ export class LoginViewModel extends ViewModel { get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } get homeserver() { return this._homeserver; } - get forwardedHomeserver() { return this._loginOptions?.homeserver; } + get resolvedHomeserver() { return this._loginOptions?.homeserver; } get errorMessage() { return this._errorMessage; } get showHomeserver() { return !this._hideHomeserver; } get loadViewModel() {return this._loadViewModel; } @@ -204,7 +204,7 @@ export class LoginViewModel extends ViewModel { this._abortQueryOperation = this.track(() => queryOperation.abort()); this.emitChange("isFetchingLoginOptions"); this._loginOptions = await queryOperation.result; - this.emitChange("forwardedHomeserver"); + this.emitChange("resolvedHomeserver"); } catch (e) { if (e.name === "AbortError") { diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 8ed91357..aa89ccca 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -48,8 +48,8 @@ export class LoginView extends TemplateView { }), t.p({className: { LoginView_forwardInfo: true, - hidden: vm => !vm.forwardedHomeserver - }}, vm => vm.i18n`You will connect to ${vm.forwardedHomeserver}.`), + hidden: vm => !vm.resolvedHomeserver + }}, vm => vm.i18n`You will connect to ${vm.resolvedHomeserver}.`), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] )), From 3d6a990c58c7b5156fffc14ef53366fb3c9f03e7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:14:20 +0200 Subject: [PATCH 104/108] release v0.2.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc4fb969..ebe5cd97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.5", + "version": "0.2.6", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "main": "index.js", "directories": { From 54ac5a96ca46d0278f99a4a02282251f13e484cb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:23:38 +0200 Subject: [PATCH 105/108] rename service-worker.js back to sw.js to unbreak updating --- scripts/build.mjs | 8 ++++---- src/platform/web/docroot/index.html | 2 +- src/platform/web/docroot/{service-worker.js => sw.js} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename src/platform/web/docroot/{service-worker.js => sw.js} (100%) diff --git a/scripts/build.mjs b/scripts/build.mjs index 82235cc4..0674cb93 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -114,8 +114,8 @@ async function build({modernOnly, overrideImports, overrideCss}) { await buildManifest(assets); // all assets have been added, create a hash from all assets name to cache unhashed files like index.html assets.addToHashForAll("index.html", devHtml); - let swSource = await fs.readFile(path.join(snowpackOutPath, "service-worker.js"), "utf8"); - assets.addToHashForAll("service-worker.js", swSource); + let swSource = await fs.readFile(path.join(snowpackOutPath, "sw.js"), "utf8"); + assets.addToHashForAll("sw.js", swSource); const globalHash = assets.hashForAll(); @@ -174,7 +174,7 @@ async function buildHtml(doc, version, baseConfig, globalHash, modernOnly, asset const configJSON = JSON.stringify(Object.assign({}, baseConfig, { worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null, downloadSandbox: assets.resolve("download-sandbox.html"), - serviceWorker: "service-worker.js", + serviceWorker: "sw.js", olm: { wasm: assets.resolve("olm.wasm"), legacyBundle: assets.resolve("olm_legacy.js"), @@ -342,7 +342,7 @@ async function buildServiceWorker(swSource, version, globalHash, assets) { swSource = replaceStringInSource("NOTIFICATION_BADGE_ICON", assets.resolve("icon.png")); // service worker should not have a hashed name as it is polled by the browser for updates - await assets.writeUnhashed("service-worker.js", swSource); + await assets.writeUnhashed("sw.js", swSource); } async function buildCssBundles(buildFn, themes, assets, mainCssFile = null) { diff --git a/src/platform/web/docroot/index.html b/src/platform/web/docroot/index.html index 90af082a..0b266ff3 100644 --- a/src/platform/web/docroot/index.html +++ b/src/platform/web/docroot/index.html @@ -26,7 +26,7 @@ downloadSandbox: "assets/download-sandbox.html", defaultHomeServer: "matrix.org", // NOTE: uncomment this if you want the service worker for local development - // serviceWorker: "service-worker.js", + // serviceWorker: "sw.js", // NOTE: provide push config if you want push notifs for local development // see assets/config.json for what the config looks like // push: {...}, diff --git a/src/platform/web/docroot/service-worker.js b/src/platform/web/docroot/sw.js similarity index 100% rename from src/platform/web/docroot/service-worker.js rename to src/platform/web/docroot/sw.js From 3d4f69d048efe4fd3419637541d41f6f04cd3c6a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:25:47 +0200 Subject: [PATCH 106/108] release v0.2.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ebe5cd97..c7e0cd9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.6", + "version": "0.2.7", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "main": "index.js", "directories": { From e105bc42370c611e5ffe9cfc5957dd78f919944d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Aug 2021 15:31:18 +0200 Subject: [PATCH 107/108] fix lint warnings --- src/domain/session/room/timeline/TilesCollection.js | 2 +- src/domain/session/room/timeline/TimelineViewModel.js | 4 ++-- src/domain/session/room/timeline/tiles/SimpleTile.js | 6 +++--- src/matrix/room/timeline/persistence/RelationWriter.js | 2 +- src/observable/map/MappedMap.js | 2 +- src/observable/map/ObservableMap.js | 2 +- src/platform/web/Platform.js | 2 +- src/platform/web/ui/general/ListView.js | 4 ++-- src/platform/web/ui/session/room/MessageComposer.js | 1 - src/platform/web/ui/session/room/RoomArchivedView.js | 4 ++-- .../web/ui/session/room/timeline/TextMessageView.js | 2 +- src/utils/EventEmitter.js | 4 ++-- 12 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 0acb2859..10062af2 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -219,7 +219,7 @@ export class TilesCollection extends BaseObservableList { } } - onMove(fromIdx, toIdx, value) { + onMove(/*fromIdx, toIdx, value*/) { // this ... cannot happen in the timeline? // perhaps we can use this event to support a local echo (in a different fragment) // to be moved to the key of the remote echo, so we don't loose state ... ? diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 7b2765f5..a08ab060 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -59,7 +59,7 @@ export class TimelineViewModel extends ViewModel { } } - unloadAtTop(tileAmount) { + unloadAtTop(/*tileAmount*/) { // get lowerSortKey for tile at index tileAmount - 1 // tell timeline to unload till there (included given key) } @@ -68,7 +68,7 @@ export class TimelineViewModel extends ViewModel { } - unloadAtBottom(tileAmount) { + unloadAtBottom(/*tileAmount*/) { // get upperSortKey for tile at index tiles.length - tileAmount // tell timeline to unload till there (included given key) } diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 68037a14..3c370b72 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -101,7 +101,7 @@ export class SimpleTile extends ViewModel { // return whether the tile should be removed // as SimpleTile only has one entry, the tile should be removed - removeEntry(entry) { + removeEntry(/*entry*/) { return true; } @@ -110,12 +110,12 @@ export class SimpleTile extends ViewModel { return false; } // let item know it has a new sibling - updatePreviousSibling(prev) { + updatePreviousSibling(/*prev*/) { } // let item know it has a new sibling - updateNextSibling(next) { + updateNextSibling(/*next*/) { } diff --git a/src/matrix/room/timeline/persistence/RelationWriter.js b/src/matrix/room/timeline/persistence/RelationWriter.js index 4944cc64..afc99e6d 100644 --- a/src/matrix/room/timeline/persistence/RelationWriter.js +++ b/src/matrix/room/timeline/persistence/RelationWriter.js @@ -147,7 +147,7 @@ export class RelationWriter { return true; } - _aggregateAnnotation(annotationEvent, targetStorageEntry, log) { + _aggregateAnnotation(annotationEvent, targetStorageEntry/*, log*/) { // TODO: do we want to verify it is a m.reaction event somehow? const relation = getRelation(annotationEvent); if (!relation) { diff --git a/src/observable/map/MappedMap.js b/src/observable/map/MappedMap.js index 47013df8..2a810058 100644 --- a/src/observable/map/MappedMap.js +++ b/src/observable/map/MappedMap.js @@ -42,7 +42,7 @@ export class MappedMap extends BaseObservableMap { this.emitAdd(key, mappedValue); } - onRemove(key, _value) { + onRemove(key/*, _value*/) { const mappedValue = this._mappedValues.get(key); if (this._mappedValues.delete(key)) { this.emitRemove(key, mappedValue); diff --git a/src/observable/map/ObservableMap.js b/src/observable/map/ObservableMap.js index b72cd039..8f5a0922 100644 --- a/src/observable/map/ObservableMap.js +++ b/src/observable/map/ObservableMap.js @@ -156,7 +156,7 @@ export function tests() { assert.equal(key, 1); assert.deepEqual(value, {value: 5}); }, - onUpdate(key, value, params) { + onUpdate(key, value/*, params*/) { update_fired += 1; assert.equal(key, 1); assert.deepEqual(value, {value: 7}); diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 7e28f36f..40f47101 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -221,7 +221,7 @@ export class Platform { if (mimeType) { input.setAttribute("accept", mimeType); } - const promise = new Promise((resolve, reject) => { + const promise = new Promise(resolve => { const checkFile = () => { input.removeEventListener("change", checkFile, true); const file = input.files[0]; diff --git a/src/platform/web/ui/general/ListView.js b/src/platform/web/ui/general/ListView.js index 884eedc4..74aa9d87 100644 --- a/src/platform/web/ui/general/ListView.js +++ b/src/platform/web/ui/general/ListView.js @@ -121,7 +121,7 @@ export class ListView { this.onListChanged(); } - onRemove(idx, _value) { + onRemove(idx/*, _value*/) { this.onBeforeListChanged(); const [child] = this._childInstances.splice(idx, 1); child.root().remove(); @@ -129,7 +129,7 @@ export class ListView { this.onListChanged(); } - onMove(fromIdx, toIdx, value) { + onMove(fromIdx, toIdx/*, value*/) { this.onBeforeListChanged(); const [child] = this._childInstances.splice(fromIdx, 1); this._childInstances.splice(toIdx, 0, child); diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js index e1d8f13c..c6e8a1ee 100644 --- a/src/platform/web/ui/session/room/MessageComposer.js +++ b/src/platform/web/ui/session/room/MessageComposer.js @@ -17,7 +17,6 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView.js"; import {Popup} from "../../general/Popup.js"; import {Menu} from "../../general/Menu.js"; -import {TextMessageView} from "./timeline/TextMessageView.js"; import {viewClassForEntry} from "./TimelineList.js" export class MessageComposer extends TemplateView { diff --git a/src/platform/web/ui/session/room/RoomArchivedView.js b/src/platform/web/ui/session/room/RoomArchivedView.js index e5e489ed..80b18a08 100644 --- a/src/platform/web/ui/session/room/RoomArchivedView.js +++ b/src/platform/web/ui/session/room/RoomArchivedView.js @@ -17,7 +17,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView.js"; export class RoomArchivedView extends TemplateView { - render(t, vm) { + render(t) { return t.div({className: "RoomArchivedView"}, t.h3(vm => vm.description)); } -} \ No newline at end of file +} diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index ef4d61d5..fcafaf27 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -97,7 +97,7 @@ const formatFunction = { link: linkPart => tag.a({href: linkPart.url, className: "link", target: "_blank", rel: "noopener" }, renderParts(linkPart.inlines)), pill: renderPill, format: formatPart => tag[formatPart.format](renderParts(formatPart.children)), - rule: rulePart => tag.hr(), + rule: () => tag.hr(), list: renderList, image: renderImage, newline: () => tag.br() diff --git a/src/utils/EventEmitter.js b/src/utils/EventEmitter.js index 2d2e4458..5dd56ac3 100644 --- a/src/utils/EventEmitter.js +++ b/src/utils/EventEmitter.js @@ -55,9 +55,9 @@ export class EventEmitter { } } - onFirstSubscriptionAdded(name) {} + onFirstSubscriptionAdded(/* name */) {} - onLastSubscriptionRemoved(name) {} + onLastSubscriptionRemoved(/* name */) {} } export function tests() { From cb9606a87bebdebebc5933b9b30ab7b326dace3c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Aug 2021 15:33:41 +0200 Subject: [PATCH 108/108] remove dead code for incomplete memory store --- .eslintignore | 1 - snowpack.config.js | 1 - src/matrix/storage/memory/Storage.js | 53 ---- src/matrix/storage/memory/Transaction.js | 73 ------ .../memory/stores/RoomTimelineStore.js | 237 ------------------ src/matrix/storage/memory/stores/Store.js | 43 ---- 6 files changed, 408 deletions(-) delete mode 100644 .eslintignore delete mode 100644 src/matrix/storage/memory/Storage.js delete mode 100644 src/matrix/storage/memory/Transaction.js delete mode 100644 src/matrix/storage/memory/stores/RoomTimelineStore.js delete mode 100644 src/matrix/storage/memory/stores/Store.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 732865cb..00000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -src/matrix/storage/memory \ No newline at end of file diff --git a/snowpack.config.js b/snowpack.config.js index 3de1d723..68f33242 100644 --- a/snowpack.config.js +++ b/snowpack.config.js @@ -17,7 +17,6 @@ module.exports = { '**/scripts/**', '**/target/**', '**/prototypes/**', - '**/src/matrix/storage/memory/**', '**/src/platform/web/legacy-polyfill.js', '**/src/platform/web/worker/polyfill.js' ], diff --git a/src/matrix/storage/memory/Storage.js b/src/matrix/storage/memory/Storage.js deleted file mode 100644 index c1c0fe3c..00000000 --- a/src/matrix/storage/memory/Storage.js +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {Transaction} from "./Transaction.js"; -import { STORE_MAP, STORE_NAMES } from "../common.js"; - -export class Storage { - constructor(initialStoreValues = {}) { - this._validateStoreNames(Object.keys(initialStoreValues)); - this.storeNames = STORE_MAP; - this._storeValues = STORE_NAMES.reduce((values, name) => { - values[name] = initialStoreValues[name] || null; - }, {}); - } - - _validateStoreNames(storeNames) { - const idx = storeNames.findIndex(name => !STORE_MAP.hasOwnProperty(name)); - if (idx !== -1) { - throw new Error(`Invalid store name ${storeNames[idx]}`); - } - } - - _createTxn(storeNames, writable) { - this._validateStoreNames(storeNames); - const storeValues = storeNames.reduce((values, name) => { - return values[name] = this._storeValues[name]; - }, {}); - return Promise.resolve(new Transaction(storeValues, writable)); - } - - readTxn(storeNames) { - // TODO: avoid concurrency - return this._createTxn(storeNames, false); - } - - readWriteTxn(storeNames) { - // TODO: avoid concurrency - return this._createTxn(storeNames, true); - } -} diff --git a/src/matrix/storage/memory/Transaction.js b/src/matrix/storage/memory/Transaction.js deleted file mode 100644 index 894db805..00000000 --- a/src/matrix/storage/memory/Transaction.js +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {RoomTimelineStore} from "./stores/RoomTimelineStore.js"; - -export class Transaction { - constructor(storeValues, writable) { - this._storeValues = storeValues; - this._txnStoreValues = {}; - this._writable = writable; - } - - _store(name, mapper) { - if (!this._txnStoreValues.hasOwnProperty(name)) { - if (!this._storeValues.hasOwnProperty(name)) { - throw new Error(`Transaction wasn't opened for store ${name}`); - } - const store = mapper(this._storeValues[name]); - const clone = store.cloneStoreValue(); - // extra prevention for writing - if (!this._writable) { - Object.freeze(clone); - } - this._txnStoreValues[name] = clone; - } - return mapper(this._txnStoreValues[name]); - } - - get session() { - throw new Error("not yet implemented"); - // return this._store("session", storeValue => new SessionStore(storeValue)); - } - - get roomSummary() { - throw new Error("not yet implemented"); - // return this._store("roomSummary", storeValue => new RoomSummaryStore(storeValue)); - } - - get roomTimeline() { - return this._store("roomTimeline", storeValue => new RoomTimelineStore(storeValue)); - } - - get roomState() { - throw new Error("not yet implemented"); - // return this._store("roomState", storeValue => new RoomStateStore(storeValue)); - } - - complete() { - for(let name of Object.keys(this._txnStoreValues)) { - this._storeValues[name] = this._txnStoreValues[name]; - } - this._txnStoreValues = null; - return Promise.resolve(); - } - - abort() { - this._txnStoreValues = null; - return Promise.resolve(); - } -} diff --git a/src/matrix/storage/memory/stores/RoomTimelineStore.js b/src/matrix/storage/memory/stores/RoomTimelineStore.js deleted file mode 100644 index 5be20eae..00000000 --- a/src/matrix/storage/memory/stores/RoomTimelineStore.js +++ /dev/null @@ -1,237 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {SortKey} from "../../room/timeline/SortKey.js"; -import {sortedIndex} from "../../../utils/sortedIndex.js"; -import {Store} from "./Store.js"; - -function compareKeys(key, entry) { - if (key.roomId === entry.roomId) { - return key.sortKey.compare(entry.sortKey); - } else { - return key.roomId < entry.roomId ? -1 : 1; - } -} - -class Range { - constructor(timeline, lower, upper, lowerOpen, upperOpen) { - this._timeline = timeline; - this._lower = lower; - this._upper = upper; - this._lowerOpen = lowerOpen; - this._upperOpen = upperOpen; - } - - /** projects the range onto the timeline array */ - project(roomId, maxCount = Number.MAX_SAFE_INTEGER) { - // determine lowest and highest allowed index. - // Important not to bleed into other roomIds here. - const lowerKey = {roomId, sortKey: this._lower || SortKey.minKey }; - // apply lower key being open (excludes given key) - let minIndex = sortedIndex(this._timeline, lowerKey, compareKeys); - if (this._lowerOpen && minIndex < this._timeline.length && compareKeys(lowerKey, this._timeline[minIndex]) === 0) { - minIndex += 1; - } - const upperKey = {roomId, sortKey: this._upper || SortKey.maxKey }; - // apply upper key being open (excludes given key) - let maxIndex = sortedIndex(this._timeline, upperKey, compareKeys); - if (this._upperOpen && maxIndex < this._timeline.length && compareKeys(upperKey, this._timeline[maxIndex]) === 0) { - maxIndex -= 1; - } - // find out from which edge we should grow - // if upper or lower bound - // again, important not to go below minIndex or above maxIndex - // to avoid bleeding into other rooms - let startIndex, endIndex; - if (!this._lower && this._upper) { - startIndex = Math.max(minIndex, maxIndex - maxCount); - endIndex = maxIndex; - } else if (this._lower && !this._upper) { - startIndex = minIndex; - endIndex = Math.min(maxIndex, minIndex + maxCount); - } else { - startIndex = minIndex; - endIndex = maxIndex; - } - - // if startIndex is out of range, make range empty - if (startIndex === this._timeline.length) { - startIndex = endIndex = 0; - } - const count = endIndex - startIndex; - return {startIndex, count}; - } - - select(roomId, maxCount) { - const {startIndex, count} = this.project(roomId, this._timeline, maxCount); - return this._timeline.slice(startIndex, startIndex + count); - } -} - -export class RoomTimelineStore extends Store { - constructor(timeline, writable) { - super(timeline || [], writable); - } - - get _timeline() { - return this._storeValue; - } - - /** Creates a range that only includes the given key - * @param {SortKey} sortKey the key - * @return {Range} the created range - */ - onlyRange(sortKey) { - return new Range(this._timeline, sortKey, sortKey); - } - - /** Creates a range that includes all keys before sortKey, and optionally also the key itself. - * @param {SortKey} sortKey the key - * @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the upper end. - * @return {Range} the created range - */ - upperBoundRange(sortKey, open=false) { - return new Range(this._timeline, undefined, sortKey, undefined, open); - } - - /** Creates a range that includes all keys after sortKey, and optionally also the key itself. - * @param {SortKey} sortKey the key - * @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the lower end. - * @return {Range} the created range - */ - lowerBoundRange(sortKey, open=false) { - return new Range(this._timeline, sortKey, undefined, open); - } - - /** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well. - * @param {SortKey} lower the lower key - * @param {SortKey} upper the upper key - * @param {boolean} [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range. - * @param {boolean} [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range. - * @return {Range} the created range - */ - boundRange(lower, upper, lowerOpen=false, upperOpen=false) { - return new Range(this._timeline, lower, upper, lowerOpen, upperOpen); - } - - /** Looks up the last `amount` entries in the timeline for `roomId`. - * @param {string} roomId - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. - */ - lastEvents(roomId, amount) { - return this.eventsBefore(roomId, SortKey.maxKey, amount); - } - - /** Looks up the first `amount` entries in the timeline for `roomId`. - * @param {string} roomId - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. - */ - firstEvents(roomId, amount) { - return this.eventsAfter(roomId, SortKey.minKey, amount); - } - - /** Looks up `amount` entries after `sortKey` in the timeline for `roomId`. - * The entry for `sortKey` is not included. - * @param {string} roomId - * @param {SortKey} sortKey - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. - */ - eventsAfter(roomId, sortKey, amount) { - const events = this.lowerBoundRange(sortKey, true).select(roomId, amount); - return Promise.resolve(events); - } - - /** Looks up `amount` entries before `sortKey` in the timeline for `roomId`. - * The entry for `sortKey` is not included. - * @param {string} roomId - * @param {SortKey} sortKey - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. - */ - eventsBefore(roomId, sortKey, amount) { - const events = this.upperBoundRange(sortKey, true).select(roomId, amount); - return Promise.resolve(events); - } - - /** Looks up the first, if any, event entry (so excluding gap entries) after `sortKey`. - * @param {string} roomId - * @param {SortKey} sortKey - * @return {Promise<(?Entry)>} a promise resolving to entry, if any. - */ - nextEvent(roomId, sortKey) { - const searchSpace = this.lowerBoundRange(sortKey, true).select(roomId); - const event = searchSpace.find(entry => !!entry.event); - return Promise.resolve(event); - } - - /** Looks up the first, if any, event entry (so excluding gap entries) before `sortKey`. - * @param {string} roomId - * @param {SortKey} sortKey - * @return {Promise<(?Entry)>} a promise resolving to entry, if any. - */ - previousEvent(roomId, sortKey) { - const searchSpace = this.upperBoundRange(sortKey, true).select(roomId); - const event = searchSpace.reverse().find(entry => !!entry.event); - return Promise.resolve(event); - } - - /** Inserts a new entry into the store. The combination of roomId and sortKey should not exist yet, or an error is thrown. - * @param {Entry} entry the entry to insert - * @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. - * @throws {StorageError} ... - */ - insert(entry) { - this.assertWritable(); - const insertIndex = sortedIndex(this._timeline, entry, compareKeys); - if (insertIndex < this._timeline.length) { - const existingEntry = this._timeline[insertIndex]; - if (compareKeys(entry, existingEntry) === 0) { - return Promise.reject(new Error("entry already exists")); - } - } - this._timeline.splice(insertIndex, 0, entry); - return Promise.resolve(); - } - - /** Updates the entry into the store with the given [roomId, sortKey] combination. - * If not yet present, will insert. Might be slower than add. - * @param {Entry} entry the entry to update. - * @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. - */ - update(entry) { - this.assertWritable(); - let update = false; - const updateIndex = sortedIndex(this._timeline, entry, compareKeys); - if (updateIndex < this._timeline.length) { - const existingEntry = this._timeline[updateIndex]; - if (compareKeys(entry, existingEntry) === 0) { - update = true; - } - } - this._timeline.splice(updateIndex, update ? 1 : 0, entry); - return Promise.resolve(); - } - - get(roomId, sortKey) { - const range = this.onlyRange(sortKey); - const {startIndex, count} = range.project(roomId); - const event = count ? this._timeline[startIndex] : undefined; - return Promise.resolve(event); - } -} diff --git a/src/matrix/storage/memory/stores/Store.js b/src/matrix/storage/memory/stores/Store.js deleted file mode 100644 index c7218ab8..00000000 --- a/src/matrix/storage/memory/stores/Store.js +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -export class Store { - constructor(storeValue, writable) { - this._storeValue = storeValue; - this._writable = writable; - } - - // makes a copy deep enough that any modifications in the store - // won't affect the original - // used for transactions - cloneStoreValue() { - // assumes 1 level deep is enough, and that values will be replaced - // rather than updated. - if (Array.isArray(this._storeValue)) { - return this._storeValue.slice(); - } else if (typeof this._storeValue === "object") { - return Object.assign({}, this._storeValue); - } else { - return this._storeValue; - } - } - - assertWritable() { - if (!this._writable) { - throw new Error("Tried to write in read-only transaction"); - } - } -}