WIP: Some visual improvements for login #20
7 changed files with 191 additions and 33 deletions
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
38
src/js/re.js
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|||||||
|
|
||||||
|
|
|
@ -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
|
||||||
cadence
commented
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?
bad
commented
I think we should get a color scheme first and then replace it with css(or sass) variables 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
cadence
commented
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
10
src/sass/tippy.sass
Normal 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
|
Loading…
Reference in a new issue
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?
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