WIP: Some visual improvements for login #20

Draft
jboi wants to merge 9 commits from fix/login-improvements into princess
7 changed files with 191 additions and 33 deletions

15
package-lock.json generated
View file

@ -1095,6 +1095,12 @@
"to-fast-properties": "^2.0.0" "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==",
"dev": true
},
"@types/color-name": { "@types/color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@ -3832,6 +3838,15 @@
"process": "~0.11.0" "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==",
"dev": true,
"requires": {
"@popperjs/core": "^2.4.4"
}
},
"to-fast-properties": { "to-fast-properties": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",

View file

@ -21,6 +21,7 @@
"jshint": "^2.12.0", "jshint": "^2.12.0",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"pug": "^3.0.0", "pug": "^3.0.0",
"sass": "^1.26.10" "sass": "^1.26.10",
"tippy.js": "^6.2.7"
} }
} }

View file

@ -1,36 +1,78 @@
const tippy = require("tippy.js");
const {q, ElemJS, ejs} = require("./basic.js") 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 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 { class Username extends ElemJS {
constructor() { constructor(homeserver) {
super(q("#username")) super(q("#username"))
this.homeserver = homeserver;
this.on("change", this.updateServer.bind(this)) this.on("change", this.updateServer.bind(this))
} }
isValid() { isValid() {
return !!this.element.value.match(/^@?[a-z0-9._=\/-]+(?::[a-zA-Z0-9.:\[\]-]+)?$/) return !!this.element.value.match(RE_LOSSY_MXID)
} }
getUsername() { getUsername() {
return this.element.value.match(/^@?([a-z0-9._=\/-]+)/)[1] return this.element.value.match(RE_LOSSY_MXID)[1]
} }
getServer() { getServer() {
const server = this.element.value.match(/^@?[a-z0-9._=\?-]+:([a-zA-Z0-9.:\[\]-]+)$/) const server = this.element.value.match(RE_LOSSY_MXID)
if (server && server[1]) return server[1] if (server && server[2]) return server[2]
else return null else return null
} }
updateServer() { updateServer() {
if (!this.isValid()) return 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"));
this.tippy = tippy.default(q(".homeserver-question"), {
content: q("#homeserver-popup-template").innerHTML,
allowHTML: true,
interactive: true,
interactiveBorder: 10,
trigger: "focus mouseenter",
theme: "carbon",
arrow: tippy.roundArrow,
})
}
suggest(value) {
this.element.placeholder = value
}
getServer() {
return this.element.value || this.element.placeholder;
}
}
class Feedback extends ElemJS { class Feedback extends ElemJS {
constructor() { constructor() {
@ -54,19 +96,20 @@ class Feedback extends ElemJS {
this.removeClass("form-feedback") this.removeClass("form-feedback")
this.removeClass("form-error") this.removeClass("form-error")
if (content) this.class("form-feedback") if (content) this.class("form-feedback")
if(isError) this.class("form-error") if (isError) this.class("form-error")
this.messageSpan.text(content) this.messageSpan.text(content)
} }
} }
const feedback = new Feedback()
class Form extends ElemJS { class Form extends ElemJS {
constructor() { constructor(username, feedback, homeserver) {
super(q("#form")) super(q("#form"))
this.processing = false this.processing = false
this.username = username
this.feedback = feedback
this.homeserver = homeserver
this.on("submit", this.submit.bind(this)) this.on("submit", this.submit.bind(this))
} }
@ -74,13 +117,13 @@ class Form extends ElemJS {
async submit() { async submit() {
if (this.processing) return if (this.processing) return
this.processing = true 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 // Resolve homeserver address
let domain let domain
try { try {
domain = await this.findHomeserver(homeserver.value) domain = await this.findHomeserver(this.homeserver.getServer())
} catch(e) { } catch (e) {
return this.cancel(e.message) return this.cancel(e.message)
} }
@ -90,7 +133,7 @@ class Form extends ElemJS {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
type: "m.login.password", type: "m.login.password",
user: username.getUsername(), user: this.username.getUsername(),
password: password.value password: password.value
}) })
}).then(res => res.json()) }).then(res => res.json())
@ -134,11 +177,11 @@ class Form extends ElemJS {
const versions = await versionsReq.json() const versions = await versionsReq.json()
if (Array.isArray(versions.versions)) return address if (Array.isArray(versions.versions)) return address
} }
} catch(e) {} } catch (e) {}
// Find the next matrix server in the chain // Find the next matrix server in the chain
const root = await fetch(`${address}/.well-known/matrix/client`).then(res => res.json()).catch(e => { 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}`) throw new Error(`Failed to look up server ${address}`)
}) })
@ -153,15 +196,21 @@ class Form extends ElemJS {
} }
status(message) { status(message) {
feedback.setLoading(true) this.feedback.setLoading(true)
feedback.message(message) this.feedback.message(message)
} }
cancel(message) { cancel(message) {
this.processing = false this.processing = false
feedback.setLoading(false) this.feedback.setLoading(false)
feedback.message(message, true) 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)

38
src/js/re.js Normal file
View file

@ -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
}

View file

@ -15,18 +15,28 @@ html
.data-input .data-input
.form-input-container .form-input-container
label(for="username") Username 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 .form-input-container
label(for="password") Password label(for="password") Password
input(name="password" autocomplete="current-password" type="password" required)#password input(name="password" autocomplete="current-password" type="password" required)#password
.form-input-container .form-input-container
label(for="homeserver") Homeserver .homeserver-label
input(type="text" name="homeserver" value="matrix.org" placeholder="matrix.org" required)#homeserver label(for="homeserver") Homeserver
span.homeserver-question(tabindex=0) (What's this?)
input(type="text" name="homeserver" placeholder="matrix.org")#homeserver
#feedback #feedback
.form-input-container .form-input-container
input(type="submit" value="Log in")#submit 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
Review

This link doesn't go anywhere. I assume the site is yet to be created?

Can we make the link look like a link, aka underlined and blue?

This link doesn't go anywhere. I assume the site is yet to be created? Can we make the link look like a link, aka underlined and blue?
Review

Yeah right now we are waiting if someone in the matrix community will make a server picker similiar to joinmastodon.org.

https://github.com/daydream-mx/keymaker looks interesting but if it doesn't get finished we might have to make our own alternative

Yeah right now we are waiting if someone in the matrix community will make a server picker similiar to joinmastodon.org. https://github.com/daydream-mx/keymaker looks interesting but if it doesn't get finished we might have to make our own alternative

View file

@ -1,6 +1,7 @@
@use "./base" @use "./base"
@use "./loading.sass" @use "./loading"
@use "./colors.sass" as c @use "./colors" as c
@use "./tippy"
.main .main
@ -43,6 +44,39 @@
.form-error .form-error
color: red color: red
Review

This needs to be a lighter red for better contrast. Try #ff6561 I guess?

This needs to be a lighter red for better contrast. Try #ff6561 I guess?
Review

I think we should get a color scheme first and then replace it with css(or sass) variables

#27

I think we should get a color scheme first and then replace it with css(or sass) variables https://gitdab.com/cadence/Carbon/issues/27
.homeserver-question
font-size: 0.8em
bad marked this conversation as resolved
Review

This does nothing, remove it.

This does nothing, remove it.
.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)
100%
transform: rotate(180deg)
.loading-icon
display: inline-block
background-color: #ccc
width: 12px
height: 12px
margin-right: 6px
animation: spin 0.7s infinite
input, button input, button
font-family: inherit font-family: inherit
@ -67,6 +101,7 @@ button, input[type="submit"]
padding: 7px padding: 7px
&:hover &:hover
cursor: pointer
background-color: c.$milder background-color: c.$milder
label label

10
src/sass/tippy.sass Normal file
View file

@ -0,0 +1,10 @@
@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