GetLink redesign

Also, try with entered username first (issue #524)
This commit is contained in:
N-Pex 2023-10-17 11:01:49 +02:00
parent 08d30e66f6
commit 7c202c2d2e
6 changed files with 145 additions and 89 deletions

View file

@ -6,7 +6,18 @@
.getlink-loggedin {
text-align: center;
button {
min-width: 200px !important;
min-width: 200px !important;
}
}
.getlink-image {
text-align: center;
max-width: 325px;
max-height: 257px;
width: 100%;
.v-icon__component {
width: unset;
height: unset;
}
}
@ -23,6 +34,19 @@
margin-top: 50px;
}
.getlink-info {
color: #000;
text-align: center;
font-feature-settings: "clig" off, "liga" off;
font-family: "Inter";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 117%; /* 18.72px */
letter-spacing: 0.4px;
margin: 15px 9px 40px 9px;
}
.getlink-subtitle {
color: #000;
text-align: center;

File diff suppressed because one or more lines are too long

View file

@ -189,9 +189,9 @@
},
"getlink": {
"title": "Get a Direct Link",
"password": "Set your password",
"password_repeat": "Confirm your password",
"create": "Create",
"info": "Direct links give people a secure line of communication with you. To start, choose a screen name to show when people enter a chat with you.",
"username": "Enter a screen name (ex: waku)",
"next": "Next",
"hello": "Hello {user},\nHeres your Direct Link",
"ready_to_share": "Its ready to share! A new direct room will open each time someone opens the link.",
"scan_title": "Scan this code to start a direct chat",

View file

@ -1,50 +1,26 @@
<template>
<div class="pa-4 getlink-root fill-height">
<div v-if="!loggedIn">
<div v-if="!loggedIn" class="text-center">
<v-icon class="getlink-image">$vuetify.icons.getlink</v-icon>
<div class="getlink-title">{{ $t("getlink.title") }}</div>
<div class="getlink-info">{{ $t("getlink.info") }}</div>
<div color="rgba(255,255,255,0.1)" class="text-center">
<v-form v-model="isValid">
<v-text-field v-model="user.user_id" :label="$t('login.username')" color="black" background-color="white" solo
<v-text-field v-model="user.user_id" :label="$t('getlink.username')" color="black" background-color="white" solo
:rules="[(v) => !!v || $t('login.username_required')]" :error="userErrorMessage != null"
:error-messages="userErrorMessage" required v-on:keyup.enter="onUsernameEnter" v-on:keydown="hasError = false"
v-on:blur="onUsernameBlur"></v-text-field>
<div class="error--text" v-if="loadingLoginFlows">Loading login flows...</div>
<v-text-field v-show="showPasswordFields" solo v-model="user.password"
:append-icon="showPassword1 ? 'visibility' : 'visibility_off'" :hint="$t('global.password_hint')"
:rules="[(v) => v ? !!v.match(passwordValidation) || $t('global.password_hint') : true]"
:label="$t('getlink.password')" counter="20" maxlength="20" :type="showPassword1 ? 'text' : 'password'"
@click:append="showPassword1 = !showPassword1" ref="password" :error="passErrorMessage != null"
:error-messages="passErrorMessage" required v-on:keydown="hasError = false"
v-on:keyup.enter="onPasswordEntered" v-on:blur="onPasswordEntered" />
<v-text-field v-show="showPasswordFields" solo v-model="passwordConfirm"
:append-icon="showPassword2 ? 'visibility' : 'visibility_off'"
:rules="[(v) => v === user.password || $t('global.password_didnot_match')]"
:label="$t('getlink.password_repeat')" counter="20" maxlength="20"
:type="showPassword2 ? 'text' : 'password'" @click:append="showPassword2 = !showPassword2"
ref="passwordConfirm" :error="passErrorMessage != null" :error-messages="passErrorMessage" required
v-on:keydown="hasError = false" v-on:keyup.enter="() => {
if (isValid && !loading) {
handleLogin();
}
}
" />
<div class="error--text" v-if="hasError">{{ this.message }}</div>
<interactive-auth ref="interactiveAuth" />
<!--
<v-checkbox
id="chk-remember-me"
class="mt-0"
v-model="rememberMe"
@change="onRememberMe"
:label="$t('join.remember_me')"
/>
-->
<v-btn id="btn-login" :disabled="!isValid || !passwordsSetAndMatching || loading || loadingLoginFlows" color="primary" depressed block
@click.stop="handleLogin" :loading="loading" class="filled-button mt-4">{{ $t("getlink.create") }}</v-btn>
<v-btn id="btn-login" :disabled="!isValid || loading || loadingLoginFlows" color="primary" depressed block
@click.stop="handleLogin" :loading="loading" class="filled-button mt-4">{{ $t("getlink.next") }}</v-btn>
<v-btn color="black" depressed text block @click.stop="goToLoginPage" class="text-button">{{ $t("menu.login")
}}</v-btn>
</v-form>
</div>
</div>
@ -59,7 +35,8 @@
</copy-link>
<div class="getlink-buttons">
<v-btn color="black" depressed @click.stop="goHome" class="outlined-button">{{ $t("getlink.continue") }}</v-btn>
<v-btn color="black" depressed text block @click.stop="getDifferentLink" class="text-button">{{ $t("getlink.different_link") }}</v-btn>
<v-btn color="black" depressed text block @click.stop="getDifferentLink" class="text-button">{{
$t("getlink.different_link") }}</v-btn>
</div>
</div>
<div :class="{ 'toast-at-bottom': true, 'visible': showQRCopiedToast }">{{ $t("getlink.qr_image_copied") }}</div>
@ -98,40 +75,30 @@ export default {
shareSupported() {
return !!navigator.share;
},
passwordsSetAndMatching() {
return this.user.password.match(this.passwordValidation) && this.user.password == this.passwordConfirm;
}
},
watch: {
user: {
handler() {
// Reset manual errors
this.userErrorMessage = null;
this.passErrorMessage = null;
},
deep: true,
}
},
methods: {
defaultData() {
return {
user: new User(this.$config.defaultServer, "", ""),
isValid: false,
loading: false,
message: "",
userErrorMessage: null,
passErrorMessage: null,
hasError: false,
currentLoginServer: "",
loadingLoginFlows: false,
loginFlows: null,
showPasswordFields: true,
passwordConfirm: "",
showPassword1: false,
showPassword2: false,
passwordValidation: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{12,20}$/,
showQRCopiedToast: false
};
return {
user: new User(this.$config.defaultServer, "", utils.randomPass()),
isValid: false,
loading: false,
message: "",
userErrorMessage: null,
hasError: false,
currentLoginServer: "",
loadingLoginFlows: false,
loginFlows: null,
showQRCopiedToast: false
};
},
goHome() {
this.$navigation.push({ name: "Home" }, -1);
@ -144,6 +111,9 @@ return {
Object.keys(obj).forEach(k => this[k] = obj[k]);
})
},
goToLoginPage() {
this.$navigation.push({ name: "Login", params: { showCreateRoomOption: false, redirect: "GetLink" } }, 1);
},
handleLogin() {
if (this.user.user_id && this.user.password) {
// Reset errors
@ -153,18 +123,27 @@ return {
const userDisplayName = this.user.user_id;
var user = Object.assign({}, this.user);
user.user_id = utils.randomUser(this.$config.userIdPrefix);
let prefix = userDisplayName.toLowerCase().replaceAll(" ", "-").replaceAll(utils.invalidUserIdChars(), "");
if (prefix.length == 0) {
prefix = this.$config.userIdPrefix;
user.user_id = utils.randomUser(prefix);
} else {
// We first try with a username that is just a processed version of the display name.
// If it is already taken, try again with random characters appended.
user.user_id = prefix;
prefix = prefix + "-";
}
user.normalize();
this.loading = true;
this.$store.dispatch("createUser", { user, registrationFlowHandler: this.$refs.interactiveAuth.registrationFlowHandler }).then(
(ignoreduser) => {
this.$matrix.setUserDisplayName(userDisplayName);
this.loading = false;
},
(error) => {
console.error(error);
this.loading = false;
this.message =
(error.data && error.data.error) ||
error.message ||
@ -172,37 +151,42 @@ return {
if (error.data && error.data.errcode === 'M_FORBIDDEN') {
this.message = this.$i18n.messages[this.$i18n.locale].login.invalid_message;
this.hasError = true;
} else if (error.data && error.data.errcode === 'M_USER_IN_USE') {
// Try again with (other/new) random chars appended
user.user_id = utils.randomUser(prefix);
return this.$store.dispatch("createUser", { user, registrationFlowHandler: this.$refs.interactiveAuth.registrationFlowHandler }).then(
(ignoreduser) => {
this.$matrix.setUserDisplayName(userDisplayName);
},
(error) => {
this.message =
(error.data && error.data.error) ||
error.message ||
error.toString();
if (error.data && error.data.errcode === 'M_FORBIDDEN') {
this.message = this.$i18n.messages[this.$i18n.locale].login.invalid_message;
this.hasError = true;
}
}
);
}
console.log("Message set to ", this.message);
}
);
})
.finally(() => {
this.loading = false;
})
}
},
handleCreateRoom() {
this.$navigation.push({ name: "CreateRoom" });
},
onUsernameEnter() {
this.$refs.password.focus();
this.onUsernameBlur();
},
validPassword(pass) {
return !!pass;
},
onPasswordEntered() {
if (this.validPassword(this.user.password)) {
this.$nextTick(() => {
this.$refs.passwordConfirm.focus();
});
}
},
onUsernameBlur() {
var user = Object.assign({}, this.user);
user.normalize();
const server = user.home_server || this.$config.defaultServer;
if (server !== this.currentLoginServer) {
this.showPasswordFields = false;
this.currentLoginServer = server;
this.loadingLoginFlows = true;
@ -217,12 +201,6 @@ return {
} else {
this.message = "";
this.hasError = false;
this.showPasswordFields = this.loginFlows.some(f => f.type == "m.login.password");
if (this.showPasswordFields) {
this.$nextTick(() => {
this.$refs.password.focus();
});
}
}
});
}

