From acb5e0104a79d8974763e1d2528e7716dc716610 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 23 Oct 2020 18:04:02 +0200 Subject: [PATCH 1/7] some improvements --- src/js/login.js | 76 +++++++++++++++++++++++++++++++++------------ src/js/re.js | 38 +++++++++++++++++++++++ src/login.pug | 4 +-- src/sass/login.sass | 1 + 4 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 src/js/re.js diff --git a/src/js/login.js b/src/js/login.js index 4ae9eae..df8cf35 100644 --- a/src/js/login.js +++ b/src/js/login.js @@ -1,36 +1,67 @@ const {q, ElemJS, ejs} = require("./basic.js") +const {S_RE_DOMAIN, S_RE_IPV6, S_RE_IPV4} = require("./re.js") const password = q("#password") -const homeserver = q("#homeserver") + +// A regex matching a lossy MXID +// Groups: +// 1: username/localpart +// MAYBE WITH +// 2: hostname/serverpart +// 3: domain +// OR +// 4: IP address +// 5: IPv4 address +// OR +// 6: IPv6 address +// MAYBE WITH +// 7: port number +const RE_LOSSY_MXID = new RegExp(`^@?([a-z0-9._=-]+?)(?::((?:(${S_RE_DOMAIN})|((${S_RE_IPV4})|(${S_RE_IPV6})))(?::(\\d+))?))?$`) + +window.RE_LOSSY_MXID = RE_LOSSY_MXID class Username extends ElemJS { - constructor() { + constructor(homeserver) { super(q("#username")) + this.homeserver = homeserver; + this.on("change", this.updateServer.bind(this)) } isValid() { - return !!this.element.value.match(/^@?[a-z0-9._=\/-]+(?::[a-zA-Z0-9.:\[\]-]+)?$/) + return !!this.element.value.match(RE_LOSSY_MXID) } getUsername() { - return this.element.value.match(/^@?([a-z0-9._=\/-]+)/)[1] + return this.element.value.match(RE_LOSSY_MXID)[1] } getServer() { - const server = this.element.value.match(/^@?[a-z0-9._=\?-]+:([a-zA-Z0-9.:\[\]-]+)$/) - if (server && server[1]) return server[1] + const server = this.element.value.match(RE_LOSSY_MXID) + if (server && server[2]) return server[2] else return null } updateServer() { if (!this.isValid()) return - if (this.getServer()) homeserver.value = this.getServer() + if (this.getServer()) this.homeserver.suggest(this.getServer()) } } -const username = new Username() +class Homeserver extends ElemJS { + constructor() { + super(q("#homeserver")); + } + + suggest(value) { + this.element.placeholder = value + } + + getServer() { + return this.element.value || this.element.placeholder; + } +} class Feedback extends ElemJS { constructor() { @@ -60,13 +91,14 @@ class Feedback extends ElemJS { } } -const feedback = new Feedback() - class Form extends ElemJS { - constructor() { + constructor(username, feedback, homeserver) { super(q("#form")) this.processing = false + this.username = username + this.feedback = feedback + this.homeserver = homeserver this.on("submit", this.submit.bind(this)) } @@ -74,12 +106,12 @@ class Form extends ElemJS { async submit() { if (this.processing) return this.processing = true - if (!username.isValid()) return this.cancel("Username is not valid.") + if (!this.username.isValid()) return this.cancel("Username is not valid.") // Resolve homeserver address let domain try { - domain = await this.findHomeserver(homeserver.value) + domain = await this.findHomeserver(this.homeserver.getServer()) } catch(e) { return this.cancel(e.message) } @@ -90,7 +122,7 @@ class Form extends ElemJS { method: "POST", body: JSON.stringify({ type: "m.login.password", - user: username.getUsername(), + user: this.username.getUsername(), password: password.value }) }).then(res => res.json()) @@ -153,15 +185,21 @@ class Form extends ElemJS { } status(message) { - feedback.setLoading(true) - feedback.message(message) + this.feedback.setLoading(true) + this.feedback.message(message) } cancel(message) { this.processing = false - feedback.setLoading(false) - feedback.message(message, true) + this.feedback.setLoading(false) + this.feedback.message(message, true) } } -const form = new Form() +const homeserver = new Homeserver() + +const username = new Username(homeserver) + +const feedback = new Feedback() + +const form = new Form(username, feedback, homeserver) diff --git a/src/js/re.js b/src/js/re.js new file mode 100644 index 0000000..57b3daa --- /dev/null +++ b/src/js/re.js @@ -0,0 +1,38 @@ +// A valid internet domain, according to https://stackoverflow.com/a/20046959 (cleaned) +const S_RE_DOMAIN = "(?:[a-zA-Z]|[a-zA-Z][a-zA-Z]|[a-zA-Z]\\d|\\d[a-zA-Z]|[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9])\\.(?:[a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\\.[a-zA-Z]{2,3})" + +// A valid ipv4 address, one that doesn't check for valid numbers (e.g. not 999) and one that does +// const S_RE_IPV4_NO_CHECK = "(?:(?:\\d{1,3}\\.){3}\\d{1,3})" +const S_RE_IPV4_HAS_CHECK = "(?:(?:25[0-5]|(?:2[0-4]|1{0,1}\\d){0,1}\\d)\\.){3}(?:25[0-5]|(?:2[0-4]|1{0,1}\\d){0,1}\\d)" + +const S_RE_IPV6_SEG = "[a-fA-F\\d]{1,4}" +// Yes, this is an ipv6 address. +const S_RE_IPV6 = ` +(?: +(?:${S_RE_IPV6_SEG}:){7}(?:${S_RE_IPV6_SEG}|:)| +(?:${S_RE_IPV6_SEG}:){6}(?:${S_RE_IPV4_HAS_CHECK}|:${S_RE_IPV6_SEG}|:)| +(?:${S_RE_IPV6_SEG}:){5}(?::${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,2}|:)| +(?:${S_RE_IPV6_SEG}:){4}(?:(?::${S_RE_IPV6_SEG}){0,1}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,3}|:)| +(?:${S_RE_IPV6_SEG}:){3}(?:(?::${S_RE_IPV6_SEG}){0,2}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,4}|:)| +(?:${S_RE_IPV6_SEG}:){2}(?:(?::${S_RE_IPV6_SEG}){0,3}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,5}|:)| +(?:${S_RE_IPV6_SEG}:){1}(?:(?::${S_RE_IPV6_SEG}){0,4}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,6}|:)| +(?::(?:(?::${S_RE_IPV6_SEG}){0,5}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,7}|:)) +)(?:%[0-9a-zA-Z]{1,})?` + .replace(/\s*\/\/.*$/gm, '') + .replace(/\n/g, '') + .trim(); + +const RE_DOMAIN_EXACT = new RegExp(`^${S_RE_DOMAIN}$`) +const RE_IPV4_EXACT = new RegExp(`^${S_RE_IPV4_HAS_CHECK}$`) +const RE_IPV6_EXACT = new RegExp(`^${S_RE_IPV6}$`) +const RE_IP_ADDR_EXACT = new RegExp(`^${S_RE_IPV6}|${S_RE_IPV4_HAS_CHECK}$`) + +module.exports = { + S_RE_DOMAIN, + S_RE_IPV6, + S_RE_IPV4: S_RE_IPV4_HAS_CHECK, + RE_DOMAIN_EXACT, + RE_IPV4_EXACT, + RE_IPV6_EXACT, + RE_IP_ADDR_EXACT +} diff --git a/src/login.pug b/src/login.pug index 470ee82..c12d132 100644 --- a/src/login.pug +++ b/src/login.pug @@ -15,7 +15,7 @@ html .data-input .form-input-container label(for="username") Username - input(type="text" name="username" autocomplete="username" placeholder="@username:server.tld" pattern="^@?[a-z0-9._=/-]+(?::[a-zA-Z0-9.:\\[\\]-]+)?$" required)#username + input(type="text" name="username" autocomplete="username" placeholder="@username:server.com" pattern="^@?[a-z0-9._=/-]+(?::[a-zA-Z0-9.:\\[\\]-]+)?$" required)#username .form-input-container label(for="password") Password @@ -23,7 +23,7 @@ html .form-input-container label(for="homeserver") Homeserver - input(type="text" name="homeserver" value="matrix.org" placeholder="matrix.org" required)#homeserver + input(type="text" name="homeserver" placeholder="matrix.org")#homeserver #feedback diff --git a/src/sass/login.sass b/src/sass/login.sass index 235fad4..4d17f25 100644 --- a/src/sass/login.sass +++ b/src/sass/login.sass @@ -78,6 +78,7 @@ button, input[type="submit"] padding: 7px &:hover + cursor: pointer background-color: c.$milder label -- 2.34.1 From 03ea22f7a8af46f3af673554eb5596df70d38615 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Sat, 24 Oct 2020 11:52:05 +0200 Subject: [PATCH 2/7] adjust ipv6 address to use brackets --- src/js/login.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/login.js b/src/js/login.js index df8cf35..f16b4fa 100644 --- a/src/js/login.js +++ b/src/js/login.js @@ -16,7 +16,7 @@ const password = q("#password") // 6: IPv6 address // MAYBE WITH // 7: port number -const RE_LOSSY_MXID = new RegExp(`^@?([a-z0-9._=-]+?)(?::((?:(${S_RE_DOMAIN})|((${S_RE_IPV4})|(${S_RE_IPV6})))(?::(\\d+))?))?$`) +const RE_LOSSY_MXID = new RegExp(`^@?([a-z0-9._=-]+?)(?::((?:(${S_RE_DOMAIN})|((${S_RE_IPV4})|(\\[${S_RE_IPV6}])))(?::(\\d+))?))?$`) window.RE_LOSSY_MXID = RE_LOSSY_MXID -- 2.34.1 From a5309a81b1f84b5cf81070b7fd6a68a463ca8ae8 Mon Sep 17 00:00:00 2001 From: Bad Date: Fri, 30 Oct 2020 12:45:44 +0100 Subject: [PATCH 3/7] Add a popup explaining what a homeserver is --- package-lock.json | 35 +++++++++++++++++++++++++++++++++++ package.json | 3 ++- src/js/login.js | 27 +++++++++++++++++++-------- src/login.pug | 12 +++++++++++- src/sass/login.sass | 24 +++++++++++++++++++++++- src/sass/tippy.sass | 9 +++++++++ 6 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 src/sass/tippy.sass diff --git a/package-lock.json b/package-lock.json index 228e7ee..915fe72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1095,6 +1095,11 @@ "to-fast-properties": "^2.0.0" } }, + "@popperjs/core": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.5.3.tgz", + "integrity": "sha512-RFwCobxsvZ6j7twS7dHIZQZituMIDJJNHS/qY6iuthVebxS3zhRY+jaC2roEKiAYaVuTcGmX6Luc6YBcf6zJVg==" + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -2092,8 +2097,30 @@ "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", "requires": { "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", "has-symbols": "^1.0.1", "object-keys": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } } } @@ -3643,6 +3670,14 @@ "process": "~0.11.0" } }, + "tippy.js": { + "version": "6.2.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.2.7.tgz", + "integrity": "sha512-k+kWF9AJz5xLQHBi3K/XlmJiyu+p9gsCyc5qZhxxGaJWIW8SMjw1R+C7saUnP33IM8gUhDA2xX//ejRSwqR0tA==", + "requires": { + "@popperjs/core": "^2.4.4" + } + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/package.json b/package.json index 8f2ead4..9e95612 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "author": "", "license": "AGPL-3.0-only", "dependencies": { - "browserify": "^17.0.0" + "browserify": "^17.0.0", + "tippy.js": "^6.2.7" }, "devDependencies": { "@babel/core": "^7.11.1", diff --git a/src/js/login.js b/src/js/login.js index f16b4fa..a7457da 100644 --- a/src/js/login.js +++ b/src/js/login.js @@ -1,3 +1,4 @@ +const tippy = require("tippy.js"); const {q, ElemJS, ejs} = require("./basic.js") const {S_RE_DOMAIN, S_RE_IPV6, S_RE_IPV4} = require("./re.js") @@ -52,6 +53,16 @@ class Username extends ElemJS { class Homeserver extends ElemJS { constructor() { super(q("#homeserver")); + + this.tippy = tippy.default(q("#homeserver-question"), { + content: q("#homeserver-popup-template").innerHTML, + allowHTML: true, + interactive: true, + interactiveBorder: 10, + trigger: "mouseenter", + theme: "carbon", + arrow: tippy.roundArrow, + }) } suggest(value) { @@ -85,7 +96,7 @@ class Feedback extends ElemJS { this.removeClass("form-feedback") this.removeClass("form-error") if (content) this.class("form-feedback") - if(isError) this.class("form-error") + if (isError) this.class("form-error") this.messageSpan.text(content) } @@ -112,7 +123,7 @@ class Form extends ElemJS { let domain try { domain = await this.findHomeserver(this.homeserver.getServer()) - } catch(e) { + } catch (e) { return this.cancel(e.message) } @@ -149,16 +160,16 @@ class Form extends ElemJS { //Protects from servers sending us on a redirect loop maxDepth-- if (maxDepth <= 0) throw new Error(`Failed to look up homeserver, maximum search depth reached`) - + //Normalise the address if (!address.match(/^https?:\/\//)) { console.warn(`${address} doesn't specify the protocol, assuming https`) address = "https://" + address } address = address.replace(/\/*$/, "") - + this.status(`Looking up homeserver... trying ${address}`) - + // Check if we found the actual matrix server try { const versionsReq = await fetch(`${address}/_matrix/client/versions`) @@ -166,11 +177,11 @@ class Form extends ElemJS { const versions = await versionsReq.json() if (Array.isArray(versions.versions)) return address } - } catch(e) {} - + } catch (e) {} + // Find the next matrix server in the chain const root = await fetch(`${address}/.well-known/matrix/client`).then(res => res.json()).catch(e => { - console.error(e) + console.error(e) throw new Error(`Failed to look up server ${address}`) }) diff --git a/src/login.pug b/src/login.pug index c12d132..e8408eb 100644 --- a/src/login.pug +++ b/src/login.pug @@ -22,11 +22,21 @@ html input(name="password" autocomplete="current-password" type="password" required)#password .form-input-container - label(for="homeserver") Homeserver + .homeserver-label + label(for="homeserver") Homeserver + span#homeserver-question (What's this?) input(type="text" name="homeserver" placeholder="matrix.org")#homeserver #feedback .form-input-container input(type="submit" value="Log in")#submit + template#homeserver-popup-template + .homeserver-popup + p + | Homeserver is the place where your account lives. + | It's usually the website where you registered. + p + | Need help finding a homeserver? + a(href='#') Click here diff --git a/src/sass/login.sass b/src/sass/login.sass index 4d17f25..339d278 100644 --- a/src/sass/login.sass +++ b/src/sass/login.sass @@ -1,5 +1,6 @@ @use "./base" -@use "./colors.sass" as c +@use "./colors" as c +@use "./tippy" .main justify-content: center @@ -41,6 +42,27 @@ .form-error color: red +#homeserver-question + font-size: 0.6em + padding-left: 0.2em + +.homeserver-label + display: flex + justify-content: space-between + align-items: flex-end + +.homeserver-popup + p + margin: 0.2em + a + &, &:hover, &:active + color: white + text-decoration: none + &:hover + color: #f00 //Placeholder + + + @keyframes spin 0% transform: rotate(0deg) diff --git a/src/sass/tippy.sass b/src/sass/tippy.sass new file mode 100644 index 0000000..f2643f3 --- /dev/null +++ b/src/sass/tippy.sass @@ -0,0 +1,9 @@ +@use "../../node_modules/tippy.js/dist/tippy.css" +@use "../../node_modules/tippy.js/dist/svg-arrow.css" +@use "./colors.sass" as c + +.tippy-box[data-theme~="carbon"] + background-color: c.$milder + border: 2px solid c.$divider + .tippy-svg-arrow + fill: c.$milder -- 2.34.1 From 157bb9fc473be3aa6e19c7c6229c6ffb072800ee Mon Sep 17 00:00:00 2001 From: Bad Date: Sun, 1 Nov 2020 17:33:27 +0100 Subject: [PATCH 4/7] style fixes --- src/sass/tippy.sass | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sass/tippy.sass b/src/sass/tippy.sass index f2643f3..bc97399 100644 --- a/src/sass/tippy.sass +++ b/src/sass/tippy.sass @@ -3,7 +3,8 @@ @use "./colors.sass" as c .tippy-box[data-theme~="carbon"] - background-color: c.$milder - border: 2px solid c.$divider - .tippy-svg-arrow - fill: c.$milder + background-color: c.$milder + border: 2px solid c.$divider + + .tippy-svg-arrow + fill: c.$milder -- 2.34.1 From d8343f59760b48cd61bf535b25c58facd8fe4bc1 Mon Sep 17 00:00:00 2001 From: Bad Date: Sun, 1 Nov 2020 17:35:10 +0100 Subject: [PATCH 5/7] Change homserver-question into a class --- src/js/login.js | 2 +- src/login.pug | 2 +- src/sass/login.sass | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/login.js b/src/js/login.js index a7457da..ba260ef 100644 --- a/src/js/login.js +++ b/src/js/login.js @@ -54,7 +54,7 @@ class Homeserver extends ElemJS { constructor() { super(q("#homeserver")); - this.tippy = tippy.default(q("#homeserver-question"), { + this.tippy = tippy.default(q(".homeserver-question"), { content: q("#homeserver-popup-template").innerHTML, allowHTML: true, interactive: true, diff --git a/src/login.pug b/src/login.pug index e8408eb..108d989 100644 --- a/src/login.pug +++ b/src/login.pug @@ -24,7 +24,7 @@ html .form-input-container .homeserver-label label(for="homeserver") Homeserver - span#homeserver-question (What's this?) + span.homeserver-question (What's this?) input(type="text" name="homeserver" placeholder="matrix.org")#homeserver #feedback diff --git a/src/sass/login.sass b/src/sass/login.sass index d9d57cc..8482819 100644 --- a/src/sass/login.sass +++ b/src/sass/login.sass @@ -44,7 +44,7 @@ .form-error color: red -#homeserver-question +.homeserver-question font-size: 0.6em padding-left: 0.2em -- 2.34.1 From fc5dbe93e72b51366922eb10d3ae50f74175d890 Mon Sep 17 00:00:00 2001 From: Bad Date: Sun, 1 Nov 2020 17:55:57 +0100 Subject: [PATCH 6/7] Make homeserver explanatoin accessible --- src/js/login.js | 2 +- src/login.pug | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/login.js b/src/js/login.js index ba260ef..216b12d 100644 --- a/src/js/login.js +++ b/src/js/login.js @@ -59,7 +59,7 @@ class Homeserver extends ElemJS { allowHTML: true, interactive: true, interactiveBorder: 10, - trigger: "mouseenter", + trigger: "focus mouseenter", theme: "carbon", arrow: tippy.roundArrow, }) diff --git a/src/login.pug b/src/login.pug index 108d989..918e157 100644 --- a/src/login.pug +++ b/src/login.pug @@ -24,7 +24,7 @@ html .form-input-container .homeserver-label label(for="homeserver") Homeserver - span.homeserver-question (What's this?) + span.homeserver-question(tabindex=0) (What's this?) input(type="text" name="homeserver" placeholder="matrix.org")#homeserver #feedback -- 2.34.1 From 48c6ae7efbea290686787eb9377259b68569a99c Mon Sep 17 00:00:00 2001 From: Bad Date: Sun, 1 Nov 2020 17:57:19 +0100 Subject: [PATCH 7/7] Increase the size of the homeserver explanation --- src/sass/login.sass | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sass/login.sass b/src/sass/login.sass index 8482819..d8385dd 100644 --- a/src/sass/login.sass +++ b/src/sass/login.sass @@ -45,8 +45,7 @@ color: red .homeserver-question - font-size: 0.6em - padding-left: 0.2em + font-size: 0.8em .homeserver-label display: flex -- 2.34.1