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 { .getlink-loggedin {
text-align: center; text-align: center;
button { 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; 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 { .getlink-subtitle {
color: #000; color: #000;
text-align: center; text-align: center;

File diff suppressed because one or more lines are too long

View file

@ -189,9 +189,9 @@
}, },
"getlink": { "getlink": {
"title": "Get a Direct Link", "title": "Get a Direct Link",
"password": "Set your password", "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.",
"password_repeat": "Confirm your password", "username": "Enter a screen name (ex: waku)",
"create": "Create", "next": "Next",
"hello": "Hello {user},\nHeres your Direct Link", "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.", "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", "scan_title": "Scan this code to start a direct chat",

View file

@ -1,50 +1,26 @@
<template> <template>
<div class="pa-4 getlink-root fill-height"> <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-title">{{ $t("getlink.title") }}</div>
<div class="getlink-info">{{ $t("getlink.info") }}</div>
<div color="rgba(255,255,255,0.1)" class="text-center"> <div color="rgba(255,255,255,0.1)" class="text-center">
<v-form v-model="isValid"> <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" :rules="[(v) => !!v || $t('login.username_required')]" :error="userErrorMessage != null"
:error-messages="userErrorMessage" required v-on:keyup.enter="onUsernameEnter" v-on:keydown="hasError = false" :error-messages="userErrorMessage" required v-on:keyup.enter="onUsernameEnter" v-on:keydown="hasError = false"
v-on:blur="onUsernameBlur"></v-text-field> v-on:blur="onUsernameBlur"></v-text-field>
<div class="error--text" v-if="loadingLoginFlows">Loading login flows...</div> <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> <div class="error--text" v-if="hasError">{{ this.message }}</div>
<interactive-auth ref="interactiveAuth" /> <interactive-auth ref="interactiveAuth" />
<!--
<v-checkbox <v-btn id="btn-login" :disabled="!isValid || loading || loadingLoginFlows" color="primary" depressed block
id="chk-remember-me" @click.stop="handleLogin" :loading="loading" class="filled-button mt-4">{{ $t("getlink.next") }}</v-btn>
class="mt-0" <v-btn color="black" depressed text block @click.stop="goToLoginPage" class="text-button">{{ $t("menu.login")
v-model="rememberMe" }}</v-btn>
@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-form> </v-form>
</div> </div>
</div> </div>
@ -59,7 +35,8 @@
</copy-link> </copy-link>
<div class="getlink-buttons"> <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 @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> </div>
<div :class="{ 'toast-at-bottom': true, 'visible': showQRCopiedToast }">{{ $t("getlink.qr_image_copied") }}</div> <div :class="{ 'toast-at-bottom': true, 'visible': showQRCopiedToast }">{{ $t("getlink.qr_image_copied") }}</div>
@ -98,40 +75,30 @@ export default {
shareSupported() { shareSupported() {
return !!navigator.share; return !!navigator.share;
}, },
passwordsSetAndMatching() {
return this.user.password.match(this.passwordValidation) && this.user.password == this.passwordConfirm;
}
}, },
watch: { watch: {
user: { user: {
handler() { handler() {
// Reset manual errors // Reset manual errors
this.userErrorMessage = null; this.userErrorMessage = null;
this.passErrorMessage = null;
}, },
deep: true, deep: true,
} }
}, },
methods: { methods: {
defaultData() { defaultData() {
return { return {
user: new User(this.$config.defaultServer, "", ""), user: new User(this.$config.defaultServer, "", utils.randomPass()),
isValid: false, isValid: false,
loading: false, loading: false,
message: "", message: "",
userErrorMessage: null, userErrorMessage: null,
passErrorMessage: null, hasError: false,
hasError: false, currentLoginServer: "",
currentLoginServer: "", loadingLoginFlows: false,
loadingLoginFlows: false, loginFlows: null,
loginFlows: null, showQRCopiedToast: false
showPasswordFields: true, };
passwordConfirm: "",
showPassword1: false,
showPassword2: false,
passwordValidation: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{12,20}$/,
showQRCopiedToast: false
};
}, },
goHome() { goHome() {
this.$navigation.push({ name: "Home" }, -1); this.$navigation.push({ name: "Home" }, -1);
@ -144,6 +111,9 @@ return {
Object.keys(obj).forEach(k => this[k] = obj[k]); Object.keys(obj).forEach(k => this[k] = obj[k]);
}) })
}, },
goToLoginPage() {
this.$navigation.push({ name: "Login", params: { showCreateRoomOption: false, redirect: "GetLink" } }, 1);
},
handleLogin() { handleLogin() {
if (this.user.user_id && this.user.password) { if (this.user.user_id && this.user.password) {
// Reset errors // Reset errors
@ -153,18 +123,27 @@ return {
const userDisplayName = this.user.user_id; const userDisplayName = this.user.user_id;
var user = Object.assign({}, this.user); 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(); user.normalize();
this.loading = true; this.loading = true;
this.$store.dispatch("createUser", { user, registrationFlowHandler: this.$refs.interactiveAuth.registrationFlowHandler }).then( this.$store.dispatch("createUser", { user, registrationFlowHandler: this.$refs.interactiveAuth.registrationFlowHandler }).then(
(ignoreduser) => { (ignoreduser) => {
this.$matrix.setUserDisplayName(userDisplayName); this.$matrix.setUserDisplayName(userDisplayName);
this.loading = false;
}, },
(error) => { (error) => {
console.error(error);
this.loading = false;
this.message = this.message =
(error.data && error.data.error) || (error.data && error.data.error) ||
error.message || error.message ||
@ -172,37 +151,42 @@ return {
if (error.data && error.data.errcode === 'M_FORBIDDEN') { if (error.data && error.data.errcode === 'M_FORBIDDEN') {
this.message = this.$i18n.messages[this.$i18n.locale].login.invalid_message; this.message = this.$i18n.messages[this.$i18n.locale].login.invalid_message;
this.hasError = true; 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() { handleCreateRoom() {
this.$navigation.push({ name: "CreateRoom" }); this.$navigation.push({ name: "CreateRoom" });
}, },
onUsernameEnter() { onUsernameEnter() {
this.$refs.password.focus();
this.onUsernameBlur(); this.onUsernameBlur();
}, },
validPassword(pass) {
return !!pass;
},
onPasswordEntered() {
if (this.validPassword(this.user.password)) {
this.$nextTick(() => {
this.$refs.passwordConfirm.focus();
});
}
},
onUsernameBlur() { onUsernameBlur() {
var user = Object.assign({}, this.user); var user = Object.assign({}, this.user);
user.normalize(); user.normalize();
const server = user.home_server || this.$config.defaultServer; const server = user.home_server || this.$config.defaultServer;
if (server !== this.currentLoginServer) { if (server !== this.currentLoginServer) {
this.showPasswordFields = false;
this.currentLoginServer = server; this.currentLoginServer = server;
this.loadingLoginFlows = true; this.loadingLoginFlows = true;
@ -217,12 +201,6 @@ return {
} else { } else {
this.message = ""; this.message = "";
this.hasError = false; 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" class="filled-button mt-4"
>{{ $t("login.login") }}</v-btn >{{ $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 <v-btn
id="btn-create-room" id="btn-create-room"
color="primary" color="primary"
@ -94,6 +94,7 @@
block block
@click.stop="handleCreateRoom" @click.stop="handleCreateRoom"
class="filled-button mt-2" class="filled-button mt-2"
v-if="showCreateRoomOption"
>{{ $t("login.create_room") }}</v-btn >{{ $t("login.create_room") }}</v-btn
> >
</v-form> </v-form>
@ -111,6 +112,20 @@ import logoMixin from "./logoMixin";
export default { export default {
name: "Login", name: "Login",
mixins:[rememberMeMixin, logoMixin], mixins:[rememberMeMixin, logoMixin],
props: {
showCreateRoomOption: {
type: Boolean,
default: function () {
return true;
},
},
redirect: {
type: String,
default: function() {
return null;
},
}
},
data() { data() {
return { return {
user: new User(this.$config.defaultServer, "", ""), user: new User(this.$config.defaultServer, "", ""),
@ -171,7 +186,10 @@ export default {
this.loading = true; this.loading = true;
this.$store.dispatch("login", { user }).then( 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( this.$navigation.push(
{ {
name: "Chat", name: "Chat",

View file

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