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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
103
src/js/login.js
103
src/js/login.js
|
@ -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())
|
||||||
|
@ -117,16 +160,16 @@ class Form extends ElemJS {
|
||||||
//Protects from servers sending us on a redirect loop
|
//Protects from servers sending us on a redirect loop
|
||||||
maxDepth--
|
maxDepth--
|
||||||
if (maxDepth <= 0) throw new Error(`Failed to look up homeserver, maximum search depth reached`)
|
if (maxDepth <= 0) throw new Error(`Failed to look up homeserver, maximum search depth reached`)
|
||||||
|
|
||||||
//Normalise the address
|
//Normalise the address
|
||||||
if (!address.match(/^https?:\/\//)) {
|
if (!address.match(/^https?:\/\//)) {
|
||||||
console.warn(`${address} doesn't specify the protocol, assuming https`)
|
console.warn(`${address} doesn't specify the protocol, assuming https`)
|
||||||
address = "https://" + address
|
address = "https://" + address
|
||||||
}
|
}
|
||||||
address = address.replace(/\/*$/, "")
|
address = address.replace(/\/*$/, "")
|
||||||
|
|
||||||
this.status(`Looking up homeserver... trying ${address}`)
|
this.status(`Looking up homeserver... trying ${address}`)
|
||||||
|
|
||||||
// Check if we found the actual matrix server
|
// Check if we found the actual matrix server
|
||||||
try {
|
try {
|
||||||
const versionsReq = await fetch(`${address}/_matrix/client/versions`)
|
const versionsReq = await fetch(`${address}/_matrix/client/versions`)
|
||||||
|
@ -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
|
||||||
cadence
commented
Please create a class Please create a class `.homeserver-question` and target that instead. Using IDs in CSS sounds fine but quickly turns into hell. I know this from experience.
|
|||||||
|
font-size: 0.8em
|
||||||
cadence
commented
12px please, it doesn't need to be tiny. 12px please, it doesn't need to be _tiny._
bad
commented
Won't using px cause scaling issues on smaller screens? Won't using px cause scaling issues on smaller screens?
cadence
commented
No, px will not cause issues. No, px will not cause issues.
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
bad marked this conversation as resolved
Outdated
cadence
commented
style: indentation 2 spaces, line break above selectors. style: indentation 2 spaces, line break above selectors.
|
|||||||
|
.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