View file

@ -86,7 +86,7 @@
class="filled-button mt-4"
>{{ $t("login.login") }}</v-btn
>
<div class="mt-2 overline">{{ $t("login.or") }}</div>
<div class="mt-2 overline" v-if="showCreateRoomOption">{{ $t("login.or") }}</div>
<v-btn
id="btn-create-room"
color="primary"
@ -94,6 +94,7 @@
block
@click.stop="handleCreateRoom"
class="filled-button mt-2"
v-if="showCreateRoomOption"
>{{ $t("login.create_room") }}</v-btn
>
</v-form>
@ -111,6 +112,20 @@ import logoMixin from "./logoMixin";
export default {
name: "Login",
mixins:[rememberMeMixin, logoMixin],
props: {
showCreateRoomOption: {
type: Boolean,
default: function () {
return true;
},
},
redirect: {
type: String,
default: function() {
return null;
},
}
},
data() {
return {
user: new User(this.$config.defaultServer, "", ""),
@ -171,7 +186,10 @@ export default {
this.loading = true;
this.$store.dispatch("login", { user }).then(
() => {
if (this.$matrix.currentRoomId) {
if (this.redirect) {
this.$navigation.push({ name: this.redirect }, -1);
}
else if (this.$matrix.currentRoomId) {
this.$navigation.push(
{
name: "Chat",

View file

@ -580,6 +580,10 @@ class Util {
return null;
}
invalidUserIdChars() {
return /[^0-9a-z-.=_/]+/g;
}
getFirstVisibleElement(parentNode, where) {
let visible = this.findVisibleElements(parentNode);
if (visible) {