Get dm link

This commit is contained in:
N Pex 2023-09-29 15:13:12 +00:00
parent 0795c25654
commit 00f95adb09
14 changed files with 422 additions and 26 deletions

View file

@ -78,7 +78,7 @@
<v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon>
</div>
<div v-if="showReadOnlyToast" class="toast-read-only">{{ $t("message.not_allowed_to_send") }}</div>
<div v-if="showReadOnlyToast" class="toast-at-bottom visible">{{ $t("message.not_allowed_to_send") }}</div>
</div>
</template>

View file

@ -7,7 +7,12 @@
<v-row cols="12" class="qr-container ma-3">
<v-col cols="auto">
<canvas
@click.stop="showFullScreenQR = true"
v-longTap:250="[
()=> {
showFullScreenQR = true;
},
(el) => { $emit('long-tap', el); }
]"
ref="roomQr"
class="qr"
id="room-qr"
@ -32,7 +37,7 @@
</v-container>
</v-card>
</v-expand-transition>
<QRCodePopup :show="showFullScreenQR" :message="locationLink" @close="showFullScreenQR = false" />
<QRCodePopup :show="showFullScreenQR" :message="locationLink" :title="popupTitle" @close="showFullScreenQR = false" />
</div>
</template>
@ -51,7 +56,12 @@ export default {
i18nCopyLinkKey: {
type: String,
default: 'copy_link'
},
i18nQrPopupTitleKey: {
type: String,
default: 'room_info.scan_code'
}
},
data() {
return {
@ -59,6 +69,11 @@ export default {
showFullScreenQR: false,
}
},
computed: {
popupTitle() {
return this.$t(this.i18nQrPopupTitleKey);
}
},
methods: {
copyRoomLink() {
if(this.locationLinkCopied) return
@ -87,7 +102,7 @@ export default {
{
type: "image/png",
margin: 1,
width: 60,
width: canvas.getBoundingClientRect().width,
},
function (error) {
if (error) console.error(error);

254
src/components/GetLink.vue Normal file
View file

@ -0,0 +1,254 @@
<template>
<div class="pa-4 getlink-root fill-height">
<div v-if="!loggedIn">
<div class="getlink-title">{{ $t("getlink.title") }}</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
: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="showPasswordField" 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="showPasswordConfirmField" 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-form>
</div>
</div>
<div v-else style="position:relative" class="getlink-loggedin">
<!-- Logged in/account created -->
<div class="getlink-title">{{ $t("getlink.hello", { user: $matrix.currentUserDisplayName }) }}</div>
<div class="getlink-subtitle">{{ $t("getlink.ready_to_share") }}</div>
<copy-link ref="qr" :locationLink="directMessageLink" i18nQrPopupTitleKey="getlink.scan_title"
v-on:long-tap="copyQRImage">
<v-img v-if="shareSupported" class="clickable getlink-share" src="@/assets/icons/ic_share.svg"
@click="shareLink" />
</copy-link>
<v-btn color="black" depressed @click.stop="goHome" class="outlined-button">{{ $t("getlink.continue") }}</v-btn>
</div>
<div :class="{ 'toast-at-bottom': true, 'visible': showQRCopiedToast }">{{ $t("getlink.qr_image_copied") }}</div>
</div>
</template>
<script>
import User from "../models/user";
import rememberMeMixin from "./rememberMeMixin";
import * as sdk from "matrix-js-sdk";
import logoMixin from "./logoMixin";
import InteractiveAuth from './InteractiveAuth.vue';
import CopyLink from "./CopyLink.vue"
import utils from "../plugins/utils";
export default {
name: "GetLink",
mixins: [rememberMeMixin, logoMixin],
components: { InteractiveAuth, CopyLink },
data() {
return {
user: new User(this.$config.defaultServer, "", ""),
isValid: false,
loading: false,
message: "",
userErrorMessage: null,
passErrorMessage: null,
hasError: false,
currentLoginServer: "",
loadingLoginFlows: false,
loginFlows: null,
showPasswordField: false,
showPasswordConfirmField: false,
passwordConfirm: "",
showPassword1: false,
showPassword2: false,
passwordValidation: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{12,20}$/,
showQRCopiedToast: false
};
},
computed: {
loggedIn() {
return this.$store.state.auth.status.loggedIn;
},
currentUser() {
return this.$store.state.auth.user;
},
showCloseButton() {
return this.$navigation && this.$navigation.canPop();
},
directMessageLink() {
return `${window.location.origin + window.location.pathname}#/user/${encodeURIComponent(this.$matrix.currentUser.user_id)}`
},
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: {
goHome() {
this.$navigation.push({ name: "Home" }, -1);
},
handleLogin() {
if (this.user.user_id && this.user.password) {
// Reset errors
this.message = null;
// Is it a full matrix user id? Modify a copy, so that the UI will still show the full ID.
const userDisplayName = this.user.user_id;
var user = Object.assign({}, this.user);
user.user_id = utils.randomUser(this.$config.userIdPrefix);
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 ||
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);
}
);
}
},
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.showPasswordConfirmField = true;
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.showPasswordField = false;
this.showPasswordConfirmField = false;
this.currentLoginServer = server;
this.loadingLoginFlows = true;
const matrixClient = sdk.createClient({ baseUrl: server });
matrixClient.loginFlows().then((response) => {
console.log("FLOWS", response.flows);
this.loginFlows = response.flows.filter(this.supportedLoginFlow);
this.loadingLoginFlows = false;
if (this.loginFlows.length == 0) {
this.message = this.$t('login.no_supported_flow')
this.hasError = true;
} else {
this.message = "";
this.hasError = false;
this.showPasswordField = this.loginFlows.some(f => f.type == "m.login.password");
if (this.showPasswordField) {
this.$nextTick(() => {
this.$refs.password.focus();
});
}
}
});
}
},
supportedLoginFlow(flow) {
return ["m.login.password"].includes(flow.type);
},
shareLink() {
const canvas = this.$refs.qr.$refs.roomQr;
if (canvas) {
canvas.toBlob((blob) => {
let file = new File([blob], encodeURIComponent(this.$matrix.currentUserDisplayName || User.localPart(this.currentUser.user_id)) + ".png", { type: "image/png" })
const shareData = { files: [file] };
if (navigator.canShare && navigator.canShare(shareData)) {
navigator.share(shareData);
}
}, 'image/png');
}
},
copyQRImage(canvas) {
if (canvas && typeof window.ClipboardItem !== "undefined" && navigator.clipboard) {
canvas.toBlob(blob => {
const clipboardItemInput = new window.ClipboardItem({ "image/png": blob });
navigator.clipboard.write([clipboardItemInput]).then(() => {
this.showQRCopiedToast = true;
setTimeout(() => {
this.showQRCopiedToast = false;
}, 3000);
}).catch(err => console.error(err));
});
}
}
},
};
</script>
<style lang="scss">
@import "@/assets/css/getlink.scss";
</style>

View file

@ -14,7 +14,7 @@
<v-card-text class="pa-0">
<RoomList
showInvites
showCreate
:showCreate="!$config.hide_add_room_on_home"
v-on:newroom="createRoom"
/>
</v-card-text>

View file

@ -8,7 +8,7 @@
<div class="d-flex justify-center">
<canvas ref="qr" class="qr" id="qr" :style="qrStyle"></canvas>
</div>
<div>{{ $t("room_info.scan_code") }}</div>
<div>{{ title }}</div>
</div>
</v-dialog>
</template>
@ -32,6 +32,12 @@ export default {
return null;
},
},
title: {
type: String,
default: function () {
return "";
},
},
},
data() {
return {