Merge branch 'dev'
1
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
|
|
||||||
13
README.md
|
|
@ -59,5 +59,18 @@ The app loads runtime configutation from the server at "./config.json" and merge
|
||||||
* Insert the resulting config blob into the "shortCodeStickers" value of the config file (assets/config.json)
|
* Insert the resulting config blob into the "shortCodeStickers" value of the config file (assets/config.json)
|
||||||
* Rearrange order of sticker packs by editing the config blob above.
|
* Rearrange order of sticker packs by editing the config blob above.
|
||||||
|
|
||||||
|
### Chat backgrounds
|
||||||
|
Chat backgrounds can be set using the **chat_backgrounds** config value. It can be set per room type, "direct", "invite" and "public". If no background is set for the current room type, the app will also check if a default "all" value has any backgrounds specified.
|
||||||
|
Backgrounds are entered as an array. Which of the backgrounds in the array is used for a given room is calculated from the room ID, so that it is constant across the lifetime of the room.
|
||||||
|
|
||||||
|
```
|
||||||
|
chat_backgrounds: {
|
||||||
|
"direct": ["https://example.com/dm1.png", "data:image/png;base64,yadiyada..."],
|
||||||
|
"all": ["/default_background.png"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Attributions
|
### Attributions
|
||||||
Sounds from [Notification Sounds](https://notificationsounds.com)
|
Sounds from [Notification Sounds](https://notificationsounds.com)
|
||||||
BIN
public/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
public/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
public/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -1,17 +1,52 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico" />
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-72x72.png" sizes="72x72" />
|
||||||
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-96x96.png" sizes="96x96" />
|
||||||
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-128x128.png" sizes="128x128" />
|
||||||
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-144x144.png" sizes="144x144" />
|
||||||
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-152x152.png" sizes="152x152" />
|
||||||
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-192x192.png" sizes="192x192" />
|
||||||
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-384x384.png" sizes="384x384" />
|
||||||
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-512x512.png" sizes="512x512" />
|
||||||
|
<link rel="manifest" href="<%= BASE_URL %>manifest.json" />
|
||||||
|
<script src="./lottie-player.js"></script>
|
||||||
|
<style>
|
||||||
|
#loader {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<strong
|
||||||
|
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
||||||
|
enable it to continue.</strong
|
||||||
|
>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app">
|
||||||
|
<!-- To get rid of blank white screen, show loading indicator before Vue app is initialized -->
|
||||||
|
<!-- This loader will be replaced by the Vue component on mounted -->
|
||||||
|
<div id="loader">
|
||||||
|
<lottie-player autoplay loop mode="normal" src="./loader.json" style="width: 128px"> </lottie-player>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
1
public/loader.json
Normal file
77
public/lottie-player.js
Normal file
52
public/manifest.json
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"id": "/",
|
||||||
|
"start_url": "/",
|
||||||
|
"name": "Convene - Chat for everyone ",
|
||||||
|
"short_name": "Convene",
|
||||||
|
"theme_color": "#FFFFFF",
|
||||||
|
"background_color": "#FFFFFF",
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-128x128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-152x152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"splash_pages": null
|
||||||
|
}
|
||||||
22
public/sw.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Notification click event listener
|
||||||
|
self.addEventListener("notificationclick", (e) => {
|
||||||
|
// Close the notification popout
|
||||||
|
e.notification.close();
|
||||||
|
// Get all the Window clients
|
||||||
|
e.waitUntil(
|
||||||
|
clients.matchAll({ type: "window" }).then((clientsArr) => {
|
||||||
|
// If a Window tab matching the targeted URL already exists, focus that;
|
||||||
|
const hadWindowToFocus = clientsArr.some((windowClient) =>
|
||||||
|
windowClient.url === e.notification.data.url
|
||||||
|
? (windowClient.focus(), true)
|
||||||
|
: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Otherwise, open a new tab to the applicable URL and focus it.
|
||||||
|
if (!hadWindowToFocus)
|
||||||
|
clients
|
||||||
|
.openWindow(e.notification.data.url)
|
||||||
|
.then((windowClient) => (windowClient ? windowClient.focus() : null));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
68
src/App.vue
|
|
@ -7,7 +7,7 @@
|
||||||
<v-container
|
<v-container
|
||||||
fluid
|
fluid
|
||||||
fill-height
|
fill-height
|
||||||
v-if="loading"
|
v-if="showLoadingScreen"
|
||||||
class="loading-container"
|
class="loading-container"
|
||||||
>
|
>
|
||||||
<v-row align="center" justify="center">
|
<v-row align="center" justify="center">
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<v-skeleton-loader
|
<v-skeleton-loader
|
||||||
type="list-item-avatar-two-line, divider, list-item-three-line, card-heading"
|
type="list-item-avatar-two-line, divider, list-item-three-line, card-heading"
|
||||||
v-if="loading"
|
v-if="showLoadingScreen"
|
||||||
></v-skeleton-loader>
|
></v-skeleton-loader>
|
||||||
</v-main>
|
</v-main>
|
||||||
</v-app>
|
</v-app>
|
||||||
|
|
@ -31,10 +31,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import stickers from "./plugins/stickers";
|
import stickers from "./plugins/stickers";
|
||||||
|
import { registerServiceWorker, notificationCount, windowNotificationPermission } from "./plugins/notificationAndServiceWorker.js"
|
||||||
import logoMixin from "./components/logoMixin";
|
import logoMixin from "./components/logoMixin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
|
mixins: [logoMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|
@ -42,11 +44,11 @@ export default {
|
||||||
availableJsonTranslation: null
|
availableJsonTranslation: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mixins: [logoMixin],
|
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.setDefaultLanguage();
|
this.setDefaultLanguage();
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
registerServiceWorker();
|
||||||
/**
|
/**
|
||||||
if (
|
if (
|
||||||
window.location.protocol == "http" &&
|
window.location.protocol == "http" &&
|
||||||
|
|
@ -82,6 +84,7 @@ export default {
|
||||||
this.$config.promise.then(this.onConfigLoaded);
|
this.$config.promise.then(this.onConfigLoaded);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
windowNotificationPermission,
|
||||||
onConfigLoaded(config) {
|
onConfigLoaded(config) {
|
||||||
if (config.shortCodeStickers) {
|
if (config.shortCodeStickers) {
|
||||||
stickers.loadStickersFromConfig(config);
|
stickers.loadStickersFromConfig(config);
|
||||||
|
|
@ -115,41 +118,24 @@ export default {
|
||||||
this.$i18n.locale = this.$store.state.language || "en";
|
this.$i18n.locale = this.$store.state.language || "en";
|
||||||
},
|
},
|
||||||
showNotification() {
|
showNotification() {
|
||||||
if(document.visibilityState === "visible") {
|
if(document.visibilityState === "hidden") {
|
||||||
return;
|
const title = this.$t('notification.title');
|
||||||
}
|
const self = this;
|
||||||
const title = this.$t('notification.title');
|
|
||||||
const notification = new Notification(title, {icon: this.logotype});
|
navigator.serviceWorker.ready.then(function(registration) {
|
||||||
notification.onclick = () => {
|
registration.showNotification(title, {
|
||||||
notification.close();
|
icon: self.logotype,
|
||||||
window.parent.focus();
|
data: { url: window.location.href }
|
||||||
}
|
});
|
||||||
},
|
|
||||||
requestAndShowPermission(notificationCount) {
|
|
||||||
Notification.requestPermission(function (permission) {
|
|
||||||
if(notificationCount > 0 && permission === "granted") {
|
|
||||||
this.showNotification();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
requestNotificationPermission(notificationCount) {
|
|
||||||
if ('Notification' in window) {
|
|
||||||
Notification.requestPermission().then((permission) => {
|
|
||||||
if(notificationCount > 0 && permission === 'granted') {
|
|
||||||
this.showNotification();
|
|
||||||
} else if(permission === "default") {
|
|
||||||
this.requestAndShowPermission(notificationCount);
|
|
||||||
} else {
|
|
||||||
this.requestAndShowPermission(notificationCount);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
notificationCount() {
|
showLoadingScreen() {
|
||||||
return this.$matrix.notificationCount
|
return this.loading || !(this.$config.loaded);
|
||||||
},
|
},
|
||||||
|
notificationCount,
|
||||||
currentUser() {
|
currentUser() {
|
||||||
return this.$store.state.auth.user;
|
return this.$store.state.auth.user;
|
||||||
},
|
},
|
||||||
|
|
@ -191,6 +177,15 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
notificationCount: {
|
||||||
|
handler(nCount) {
|
||||||
|
// windowNotificationPermission
|
||||||
|
// return value: 'granted', 'default', 'denied'
|
||||||
|
if (nCount > 0 && this.windowNotificationPermission() === "granted") {
|
||||||
|
this.showNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"$i18n.locale": {
|
"$i18n.locale": {
|
||||||
handler(val) {
|
handler(val) {
|
||||||
// Locale changed, check file if RTL
|
// Locale changed, check file if RTL
|
||||||
|
|
@ -217,13 +212,8 @@ export default {
|
||||||
document.getElementById("favicon").setAttribute('href', favicon);
|
document.getElementById("favicon").setAttribute('href', favicon);
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
|
||||||
notificationCount: {
|
|
||||||
handler(nCount) {
|
|
||||||
this.requestNotificationPermission(nCount)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,5 +48,6 @@
|
||||||
"experimental_file_mode": true,
|
"experimental_file_mode": true,
|
||||||
"experimental_read_only_room": true,
|
"experimental_read_only_room": true,
|
||||||
"experimental_public_room": true,
|
"experimental_public_room": true,
|
||||||
"show_status_messages": "never"
|
"show_status_messages": "never",
|
||||||
|
"hide_add_room_on_home": false
|
||||||
}
|
}
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.h-100 {
|
.h-100 {
|
||||||
width: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.white-space-pre {
|
.white-space-pre {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
@ -37,4 +37,4 @@
|
||||||
}
|
}
|
||||||
.box-shadow-none {
|
.box-shadow-none {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ $admin-fg: white;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
--v-app-background: $app-background;
|
--v-app-background: $app-background;
|
||||||
--v-background-color: white;
|
--v-background-color: rgba(255, 255, 255, 0.8);
|
||||||
--v-foreground-color: black;
|
--v-foreground-color: black;
|
||||||
--v-secondary-color: #242424;
|
--v-secondary-color: #242424;
|
||||||
--v-divider-color: #eeeeee;
|
--v-divider-color: #eeeeee;
|
||||||
|
|
@ -86,9 +86,8 @@ body {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 11 * $chat-text-size;
|
font-size: 11 * $chat-text-size;
|
||||||
color: var(--v-foreground-color);
|
color: white;
|
||||||
background-color: var(--v-background-color) !important;
|
background-color: red !important;
|
||||||
border: 1px solid var(--v-foreground-color);
|
|
||||||
border-radius: $chat-standard-padding / 2;
|
border-radius: $chat-standard-padding / 2;
|
||||||
height: $chat-standard-padding;
|
height: $chat-standard-padding;
|
||||||
margin-top: $chat-standard-padding-xs;
|
margin-top: $chat-standard-padding-xs;
|
||||||
|
|
@ -99,6 +98,26 @@ body {
|
||||||
color: var(--v-secondary-color);
|
color: var(--v-secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leave-button {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11.54 * $chat-text-size;
|
||||||
|
line-height: 140%;
|
||||||
|
color: white !important;
|
||||||
|
background-color: #ff3300 !important;
|
||||||
|
border-radius: $small-button-height / 2;
|
||||||
|
min-height: 0;
|
||||||
|
height: $small-button-height !important;
|
||||||
|
margin-top: $chat-standard-padding-xs;
|
||||||
|
margin-bottom: $chat-standard-padding-xs;
|
||||||
|
padding-left: $chat-standard-padding-s !important;
|
||||||
|
padding-right: $chat-standard-padding-s !important;
|
||||||
|
width: auto !important;
|
||||||
|
.v-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.icon-dropdown {
|
.icon-dropdown {
|
||||||
margin: 0px 8px;
|
margin: 0px 8px;
|
||||||
}
|
}
|
||||||
|
|
@ -209,25 +228,20 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||||
margin-top: 72px;
|
//margin-top: 72px;
|
||||||
margin-bottom: 70px;
|
margin-bottom: 70px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-area {
|
|
||||||
background-color: #e2e2e2;
|
|
||||||
margin: 0;
|
|
||||||
padding-left: $chat-standard-padding-s;
|
|
||||||
padding-right: $chat-standard-padding-s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-area-outer {
|
.input-area-outer {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #ffffff;
|
background-color: var(--v-background-color);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-bottom: -10px;
|
||||||
padding-left: 2 * $chat-standard-padding-s;
|
padding-left: 2 * $chat-standard-padding-s;
|
||||||
padding-right: 2 * $chat-standard-padding-s;
|
padding-right: 2 * $chat-standard-padding-s;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
&.reply-to {
|
&.reply-to {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -264,9 +278,9 @@ body {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #d4d4d4;
|
border: 1px solid #d4d4d4;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
|
margin-bottom: 10px;
|
||||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.input-area-button {
|
.input-area-button {
|
||||||
|
|
@ -336,9 +350,9 @@ body {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top !important;
|
vertical-align: top !important;
|
||||||
.v-icon {
|
.v-icon {
|
||||||
color: #eeeeee;
|
color: #595959;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #888888;
|
color: #000000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -404,7 +418,7 @@ body {
|
||||||
}
|
}
|
||||||
position: relative;
|
position: relative;
|
||||||
.bubble {
|
.bubble {
|
||||||
background-color: #ededed;
|
background-color: rgba(#ededed,0.8);
|
||||||
border-radius: 0px 10px 10px 10px;
|
border-radius: 0px 10px 10px 10px;
|
||||||
[dir="rtl"] & {
|
[dir="rtl"] & {
|
||||||
border-radius: 10px 0px 10px 0px;
|
border-radius: 10px 0px 10px 0px;
|
||||||
|
|
@ -418,7 +432,7 @@ body {
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
&.from-admin .bubble {
|
&.from-admin .bubble {
|
||||||
background-color: $admin-bg;
|
background-color: rgba($admin-bg,0.8);
|
||||||
}
|
}
|
||||||
.audio-bubble {
|
.audio-bubble {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
|
|
@ -497,7 +511,7 @@ body {
|
||||||
}
|
}
|
||||||
position: relative;
|
position: relative;
|
||||||
.bubble {
|
.bubble {
|
||||||
background-color: #e5e5e5;
|
background-color: rgba(#e5e5e5,0.8);
|
||||||
border-radius: 10px 10px 0 10px;
|
border-radius: 10px 10px 0 10px;
|
||||||
[dir="rtl"] & {
|
[dir="rtl"] & {
|
||||||
border-radius: 10px 10px 10px 0px;
|
border-radius: 10px 10px 10px 0px;
|
||||||
|
|
@ -508,7 +522,7 @@ body {
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
.audio-bubble {
|
.audio-bubble {
|
||||||
background-color: #e5e5e5;
|
background-color: rgba(#e5e5e5,0.8);
|
||||||
border-radius: 10px 10px 0 10px;
|
border-radius: 10px 10px 0 10px;
|
||||||
[dir="rtl"] & {
|
[dir="rtl"] & {
|
||||||
border-radius: 10px 10px 10px 0px;
|
border-radius: 10px 10px 10px 0px;
|
||||||
|
|
@ -520,13 +534,6 @@ body {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
.video2-bubble {
|
|
||||||
background-color: #e5e5e5;
|
|
||||||
border-radius: 10px 10px 0 10px;
|
|
||||||
[dir="rtl"] & {
|
|
||||||
border-radius: 10px 10px 10px 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bubble.image-bubble {
|
.bubble.image-bubble {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
@ -747,6 +754,7 @@ body {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-operations {
|
.avatar-operations {
|
||||||
|
|
@ -805,11 +813,8 @@ body {
|
||||||
.read-marker {
|
.read-marker {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
height: 1px;
|
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
line-height: var(--v-theme-title-featured-line-height);
|
line-height: var(--v-theme-title-featured-line-height);
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
@ -818,19 +823,18 @@ body {
|
||||||
/* identical to box height, or 14px */
|
/* identical to box height, or 14px */
|
||||||
letter-spacing: 0.29px;
|
letter-spacing: 0.29px;
|
||||||
color: #c0c0c0;
|
color: #c0c0c0;
|
||||||
background-color: #c0c0c0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
&::after {
|
display: flex;
|
||||||
position: absolute;
|
align-items: center;
|
||||||
top: -4px;
|
& div.text {
|
||||||
background: white;
|
flex: 0 0 auto;
|
||||||
transform: translate(-50%, 0);
|
padding-left: 10px;
|
||||||
[dir="rtl"] & {
|
padding-right: 10px;
|
||||||
transform: translate(50%, 0);
|
}
|
||||||
}
|
& div.line {
|
||||||
padding-left: 4px;
|
background: #c0c0c0;
|
||||||
padding-right: 4px;
|
height: 1px;
|
||||||
content: attr(title);
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -839,7 +843,6 @@ body {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
height: 1px;
|
|
||||||
line-height: var(--v-theme-title-featured-line-height);
|
line-height: var(--v-theme-title-featured-line-height);
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|
@ -849,20 +852,17 @@ body {
|
||||||
/* identical to box height, or 14px */
|
/* identical to box height, or 14px */
|
||||||
letter-spacing: 0.29px;
|
letter-spacing: 0.29px;
|
||||||
color: black;
|
color: black;
|
||||||
background-color: black;
|
display: flex;
|
||||||
text-align: center;
|
align-items: center;
|
||||||
position: relative;
|
& div.text {
|
||||||
&::after {
|
flex: 0 0 auto;
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
background: white;
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
[dir="rtl"] & {
|
|
||||||
transform: translate(50%, 0);
|
|
||||||
}
|
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
content: attr(title);
|
}
|
||||||
|
& div.line {
|
||||||
|
background: black;
|
||||||
|
height: 1px;
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1018,13 +1018,15 @@ body {
|
||||||
& > *:last-child {
|
& > *:last-child {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
.option-title {
|
}
|
||||||
color: #000;
|
|
||||||
font-size: 16 * $chat-text-size;
|
.option-text {
|
||||||
}
|
font-size: 13 * $chat-text-size;
|
||||||
.option-text {
|
}
|
||||||
font-size: 13 * $chat-text-size;
|
|
||||||
}
|
.option-title {
|
||||||
|
color: #000;
|
||||||
|
font-size: 16 * $chat-text-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-button-left {
|
.header-button-left {
|
||||||
|
|
@ -1130,8 +1132,9 @@ body {
|
||||||
&.ptt {
|
&.ptt {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
bottom: 0px;
|
bottom: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
|
@ -1148,6 +1151,7 @@ body {
|
||||||
}
|
}
|
||||||
.will-cancel {
|
.will-cancel {
|
||||||
background-color: #ff3300;
|
background-color: #ff3300;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.recording-time {
|
.recording-time {
|
||||||
color: white;
|
color: white;
|
||||||
|
|
@ -1157,9 +1161,11 @@ body {
|
||||||
}
|
}
|
||||||
.locked {
|
.locked {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
background-color: orange;
|
background-color: orange;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.voice-recorder-lock {
|
.voice-recorder-lock {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -1326,6 +1332,10 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.new-room {
|
.new-room {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
font-size: 16px !important;
|
font-size: 16px !important;
|
||||||
|
|
@ -1351,6 +1361,9 @@ body {
|
||||||
.loading-indicator {
|
.loading-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
&.transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.exporting-indicator {
|
.exporting-indicator {
|
||||||
|
|
@ -1383,6 +1396,24 @@ body {
|
||||||
transition: opacity 0.3s linear;
|
transition: opacity 0.3s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-at-bottom {
|
||||||
|
position: fixed;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
background-color: rgba(black, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 40;
|
||||||
|
color: white;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.auto-audio-player-root {
|
.auto-audio-player-root {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 72px;
|
top: 72px;
|
||||||
|
|
@ -1512,18 +1543,6 @@ body {
|
||||||
.mic-button.dimmed {
|
.mic-button.dimmed {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.toast-read-only {
|
|
||||||
position: fixed;
|
|
||||||
left: 10px;
|
|
||||||
right: 10px;
|
|
||||||
bottom: 10px;
|
|
||||||
background-color: rgba(black, 0.7);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 40;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-layout.voice-recorder {
|
.audio-layout.voice-recorder {
|
||||||
|
|
@ -1531,4 +1550,18 @@ body {
|
||||||
right: 20px;
|
right: 20px;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-all-button {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11.54 * $chat-text-size;
|
||||||
|
line-height: 140%;
|
||||||
|
color: white !important;
|
||||||
|
background-color: #4642f1 !important;
|
||||||
|
border-radius: $small-button-height / 2;
|
||||||
|
min-height: 0;
|
||||||
|
height: $small-button-height !important;
|
||||||
|
margin-top: $chat-standard-padding-xs;
|
||||||
|
margin-bottom: $chat-standard-padding-xs;
|
||||||
}
|
}
|
||||||
|
|
@ -355,6 +355,11 @@ $small-button-height: 36px;
|
||||||
right: unset;
|
right: unset;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
background: linear-gradient(0deg, #000 0%, #000 100%), #4642f1;
|
background: linear-gradient(0deg, #000 0%, #000 100%), #4642f1;
|
||||||
|
&.close {
|
||||||
|
right: 8px;
|
||||||
|
left: unset;
|
||||||
|
background: $hiliteColor !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
127
src/assets/css/getlink.scss
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
@import "@/assets/css/main.scss";
|
||||||
|
|
||||||
|
.getlink-root {
|
||||||
|
background-color: $background;
|
||||||
|
|
||||||
|
.getlink-loggedin {
|
||||||
|
text-align: center;
|
||||||
|
button {
|
||||||
|
min-width: 200px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.getlink-image {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 325px;
|
||||||
|
max-height: 257px;
|
||||||
|
width: 100%;
|
||||||
|
.v-icon__component {
|
||||||
|
width: unset;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.getlink-title {
|
||||||
|
color: #000;
|
||||||
|
text-align: center;
|
||||||
|
font-family: "Poppins";
|
||||||
|
font-size: 28px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 108.5%; /* 30.38px */
|
||||||
|
letter-spacing: -0.8px;
|
||||||
|
white-space: pre-line;
|
||||||
|
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;
|
||||||
|
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: 13px 20px 18px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid #ededed;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.07);
|
||||||
|
|
||||||
|
.col:nth-child(1) {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
|
||||||
|
text-align: center;
|
||||||
|
.qr {
|
||||||
|
width: 120px !important;
|
||||||
|
height: 120px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.public-link {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none !important;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-copy-room-link {
|
||||||
|
background-color: var(--v-primary-base) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-copied-in-place .v-btn__content {
|
||||||
|
font-family: "Inter", sans-serif !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
text-transform: none !important;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.getlink-share {
|
||||||
|
color: #161616;
|
||||||
|
text-align: center;
|
||||||
|
font-family: "Inter";
|
||||||
|
font-size: 11.541px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 140%; /* 16.158px */
|
||||||
|
letter-spacing: 0.34px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: black;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
.v-image {
|
||||||
|
flex: 0 0 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 10px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.getlink-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -105,7 +105,7 @@ body { position:absolute; top:0; bottom:0; right:0; left:0; }
|
||||||
.v-btn.text-button {
|
.v-btn.text-button {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 11 * $chat-text-size;
|
font-size: 11 * $chat-text-size !important;
|
||||||
border: none;
|
border: none;
|
||||||
height: $chat-standard-padding;
|
height: $chat-standard-padding;
|
||||||
margin-top: $chat-standard-padding-xs;
|
margin-top: $chat-standard-padding-xs;
|
||||||
|
|
|
||||||
32
src/assets/icons/getlink.vue
Normal file
|
|
@ -2,6 +2,6 @@
|
||||||
<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
d="M16.0247 8.80006H15.4908V6.42878C15.4908 2.88379 12.5789 0 9.00064 0C5.42053 0 2.50904 2.88379 2.50904 6.42878V8.80047H1.97615C0.88518 8.80047 0 9.67678 0 10.7572V20.0423C0 21.1231 0.884952 22 1.97615 22H16.0248C17.1153 22 18 21.1232 18 20.0423L17.9999 10.7568C17.9999 9.67638 17.1151 8.80006 16.025 8.80006H16.0247ZM9.82754 15.5262V16.7406C9.82754 16.9215 9.6795 17.0687 9.4959 17.0687H8.50277C8.32063 17.0687 8.17185 16.9216 8.17185 16.7406V15.5262C7.7193 15.2498 7.41503 14.7589 7.41503 14.193C7.41503 13.3265 8.12456 12.6242 9.0001 12.6242C9.87461 12.6242 10.5841 13.3265 10.5841 14.193C10.584 14.7589 10.2795 15.2498 9.82754 15.5262ZM12.6451 8.80006H5.35551V6.42878C5.35551 4.43786 6.99007 2.81942 9.00073 2.81942C11.0097 2.81942 12.6451 4.43763 12.6451 6.42878V8.80006Z"
|
d="M16.0247 8.80006H15.4908V6.42878C15.4908 2.88379 12.5789 0 9.00064 0C5.42053 0 2.50904 2.88379 2.50904 6.42878V8.80047H1.97615C0.88518 8.80047 0 9.67678 0 10.7572V20.0423C0 21.1231 0.884952 22 1.97615 22H16.0248C17.1153 22 18 21.1232 18 20.0423L17.9999 10.7568C17.9999 9.67638 17.1151 8.80006 16.025 8.80006H16.0247ZM9.82754 15.5262V16.7406C9.82754 16.9215 9.6795 17.0687 9.4959 17.0687H8.50277C8.32063 17.0687 8.17185 16.9216 8.17185 16.7406V15.5262C7.7193 15.2498 7.41503 14.7589 7.41503 14.193C7.41503 13.3265 8.12456 12.6242 9.0001 12.6242C9.87461 12.6242 10.5841 13.3265 10.5841 14.193C10.584 14.7589 10.2795 15.2498 9.82754 15.5262ZM12.6451 8.80006H5.35551V6.42878C5.35551 4.43786 6.99007 2.81942 9.00073 2.81942C11.0097 2.81942 12.6451 4.43763 12.6451 6.42878V8.80006Z"
|
||||||
fill="white" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
3
src/assets/icons/ic_share.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="17" viewBox="0 0 24 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.2477 0.000670823C16.1384 0.00531075 16.0311 0.0307591 15.9314 0.0754688C15.7815 0.142957 15.6546 0.25192 15.5656 0.389005C15.4766 0.526231 15.4291 0.685951 15.4291 0.849038V3.32107C14.7529 3.28142 14.0829 3.28494 13.4234 3.33598C6.62122 3.86253 0.885374 9.07191 0.00664931 16.0431H0.00679109C-0.0251098 16.2933 0.0564145 16.5444 0.229528 16.7288C0.402641 16.9133 0.649181 17.0122 0.902999 16.9988C1.15679 16.9853 1.39143 16.8609 1.54355 16.6591C4.61977 12.5918 9.44481 10.1994 14.572 10.1994H15.4292V12.7494C15.4295 12.9682 15.5147 13.1784 15.6674 13.3364C15.8199 13.4945 16.0281 13.5881 16.2484 13.5978C16.4687 13.6075 16.6845 13.5326 16.8505 13.3886L23.7079 7.43851C23.8935 7.2771 24 7.04413 24 6.79934C24 6.55455 23.8935 6.3216 23.7079 6.16016L16.8505 0.210072C16.6844 0.0659577 16.4685 -0.00898088 16.2478 0.000859754L16.2477 0.000670823Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1,022 B |
|
|
@ -12,12 +12,7 @@
|
||||||
"room_info": {
|
"room_info": {
|
||||||
"title": "ཁ་བརྡ་ཁང་གི་ཞིབ་ཕྲའི་གནས་ཚུལ།",
|
"title": "ཁ་བརྡ་ཁང་གི་ཞིབ་ཕྲའི་གནས་ཚུལ།",
|
||||||
"version_info": "གྷར་ཌིན་ལས་འཆར་གྱིས་ནུས་ཤུགས་བསྩལ། ཐོན་རིམ། : {version}",
|
"version_info": "གྷར་ཌིན་ལས་འཆར་གྱིས་ནུས་ཤུགས་བསྩལ། ཐོན་རིམ། : {version}",
|
||||||
"leave_room_info": "གསལ་བཤད། གོ་རིམ་འདི་ཕྱིར་ཟློག་ཐབས་མེད། ཁྱེད་རང་ཕྱིར་ཐོན་རྒྱུ་ཡིན་མིན་དང་གླེང་མོལ་ཁག་གཏན་དུ་གསུབ་རྒྱུ་ཡིན་མིན་ཁག་ཐེག་བྱོས།",
|
|
||||||
"leave_room": "ཕྱིར་ཐོན།",
|
"leave_room": "ཕྱིར་ཐོན།",
|
||||||
"view_profile": "ལྟ་ཞིབ།",
|
|
||||||
"identity_temporary": "ཁྱེད་ཀྱི་ངོ་བོ {displayName} འདི་གནས་སྐབས་ཙམ་ཡིན། ཁྱེད་ཀྱིས་སོ་སོའི་མིང་དང་གསང་ཚིག་བརྗེས་ཏེ་འདི་ཉར་ཚགས་བྱེད་ཆོག",
|
|
||||||
"identity": "ཁྱེད་རང་{displayName} མིང་ཐོག་ནས་ནང་འཛུལ་བྱེད་བཞིན་འདུག",
|
|
||||||
"my_profile": "ངའི་ཡིག་ཆ།",
|
|
||||||
"show_all": "ཚང་མ་སྟོན། >",
|
"show_all": "ཚང་མ་སྟོན། >",
|
||||||
"hide_all": "སྦེད།",
|
"hide_all": "སྦེད།",
|
||||||
"user_you": "{user} (ཁྱེད།)",
|
"user_you": "{user} (ཁྱེད།)",
|
||||||
|
|
@ -69,7 +64,6 @@
|
||||||
},
|
},
|
||||||
"new_room": {
|
"new_room": {
|
||||||
"next": "རྗེས་མ།",
|
"next": "རྗེས་མ།",
|
||||||
"done": "ཚར་སོང་།",
|
|
||||||
"status_avatar": "མགོ་པར་ཡར་འཇུག་བྱེད་བཞིན་པ།: {count}",
|
"status_avatar": "མགོ་པར་ཡར་འཇུག་བྱེད་བཞིན་པ།: {count}",
|
||||||
"status_avatar_total": "མགོ་པར་ཡར་འཇུག་བྱེད་བཞིན་པ། {total} ཡི་{count}",
|
"status_avatar_total": "མགོ་པར་ཡར་འཇུག་བྱེད་བཞིན་པ། {total} ཡི་{count}",
|
||||||
"status_creating": "ཁ་བརྡ་ཁང་བཟོ་བཞིན་པ།",
|
"status_creating": "ཁ་བརྡ་ཁང་བཟོ་བཞིན་པ།",
|
||||||
|
|
@ -143,7 +137,6 @@
|
||||||
"join_invite": "ཁྱེད་ཀྱིས་གདན་ཞུ་གནང་བའི་མི་ཁོ་ན་མ་གཏོགས་འཛུལ་མི་ཐུབ།",
|
"join_invite": "ཁྱེད་ཀྱིས་གདན་ཞུ་གནང་བའི་མི་ཁོ་ན་མ་གཏོགས་འཛུལ་མི་ཐུབ།",
|
||||||
"join_public": "སུ་ཡིན་རུང་འབྲེལ་ཐག་འདིའི་ཐོག་ལ་མནན་ཏེ་འཛུལ་ཆོག: {link}.",
|
"join_public": "སུ་ཡིན་རུང་འབྲེལ་ཐག་འདིའི་ཐོག་ལ་མནན་ཏེ་འཛུལ་ཆོག: {link}.",
|
||||||
"info": "འདིར་ཁྱེད་ཀྱིས་སོ་སོའི་ཚོགས་པའི་སྐོར་ལ་ཤེས་དགོས་པའི་དོན་གནད་འགའ་ཡོད།:",
|
"info": "འདིར་ཁྱེད་ཀྱིས་སོ་སོའི་ཚོགས་པའི་སྐོར་ལ་ཤེས་དགོས་པའི་དོན་གནད་འགའ་ཡོད།:",
|
||||||
"welcome": "དགའ་བསུ་ཞུ།",
|
|
||||||
"got_it": "ཧ་གོ་སོང་།",
|
"got_it": "ཧ་གོ་སོང་།",
|
||||||
"room_history_joined": "ཚོགས་མི་ཁག་ཁ་བརྡ་ཁང་དུ་ཞུགས་པའི་རྗེས་སུ། ད་གཟོད་དེའི་ནང་དུ་བཏང་ཡོད་པའི་འཕྲིན་ཐུང་ཁག་མཐོང་ཐུབ།",
|
"room_history_joined": "ཚོགས་མི་ཁག་ཁ་བརྡ་ཁང་དུ་ཞུགས་པའི་རྗེས་སུ། ད་གཟོད་དེའི་ནང་དུ་བཏང་ཡོད་པའི་འཕྲིན་ཐུང་ཁག་མཐོང་ཐུབ།",
|
||||||
"room_history_is": "ཁ་བརྡ་ཁང་གི་ཟིན་ཐོ་ཁག {type}.",
|
"room_history_is": "ཁ་བརྡ་ཁང་གི་ཟིན་ཐོ་ཁག {type}.",
|
||||||
|
|
@ -155,13 +148,13 @@
|
||||||
"room_list_rooms": "ཁ་བརྡ་ཁང་།",
|
"room_list_rooms": "ཁ་བརྡ་ཁང་།",
|
||||||
"room_list_invites": "གདན་ཞུ་ཁག",
|
"room_list_invites": "གདན་ཞུ་ཁག",
|
||||||
"purge_failed": "ཁ་བརྡ་ཁང་བཤིག་ཐུབ་མ་སོང་།",
|
"purge_failed": "ཁ་བརྡ་ཁང་བཤིག་ཐུབ་མ་སོང་།",
|
||||||
"purge_removing_members": "ཚོགས་མི་ཁག་ཕྱིར་འདོན། {total})་ཀྱི་({members}",
|
"purge_removing_members": "ཚོགས་མི་ཁག་ཕྱིར་འདོན། ({total}་ཀྱི་({count})",
|
||||||
"purge_redacting_events": "ཁ་བརྡ་གཙང་གསུབ། {total})་ཀྱི་({count}",
|
"purge_redacting_events": "ཁ་བརྡ་གཙང་གསུབ། {total})་ཀྱི་({count}",
|
||||||
"purge_set_room_state": "ཁ་བརྡ་ཁང་གི་རྣམ་པ་སྒྲིག་འགོད།",
|
"purge_set_room_state": "ཁ་བརྡ་ཁང་གི་རྣམ་པ་སྒྲིག་འགོད།",
|
||||||
"room_list_new_messages": "{count} ཆ་འཕྲིན་གསར་པ།",
|
"room_list_new_messages": "{count} ཆ་འཕྲིན་གསར་པ།",
|
||||||
"room_topic_required": "ཁ་བརྡ་ཁང་ལ་འགྲེལ་བཤད་དགོས།",
|
"room_topic_required": "ཁ་བརྡ་ཁང་ལ་འགྲེལ་བཤད་དགོས།",
|
||||||
"room_name_required": "ཁ་བརྡ་ཁང་ལ་མིང་ཞིག་དགོས།",
|
"room_name_required": "ཁ་བརྡ་ཁང་ལ་མིང་ཞིག་དགོས།",
|
||||||
"invitations": "ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་གང་ཡང་མི་འདུག | ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་གྲངས་གཅིག་འདུག | ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་{གྲངས}་འདུག",
|
"invitations": "ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་གང་ཡང་མི་འདུག | ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་གྲངས་གཅིག་འདུག | ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་{count}་འདུག",
|
||||||
"unseen_messages": "ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་གང་ཡང་མི་འདུག | ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་གཅིག་འདུག | ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་{count}འདུག"
|
"unseen_messages": "ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་གང་ཡང་མི་འདུག | ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་གཅིག་འདུག | ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་{count}འདུག"
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
|
|
@ -208,11 +201,11 @@
|
||||||
"incoming_message_deleted_text": "ཆ་འཕྲིན་འདི་བསུབས་ཟིན།",
|
"incoming_message_deleted_text": "ཆ་འཕྲིན་འདི་བསུབས་ཟིན།",
|
||||||
"reply_poll": "བསམ་ཚུལ་བསྡུ་ལེན།",
|
"reply_poll": "བསམ་ཚུལ་བསྡུ་ལེན།",
|
||||||
"not_allowed_to_send": "དོ་དམ་པ་དང་གཙོ་སྐྱོང་བ་ཁོ་ནས་མ་གཏོགས་ཁ་བརྡ་ཁང་དུ་གཏོང་མི་ཆོག",
|
"not_allowed_to_send": "དོ་དམ་པ་དང་གཙོ་སྐྱོང་བ་ཁོ་ནས་མ་གཏོགས་ཁ་བརྡ་ཁང་དུ་གཏོང་མི་ཆོག",
|
||||||
"user_was_kicked_by_you": "ཁྱེད་ཀྱིས་{སྤྱོད་མཁན}་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
|
"user_was_kicked_by_you": "ཁྱེད་ཀྱིས་{user}་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
|
||||||
"user_was_kicked": "{སྤྱོད་མཁན} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་ཟིན།",
|
"user_was_kicked": "{user} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་ཟིན།",
|
||||||
"user_was_kicked_you": "ཁྱེད་རང་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
|
"user_was_kicked_you": "ཁྱེད་རང་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
|
||||||
"user_was_banned": "{སྤྱོད་མཁན}་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།",
|
"user_was_banned": "{user}་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།",
|
||||||
"user_was_banned_by_you": "ཁྱེད་ཀྱིས་{སྤྱོད་མཁན} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།",
|
"user_was_banned_by_you": "ཁྱེད་ཀྱིས་{user} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།",
|
||||||
"time_ago": "དེ་རིང་། | ཁ་སང་། | ཉིན་གྲངས་{count} གོང་།",
|
"time_ago": "དེ་རིང་། | ཁ་སང་། | ཉིན་གྲངས་{count} གོང་།",
|
||||||
"outgoing_message_deleted_text": "ཁྱེད་ཀྱིས་ཆ་འཕྲིན་འདི་བསུབས་སོང་།",
|
"outgoing_message_deleted_text": "ཁྱེད་ཀྱིས་ཆ་འཕྲིན་འདི་བསུབས་སོང་།",
|
||||||
"reaction_count_more": "{reactionCount} མང་བ།"
|
"reaction_count_more": "{reactionCount} མང་བ།"
|
||||||
|
|
@ -232,7 +225,6 @@
|
||||||
"swipe_to_cancel": "ཤུད་འདེད་བྱས་ཏེ་སུབས།"
|
"swipe_to_cancel": "ཤུད་འདེད་བྱས་ཏེ་སུབས།"
|
||||||
},
|
},
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"create_room": "ཚོགས་པ་བཟོས།",
|
|
||||||
"view_details": "ཞིབ་ཕྲར་གཟིགས།",
|
"view_details": "ཞིབ་ཕྲར་གཟིགས།",
|
||||||
"this_room": "ཁ་བརྡ་ཁང་འདི།"
|
"this_room": "ཁ་བརྡ་ཁང་འདི།"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
{
|
{
|
||||||
"language_display_name": "Deutsch",
|
"language_display_name": "Deutsch",
|
||||||
"global": {
|
|
||||||
},
|
|
||||||
"menu": {
|
"menu": {
|
||||||
"start_private_chat": "Private Diskussion mit diesem Benutzer",
|
"start_private_chat": "Private Diskussion mit diesem Benutzer",
|
||||||
"reply": "Antworten",
|
"reply": "Antworten",
|
||||||
|
|
@ -18,12 +16,13 @@
|
||||||
"undo": "Rückgängig",
|
"undo": "Rückgängig",
|
||||||
"join": "Beitreten",
|
"join": "Beitreten",
|
||||||
"ignore": "Ignorieren",
|
"ignore": "Ignorieren",
|
||||||
"loading": "{appName} wird geladen"
|
"loading": "{appName} wird geladen",
|
||||||
|
"done": "Fertig"
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"you": "Du",
|
"you": "Du",
|
||||||
"user_created_room": "{user} hat den Raum erstellt",
|
"user_created_room": "{user} hat den Raum erstellt",
|
||||||
"user_aliased_room": "{Benutzer} hat den Raumalias {alias} erstellt",
|
"user_aliased_room": "{user} hat den Raumalias {alias} erstellt",
|
||||||
"user_changed_display_name": "{user} hat den Anzeigenamen in {displayName} geändert",
|
"user_changed_display_name": "{user} hat den Anzeigenamen in {displayName} geändert",
|
||||||
"user_changed_avatar": "{user} hat den Avatar geändert",
|
"user_changed_avatar": "{user} hat den Avatar geändert",
|
||||||
"user_changed_room_avatar": "{user} hat den Raumavatar geändert",
|
"user_changed_room_avatar": "{user} hat den Raumavatar geändert",
|
||||||
|
|
@ -44,7 +43,6 @@
|
||||||
"room_joinrule_public": "öffentlich",
|
"room_joinrule_public": "öffentlich",
|
||||||
"user_changed_room_topic": "{user} hat das Raumthema auf {topic} geändert",
|
"user_changed_room_topic": "{user} hat das Raumthema auf {topic} geändert",
|
||||||
"unread_messages": "Ungelesene Nachrichten",
|
"unread_messages": "Ungelesene Nachrichten",
|
||||||
"replying_to_event": "ANTWORT AUF EREIGNIS: {message}",
|
|
||||||
"your_message": "Deine Nachricht …",
|
"your_message": "Deine Nachricht …",
|
||||||
"scale_image": "Bild skalieren",
|
"scale_image": "Bild skalieren",
|
||||||
"user_is_typing": "{user} schreibt",
|
"user_is_typing": "{user} schreibt",
|
||||||
|
|
@ -54,9 +52,14 @@
|
||||||
"user_changed_guest_access_open": "{user} hat Gästen erlaubt, den Raum beizutreten",
|
"user_changed_guest_access_open": "{user} hat Gästen erlaubt, den Raum beizutreten",
|
||||||
"room_powerlevel_change": "{user} hat den Status von {changes} geändert",
|
"room_powerlevel_change": "{user} hat den Status von {changes} geändert",
|
||||||
"user_left": "{user} hat das Gespräch verlassen",
|
"user_left": "{user} hat das Gespräch verlassen",
|
||||||
"user_joined": "{Benutzer} ist dem Gespräch beigetreten",
|
"user_joined": "{user} ist dem Gespräch beigetreten",
|
||||||
"download_progress": "{percentage} % heruntergeladen",
|
"download_progress": "{percentage} % heruntergeladen",
|
||||||
"user_changed_room_name": "{user} hat den Raumnamen in {name} geändert"
|
"user_changed_room_name": "{user} hat den Raumnamen in {name} geändert",
|
||||||
|
"replying_to": "Antwort an {user}",
|
||||||
|
"reply_image": "Bild",
|
||||||
|
"reply_audio_message": "Sprachnachricht",
|
||||||
|
"reply_video": "Video",
|
||||||
|
"time_ago": "Heute | Gestern | Vor {count} Tagen"
|
||||||
},
|
},
|
||||||
"room": {
|
"room": {
|
||||||
"leave": "Verlassen",
|
"leave": "Verlassen",
|
||||||
|
|
@ -66,7 +69,8 @@
|
||||||
"members": "keine Mitglieder | 1 Mitglied | {count} Mitglieder",
|
"members": "keine Mitglieder | 1 Mitglied | {count} Mitglieder",
|
||||||
"purge_removing_members": "Entfernen von Mitgliedern",
|
"purge_removing_members": "Entfernen von Mitgliedern",
|
||||||
"purge_failed": "Fehler beim Bereinigen des Raums!",
|
"purge_failed": "Fehler beim Bereinigen des Raums!",
|
||||||
"room_list_rooms": "Räume"
|
"room_list_rooms": "Räume",
|
||||||
|
"invitations": "Du hast keine Einladungen | Du hast 1 Einladung | Du hast {count} Einladungen"
|
||||||
},
|
},
|
||||||
"room_welcome": {
|
"room_welcome": {
|
||||||
"info": "Herzlich willkommen! Hier sind ein paar Dinge, die du über deinen Raum wissen solltest:",
|
"info": "Herzlich willkommen! Hier sind ein paar Dinge, die du über deinen Raum wissen solltest:",
|
||||||
|
|
@ -96,7 +100,8 @@
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"next": "Nächste",
|
"next": "Nächste",
|
||||||
"name_room": "Raum benennen",
|
"name_room": "Raum benennen",
|
||||||
"room_topic": "Füge eine Beschreibung hinzu, wenn du möchtest"
|
"room_topic": "Füge eine Beschreibung hinzu, wenn du möchtest",
|
||||||
|
"room_name_limit_error_msg": "Maximal 50 Zeichen erlaubt"
|
||||||
},
|
},
|
||||||
"device_list": {
|
"device_list": {
|
||||||
"title": "GERÄTE",
|
"title": "GERÄTE",
|
||||||
|
|
@ -112,7 +117,8 @@
|
||||||
"password_required": "Das Passwort ist erforderlich",
|
"password_required": "Das Passwort ist erforderlich",
|
||||||
"login": "Anmelden",
|
"login": "Anmelden",
|
||||||
"create_room": "Registrieren und Raum erstellen",
|
"create_room": "Registrieren und Raum erstellen",
|
||||||
"or": "ODER"
|
"or": "ODER",
|
||||||
|
"invalid_message": "Benutzername oder Passwort falsch"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Mein Profil",
|
"title": "Mein Profil",
|
||||||
|
|
@ -124,7 +130,10 @@
|
||||||
"password_old": "Altes Passwort",
|
"password_old": "Altes Passwort",
|
||||||
"password_new": "Neues Kennwort",
|
"password_new": "Neues Kennwort",
|
||||||
"password_repeat": "Wiederhole das neue Passwort",
|
"password_repeat": "Wiederhole das neue Passwort",
|
||||||
"display_name": "Anzeigename"
|
"display_name": "Anzeigename",
|
||||||
|
"set_language": "Stelle deine Sprache ein",
|
||||||
|
"language_description": "Convene ist in vielen Sprachen verfügbar.",
|
||||||
|
"tell_us": "Teile uns mit."
|
||||||
},
|
},
|
||||||
"profile_info_popup": {
|
"profile_info_popup": {
|
||||||
"you_are": "Du bist",
|
"you_are": "Du bist",
|
||||||
|
|
@ -138,14 +147,15 @@
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"user_name_label": "Benutzername",
|
"user_name_label": "Benutzername",
|
||||||
"shared_computer": "Dies ist ein gemeinsam genutztes Gerät",
|
|
||||||
"joining_as": "Du trittst bei als:",
|
"joining_as": "Du trittst bei als:",
|
||||||
"join": "Raum beitreten",
|
"join": "Raum beitreten",
|
||||||
"join_guest": "Als Gast beitreten",
|
|
||||||
"status_logging_in": "Wird angemeldet …",
|
"status_logging_in": "Wird angemeldet …",
|
||||||
"status_joining": "Raum beitreten …",
|
"status_joining": "Raum beitreten …",
|
||||||
"join_failed": "Beitritt zum Raum fehlgeschlagen.",
|
"join_failed": "Beitritt zum Raum fehlgeschlagen.",
|
||||||
"title": "Willkommen in {roomName}"
|
"title": "Willkommen in {roomName}",
|
||||||
|
"enter_room": "Raum betreten",
|
||||||
|
"remember_me": "Mich merken",
|
||||||
|
"choose_name": "Wähle einen zu nutzenden Namen"
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"title": "Freunde hinzufügen",
|
"title": "Freunde hinzufügen",
|
||||||
|
|
@ -164,8 +174,6 @@
|
||||||
"leave": "Verlassen",
|
"leave": "Verlassen",
|
||||||
"title_invite": "Bist du sicher, dass du gehen willst?"
|
"title_invite": "Bist du sicher, dass du gehen willst?"
|
||||||
},
|
},
|
||||||
"logout": {
|
|
||||||
},
|
|
||||||
"purge_room": {
|
"purge_room": {
|
||||||
"info": "Alle Mitglieder und Nachrichten werden entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",
|
"info": "Alle Mitglieder und Nachrichten werden entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
"button": "Löschen",
|
"button": "Löschen",
|
||||||
|
|
@ -197,7 +205,8 @@
|
||||||
"show_all": "Alle anzeigen >",
|
"show_all": "Alle anzeigen >",
|
||||||
"leave_room": "Verlassen",
|
"leave_room": "Verlassen",
|
||||||
"version_info": "Angetrieben von Guardian Project. Version: {version}",
|
"version_info": "Angetrieben von Guardian Project. Version: {version}",
|
||||||
"scan_code": "Scannen, um den Raum zu betreten"
|
"scan_code": "Scannen, um den Raum zu betreten",
|
||||||
|
"export_room": "Chat exportieren"
|
||||||
},
|
},
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"this_room": "Dieser Raum",
|
"this_room": "Dieser Raum",
|
||||||
|
|
@ -222,5 +231,36 @@
|
||||||
"video_file": "Videodatei",
|
"video_file": "Videodatei",
|
||||||
"original_text": "<Originaltext>",
|
"original_text": "<Originaltext>",
|
||||||
"download_name": "Herunterladen"
|
"download_name": "Herunterladen"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"name": "Convene",
|
||||||
|
"tag_line": "Einfach miteinander verbinden"
|
||||||
|
},
|
||||||
|
"poll_create": {
|
||||||
|
"title": "Umfrage erstellen",
|
||||||
|
"create": "Erstellen",
|
||||||
|
"poll_undisclosed": "Geschlossen – Nutzer werden die Ergebnisse sehen, sobald die Umfrage geschlossen ist.",
|
||||||
|
"failed": "Konnte Umfrage nicht erstellen, bitte versuche es später erneut.",
|
||||||
|
"creating": "Umfrage erstellen",
|
||||||
|
"poll_disclosed": "Offen – aktuelle Ergebnisse werden immer angezeigt.",
|
||||||
|
"add_answer": "Antwort hinzufügen",
|
||||||
|
"create_poll_menu_option": "Umfrage erstellen",
|
||||||
|
"poll_status_closed": "Umfrage geschlossen",
|
||||||
|
"question_required": "Du musst eine Frage eingeben!",
|
||||||
|
"answer_required": "Antwort kann nicht leer sein. Bitte gib einigen Text ein oder entferne diese Option.",
|
||||||
|
"poll_submit": "Absenden",
|
||||||
|
"num_answered": "{count} haben geantwortet",
|
||||||
|
"question_label": "Gib deine Frage hier ein",
|
||||||
|
"poll_status_open": "Umfrage ist offen",
|
||||||
|
"poll_status_disclosed": "Ergebnisse werden angezeigt, sobald die Umfrage geschlossen wurde.",
|
||||||
|
"poll_status_open_not_voted": "Umfrage ist offen – stimme ab, um die Ergebnisse zu sehen",
|
||||||
|
"close_poll": "Umfrage schließen"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"fetched_n_of_total_events": "{count} von {total} Ereignissen geladen",
|
||||||
|
"exported_date": "Am {date} exportiert",
|
||||||
|
"processed_n_of_total_events": "Medien für {count} von {total} Ereignissen verarbeitet",
|
||||||
|
"fetched_n_events": "{count} Ereignisse geladen",
|
||||||
|
"export_filename": "Chat exportiert: {date}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"global": {
|
"global": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"password_didnot_match": "Password didn't match",
|
"password_didnot_match": "Password didn't match",
|
||||||
"password_hint": "Minimum 12 character containing atleast one numeric, one uppercase and one lowercase letter",
|
"password_hint": "Minimum 12 character containing at least one numeric, one uppercase and one lowercase letter",
|
||||||
"show_less": "Show less",
|
"show_less": "Show less",
|
||||||
"show_more": "Show more",
|
"show_more": "Show more",
|
||||||
"add_reaction": "Add reaction",
|
"add_reaction": "Add reaction",
|
||||||
|
|
@ -17,10 +17,12 @@
|
||||||
"minutes": "1 minute ago | {n} minutes ago",
|
"minutes": "1 minute ago | {n} minutes ago",
|
||||||
"hours": "1 hour ago | {n} hours ago",
|
"hours": "1 hour ago | {n} hours ago",
|
||||||
"days": "1 day ago | {n} days ago"
|
"days": "1 day ago | {n} days ago"
|
||||||
}
|
},
|
||||||
|
"close": "close",
|
||||||
|
"notify": "Notify"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"start_private_chat": "Private chat with this user",
|
"start_private_chat": "Direct Message with this user",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -34,6 +36,7 @@
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"new_room": "New Room",
|
"new_room": "New Room",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
|
"delete_now": "Delete now",
|
||||||
"join": "Join",
|
"join": "Join",
|
||||||
"ignore": "Ignore",
|
"ignore": "Ignore",
|
||||||
"loading": "Loading {appName}",
|
"loading": "Loading {appName}",
|
||||||
|
|
@ -60,11 +63,15 @@
|
||||||
"user_was_banned_you": "You were kicked and banned from the chat.",
|
"user_was_banned_you": "You were kicked and banned from the chat.",
|
||||||
"user_joined": "{user} joined the chat",
|
"user_joined": "{user} joined the chat",
|
||||||
"user_left": "{user} left the chat",
|
"user_left": "{user} left the chat",
|
||||||
|
"someone": "Someone",
|
||||||
"user_said": "{user} said:",
|
"user_said": "{user} said:",
|
||||||
|
"sent_media": "Sent {count} media items.",
|
||||||
"file_prefix": "File: ",
|
"file_prefix": "File: ",
|
||||||
"edited": "(edited)",
|
"edited": "(edited)",
|
||||||
"download_progress": "{percentage}% downloaded",
|
"download_progress": "{percentage}% downloaded",
|
||||||
|
"preparing_to_upload": "Preparing to upload...",
|
||||||
"upload_file_too_large": "File is too large to upload!",
|
"upload_file_too_large": "File is too large to upload!",
|
||||||
|
"upload_exceeded_file_limit": "Maximum file size of ({configFormattedUploadSize}) exceeded. ",
|
||||||
"upload_progress": "Uploaded {count}",
|
"upload_progress": "Uploaded {count}",
|
||||||
"upload_progress_with_total": "Uploaded {count} of {total}",
|
"upload_progress_with_total": "Uploaded {count} of {total}",
|
||||||
"user_changed_room_history": "{user} made room history {type}",
|
"user_changed_room_history": "{user} made room history {type}",
|
||||||
|
|
@ -101,7 +108,8 @@
|
||||||
"file": "File",
|
"file": "File",
|
||||||
"files": "Files",
|
"files": "Files",
|
||||||
"images": "Images",
|
"images": "Images",
|
||||||
"send_attachements_dialog_title": "Do you want to send following attachments ?"
|
"send_attachements_dialog_title": "Do you want to send following attachments ?",
|
||||||
|
"download_all": "Download all"
|
||||||
},
|
},
|
||||||
"room": {
|
"room": {
|
||||||
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",
|
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",
|
||||||
|
|
@ -127,7 +135,9 @@
|
||||||
"join_invite": "Only people you invite can join.",
|
"join_invite": "Only people you invite can join.",
|
||||||
"info_permissions": "You can change ‘join permissions’ at any time in the room settings.",
|
"info_permissions": "You can change ‘join permissions’ at any time in the room settings.",
|
||||||
"got_it": "Got it",
|
"got_it": "Got it",
|
||||||
"no_past_messages": "Welcome! For your security, past messages are not available."
|
"no_past_messages": "Welcome! For your security, past messages are not available.",
|
||||||
|
"direct_info": "Hi, {you}. You’re in a private chat with {user}.",
|
||||||
|
"direct_private_chat": "Direct Message"
|
||||||
},
|
},
|
||||||
"new_room": {
|
"new_room": {
|
||||||
"new_room": "New Room",
|
"new_room": "New Room",
|
||||||
|
|
@ -180,6 +190,19 @@
|
||||||
"send_token": "Send token",
|
"send_token": "Send token",
|
||||||
"token_not_valid": "Invalid token"
|
"token_not_valid": "Invalid token"
|
||||||
},
|
},
|
||||||
|
"getlink": {
|
||||||
|
"title": "Get a Direct Link",
|
||||||
|
"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},\nHere’s your Direct Link",
|
||||||
|
"ready_to_share": "It’s 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",
|
||||||
|
"continue": "Continue",
|
||||||
|
"different_link": "Get a different link",
|
||||||
|
"share_qr": "Share QR",
|
||||||
|
"qr_image_copied": "Image copied to clipboard"
|
||||||
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "My Profile",
|
"title": "My Profile",
|
||||||
"temporary_identity": "This identity is temporary. Set a password to use it again",
|
"temporary_identity": "This identity is temporary. Set a password to use it again",
|
||||||
|
|
@ -248,7 +271,7 @@
|
||||||
"info": "All members and messages will be removed. This action cannot be undone.",
|
"info": "All members and messages will be removed. This action cannot be undone.",
|
||||||
"button": "Delete",
|
"button": "Delete",
|
||||||
"n_seconds": "{seconds} seconds",
|
"n_seconds": "{seconds} seconds",
|
||||||
"self_destruct": "Room will self destruct in seconds.",
|
"self_destruct": "Your room will self destruct in seconds.",
|
||||||
"deleting": "Deleting room:",
|
"deleting": "Deleting room:",
|
||||||
"notified": "We've notified members.",
|
"notified": "We've notified members.",
|
||||||
"room_deletion_notice": "Time to say goodbye! This room has been deleted by {user}. It will self destruct in seconds."
|
"room_deletion_notice": "Time to say goodbye! This room has been deleted by {user}. It will self destruct in seconds."
|
||||||
|
|
@ -291,8 +314,8 @@
|
||||||
"read_only_room_info": "Only admins and moderators are allowed to send to the room",
|
"read_only_room_info": "Only admins and moderators are allowed to send to the room",
|
||||||
"make_public": "Make Public",
|
"make_public": "Make Public",
|
||||||
"make_public_warning": "warning: Full message history will be visible to new participants",
|
"make_public_warning": "warning: Full message history will be visible to new participants",
|
||||||
"contact_link": "My Contact Link",
|
"direct_link": "My Direct Link",
|
||||||
"contact_link_desc": "Share your contact link. When opened, a direct message will be started with you."
|
"direct_link_desc": "It's ready to share! A new direct room will open each time someone opens the link."
|
||||||
},
|
},
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"this_room": "This room",
|
"this_room": "This room",
|
||||||
|
|
@ -353,7 +376,13 @@
|
||||||
"export_filename": "Exported chat {date}"
|
"export_filename": "Exported chat {date}"
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"title": "New message received"
|
"title": "New message received",
|
||||||
|
"dialog" : {
|
||||||
|
"title": "Stay Connected with Chat Notifications!",
|
||||||
|
"body": "Never miss a message or important conversation again! Be notified whenever someone sends you a message or replies to your chat.",
|
||||||
|
"enable": "Enable"
|
||||||
|
},
|
||||||
|
"blocked_message": "Notifications blocked. Please reset the permissions"
|
||||||
},
|
},
|
||||||
"emoji": {
|
"emoji": {
|
||||||
"search": "Search...",
|
"search": "Search...",
|
||||||
|
|
@ -378,7 +407,8 @@
|
||||||
"sending": "Sending",
|
"sending": "Sending",
|
||||||
"files_sent":"1 file sent! | {count} files sent!",
|
"files_sent":"1 file sent! | {count} files sent!",
|
||||||
"files_sent_with_note":"1 file sent with a note! | {count} files sent with a note!",
|
"files_sent_with_note":"1 file sent with a note! | {count} files sent with a note!",
|
||||||
"return_to_home": "Return to home",
|
"send_more_files": "Send more files",
|
||||||
|
"close": "Close",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,10 @@
|
||||||
"name": "Convene",
|
"name": "Convene",
|
||||||
"tag_line": "Simplemente conectar"
|
"tag_line": "Simplemente conectar"
|
||||||
},
|
},
|
||||||
"global": {
|
|
||||||
},
|
|
||||||
"room_info": {
|
"room_info": {
|
||||||
"identity": "Has iniciado sesión como {displayName}.",
|
|
||||||
"my_profile": "Mi perfil",
|
|
||||||
"show_all": "Mostrar todos>",
|
"show_all": "Mostrar todos>",
|
||||||
"hide_all": "Ocultar",
|
"hide_all": "Ocultar",
|
||||||
"user_you": "{user} (you)",
|
"user_you": "{user} (tú)",
|
||||||
"user": "{user}",
|
"user": "{user}",
|
||||||
"members": "Miembros",
|
"members": "Miembros",
|
||||||
"purge": "Borrar la sala",
|
"purge": "Borrar la sala",
|
||||||
|
|
@ -22,12 +18,27 @@
|
||||||
"created_by": "Creado por {user}",
|
"created_by": "Creado por {user}",
|
||||||
"title": "Detalles de la sala",
|
"title": "Detalles de la sala",
|
||||||
"version_info": "Creado por Guardian Project. Version: {version}",
|
"version_info": "Creado por Guardian Project. Version: {version}",
|
||||||
"leave_room_info": "Nota: este paso no se puede deshacer. Asegúrate de que deseas cerrar la sesión y eliminar el chat para siempre.",
|
|
||||||
"leave_room": "Salir",
|
"leave_room": "Salir",
|
||||||
"view_profile": "Vista",
|
|
||||||
"identity_temporary": "Tu identidad {displayName} es temporal. Puedes cambiar tu nombre o establecer una contraseña para conservarla.",
|
|
||||||
"copy_invite_link": "Copiar el enlace de invitación",
|
"copy_invite_link": "Copiar el enlace de invitación",
|
||||||
"scan_code": "Escanear para unirse a la sala"
|
"scan_code": "Escanear para unirse a la sala",
|
||||||
|
"user_admin": "Administrador/a",
|
||||||
|
"user_moderator": "Moderador/a",
|
||||||
|
"room_type_default": "Por Defecto",
|
||||||
|
"copy_link": "Copiar enlace",
|
||||||
|
"direct_link": "Mi enlace directo",
|
||||||
|
"read_only_room": "Sala de sólo lectura",
|
||||||
|
"voice_mode": "Por voz",
|
||||||
|
"make_public_warning": "Advertencia: El historial completo de mensajes será visible para los nuevos participantes",
|
||||||
|
"room_type": "Genero de la sala",
|
||||||
|
"experimental_features": "Funciones experimentales",
|
||||||
|
"file_mode_info": "Cambia la interfaz del chat al modo \"soltar archivos\"",
|
||||||
|
"read_only_room_info": "Sólo los administradores y moderadores pueden escribir en la sala",
|
||||||
|
"file_mode": "Modo del archivo",
|
||||||
|
"direct_link_desc": "¡Ya está listo para compartir! Se abrirá una nueva sala privada cada vez que alguien abra el enlace.",
|
||||||
|
"download_chat": "Descargar el chat",
|
||||||
|
"make_public": "Hacer público",
|
||||||
|
"voice_mode_info": "Cambia la interfaz del chat al modo \"escuchar y grabar\"",
|
||||||
|
"export_room": "Exportar el chat"
|
||||||
},
|
},
|
||||||
"purge_room": {
|
"purge_room": {
|
||||||
"button": "Borrar",
|
"button": "Borrar",
|
||||||
|
|
@ -47,9 +58,7 @@
|
||||||
"title_invite": "Estas seguro que deseeas salir?",
|
"title_invite": "Estas seguro que deseeas salir?",
|
||||||
"text_public_lastroom": "Si deseas unirte a esta sala de nuevo, puedes hacerlo con una nueva identidad. Para mantener {user}, {action}.",
|
"text_public_lastroom": "Si deseas unirte a esta sala de nuevo, puedes hacerlo con una nueva identidad. Para mantener {user}, {action}.",
|
||||||
"text_public": "Siempre puedes volver a unirte a esta sala si conoces el enlace.",
|
"text_public": "Siempre puedes volver a unirte a esta sala si conoces el enlace.",
|
||||||
"title_public": "Adios, [user}"
|
"title_public": "Adiós, {user}"
|
||||||
},
|
|
||||||
"logout": {
|
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"status_error": "No se pudo invitar a uno o más amigos!",
|
"status_error": "No se pudo invitar a uno o más amigos!",
|
||||||
|
|
@ -65,9 +74,14 @@
|
||||||
"joining_as": "Te estas uniendo como:",
|
"joining_as": "Te estas uniendo como:",
|
||||||
"join": "Unirse a la sala",
|
"join": "Unirse a la sala",
|
||||||
"enter_room": "Entrar habitacion",
|
"enter_room": "Entrar habitacion",
|
||||||
"status_logging_in": "Iniciando sesión...",
|
"status_logging_in": "Iniciando la sesión...",
|
||||||
"status_joining": "Uniendose a la sala...",
|
"status_joining": "Uniendose a la sala de chat...",
|
||||||
"join_failed": "No se pudo unir a la sala."
|
"join_failed": "No se pudo unir a la sala.",
|
||||||
|
"choose_name": "Elige un nombre",
|
||||||
|
"title_user": "Bienvenido, has sido invitado a chatear con",
|
||||||
|
"enter_room_user": "Empezar un chat",
|
||||||
|
"you_have_been_banned": "Has sido expulsado de esta sala.",
|
||||||
|
"join_user": "Empezar un chat"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"display_name": "Nombre para mostrar",
|
"display_name": "Nombre para mostrar",
|
||||||
|
|
@ -83,7 +97,8 @@
|
||||||
"set_language": "Establece tu Idioma",
|
"set_language": "Establece tu Idioma",
|
||||||
"language_description": "Convine esta disponible en varios Idiomas.",
|
"language_description": "Convine esta disponible en varios Idiomas.",
|
||||||
"dont_see_yours": "¿No ves el tuyo?",
|
"dont_see_yours": "¿No ves el tuyo?",
|
||||||
"tell_us": "Dinos."
|
"tell_us": "Dinos.",
|
||||||
|
"display_name_required": "El nombre para mostrar es obligatorio"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "Iniciar sesión",
|
"login": "Iniciar sesión",
|
||||||
|
|
@ -94,7 +109,18 @@
|
||||||
"title": "Iniciar sesión",
|
"title": "Iniciar sesión",
|
||||||
"create_room": "Registrarse y crear una sala",
|
"create_room": "Registrarse y crear una sala",
|
||||||
"or": "O",
|
"or": "O",
|
||||||
"invalid_message": "Nombre de usuario o contraseña incorrecto"
|
"invalid_message": "Nombre de usuario o contraseña incorrecto",
|
||||||
|
"accept_terms": "Aceptar",
|
||||||
|
"send_verification": "Enviar un correo electrónico de verificación",
|
||||||
|
"resend_verification": "Reenviar mensaje de verificación",
|
||||||
|
"token_not_valid": "Token inválido",
|
||||||
|
"email": "Debe verificar su dirección de correo electrónico",
|
||||||
|
"sent_verification": "Se ha enviado un correo electrónico a {email}. Utilice su cliente de correo electrónico habitual para verificar la dirección.",
|
||||||
|
"no_supported_flow": "La aplicación no puede iniciar sesión en el servidor",
|
||||||
|
"email_not_valid": "Correo electrónico no válido",
|
||||||
|
"registration_token": "Por favor, introduzca la clave de registro",
|
||||||
|
"send_token": "Enviar un token",
|
||||||
|
"terms": "El servidor requiere que revises y aceptes las siguientes políticas:"
|
||||||
},
|
},
|
||||||
"device_list": {
|
"device_list": {
|
||||||
"not_verified": "No ha sido Verificado",
|
"not_verified": "No ha sido Verificado",
|
||||||
|
|
@ -116,33 +142,41 @@
|
||||||
"join_permissions_info": "Estos permisos determinan cómo las personas pueden unirse a la sala y con qué facilidad se puede invitar a otras personas. Pueden cambiarse en cualquier momento.",
|
"join_permissions_info": "Estos permisos determinan cómo las personas pueden unirse a la sala y con qué facilidad se puede invitar a otras personas. Pueden cambiarse en cualquier momento.",
|
||||||
"set_join_permissions": "Establecer permisos para unirse",
|
"set_join_permissions": "Establecer permisos para unirse",
|
||||||
"join_permissions": "Permisos para unirse",
|
"join_permissions": "Permisos para unirse",
|
||||||
"name_room": "Nombra la sala",
|
"name_room": "Nombre de la sala de chat",
|
||||||
"next": "Siguiente",
|
"next": "Siguiente",
|
||||||
"done": "Listo",
|
|
||||||
"new_room": "Nueva Sala",
|
"new_room": "Nueva Sala",
|
||||||
"create": "Crear",
|
"create": "Crear",
|
||||||
"room_topic": "Añade una descripción si quieres"
|
"room_topic": "Añade una descripción si quieres",
|
||||||
|
"options": "Opciones",
|
||||||
|
"room_name_limit_error_msg": "50 caracteres como máximo"
|
||||||
},
|
},
|
||||||
"room_welcome": {
|
"room_welcome": {
|
||||||
"join_public": "Cualquiera puede unirse abriendo este vínculo: {link}",
|
"join_public": "Cualquiera puede unirse abriendo este vínculo: {link}.",
|
||||||
"got_it": "Entiendo",
|
"got_it": "Entiendo",
|
||||||
"info_permissions": "Puedes cambiar los 'permisos de participación' en cualquier momento en la configuración de la sala.",
|
"info_permissions": "Puedes cambiar los 'permisos de participación' en cualquier momento en la configuración de la sala.",
|
||||||
"join_invite": "Solo personas que invitas se pueden unir.",
|
"join_invite": "Solo personas que invitas se pueden unir.",
|
||||||
"info": "Bienvenido. He aquí algunas cosas que debes saber sobre tu sala:",
|
"info": "Bienvenido. He aquí algunas cosas que debes saber sobre tu sala:",
|
||||||
"welcome": "Bienvenido!",
|
|
||||||
"room_history_is": "El historial de la sala es {type}.",
|
"room_history_is": "El historial de la sala es {type}.",
|
||||||
"encrypted": "Los mensajes están encriptados de extremo a extremo.",
|
"encrypted": "Los mensajes están encriptados de extremo a extremo.",
|
||||||
"room_history_joined": "La gente sólo puede ver los mensajes enviados después de unirse."
|
"room_history_joined": "La gente sólo puede ver los mensajes enviados después de unirse.",
|
||||||
|
"no_past_messages": "¡Bienvenido! Por su seguridad, los mensajes anteriores no están disponibles.",
|
||||||
|
"direct_info": "Hola, {you}. Estás en un chat privado con {user}.",
|
||||||
|
"direct_private_chat": "Mensaje directo"
|
||||||
},
|
},
|
||||||
"room": {
|
"room": {
|
||||||
"leave": "Salir",
|
"leave": "Salir",
|
||||||
"members": "no miembros | 1 miembro| {count} miembros",
|
"members": "no miembros | 1 miembro| {count} miembros",
|
||||||
"purge_redacting_events": "Redactar eventos",
|
"purge_redacting_events": "Redactar eventos ({count} de {total})",
|
||||||
"room_list_invites": "Invita",
|
"room_list_invites": "Invita",
|
||||||
"purge_set_room_state": "Estado de la sala",
|
"purge_set_room_state": "Estado de la sala",
|
||||||
"purge_removing_members": "Eliminar miembros",
|
"purge_removing_members": "Eliminar miembros ({count} de {total})",
|
||||||
"purge_failed": "¡Fallo en la purga de la sala!",
|
"purge_failed": "¡Fallo en la purga de la sala!",
|
||||||
"room_list_rooms": "Salas"
|
"room_list_rooms": "Salas",
|
||||||
|
"room_name_required": "Nombre de la sala obligatorio",
|
||||||
|
"room_list_new_messages": "{count} nuevos mensajes",
|
||||||
|
"invitations": "No tiene invitaciones | Tiene 1 invitación | Tiene {count} invitaciones",
|
||||||
|
"unseen_messages": "No tiene mensajes sin ver | Tiene 1 mensaje sin ver | Tiene {count} mensajes sin ver",
|
||||||
|
"room_topic_required": "Se requiere la descripción de la sala"
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"user_powerlevel_change_from_to": "{user} de {powerOld} a {powerNew}",
|
"user_powerlevel_change_from_to": "{user} de {powerOld} a {powerNew}",
|
||||||
|
|
@ -169,21 +203,43 @@
|
||||||
"edited": "(editado)",
|
"edited": "(editado)",
|
||||||
"file_prefix": "Archivo: ",
|
"file_prefix": "Archivo: ",
|
||||||
"user_said": "{user} dijo:",
|
"user_said": "{user} dijo:",
|
||||||
"user_left": "{user} abandono el chat",
|
"user_left": "{user} abandonó el chat",
|
||||||
"user_joined": "{user} se unio al chat",
|
"user_joined": "{user} se unió al chat",
|
||||||
"user_was_invited": "{user} ha sido invitado al chat...",
|
"user_was_invited": "{user} ha sido invitado al chat...",
|
||||||
"user_encrypted_room": "{user} hizo la habitación encriptada",
|
"user_encrypted_room": "{user} encriptó la habitación",
|
||||||
"user_changed_room_avatar": "{user} cambio el avatar de la sala",
|
"user_changed_room_avatar": "{user} cambió el avatar de la sala",
|
||||||
"user_changed_avatar": "{user} cambio su avatar",
|
"user_changed_avatar": "{user} cambió su avatar",
|
||||||
"user_created_room": "{user} creo la sala",
|
"user_created_room": "{user} creó la sala",
|
||||||
"user_aliased_room": "{user} hizo el alias de la sala {alias}",
|
"user_aliased_room": "{user} creó el alias de sala {alias}",
|
||||||
"user_changed_display_name": "{user}cambio su nombre para mostrar a {displayName}",
|
"user_changed_display_name": "{user} cambió su nombre a {displayName}",
|
||||||
"you": "Tú",
|
"you": "Tú",
|
||||||
"reply_image": "Imagen",
|
"reply_image": "Imagen",
|
||||||
"reply_audio_message": "Mensaje de audio",
|
"reply_audio_message": "Mensaje de audio",
|
||||||
"reply_video": "Vídeo",
|
"reply_video": "Vídeo",
|
||||||
"user_changed_guest_access_closed": "{user} no has permitido que los invitados se unan a la sala",
|
"user_changed_guest_access_closed": "{user} no has permitido que los invitados se unan a la sala",
|
||||||
"user_changed_guest_access_open": "{user} has permitido que los invitados se unieran a la sala"
|
"user_changed_guest_access_open": "{user} ha permitido que los invitados se unieran a la sala",
|
||||||
|
"seen_by": "Visto por",
|
||||||
|
"reply_poll": "Encuesta",
|
||||||
|
"file": "Archivo",
|
||||||
|
"files": "Archivos",
|
||||||
|
"images": "Imágenes",
|
||||||
|
"incoming_message_deleted_text": "Este mensaje ha sido borrado.",
|
||||||
|
"user_was_kicked": "{user} ha sido expulsado del chat.",
|
||||||
|
"send_attachements_dialog_title": "¿Desea enviar los siguientes archivos adjuntos?",
|
||||||
|
"preparing_to_upload": "Preparando para subir...",
|
||||||
|
"not_allowed_to_send": "Sólo los administradores y moderadores pueden enviar mensajes a la sala",
|
||||||
|
"reaction_count_more": "{reactionCount} más",
|
||||||
|
"user_was_kicked_you": "Has sido expulsado del chat.",
|
||||||
|
"outgoing_message_deleted_text": "Borraste este mensaje.",
|
||||||
|
"user_was_banned_you": "Fuiste expulsado y baneado del chat.",
|
||||||
|
"user_was_banned_by_you": "Has expulsado y baneado a {user} del chat.",
|
||||||
|
"seen_by_count": "No lo vió ningún miembro | Visto por 1 miembro | Visto por {count} miembros",
|
||||||
|
"user_was_banned": "{user} fue expulsado y baneado del chat.",
|
||||||
|
"download_all": "Descargar todo",
|
||||||
|
"user_was_kicked_by_you": "Has expulsado a {user} del chat.",
|
||||||
|
"upload_file_too_large": "¡El archivo es demasiado grande para subirlo!",
|
||||||
|
"time_ago": "Hoy | Ayer | Hace {count} días",
|
||||||
|
"upload_exceeded_file_limit": "Se ha superado el tamaño máximo para el archivo ({configFormattedUploadSize}). "
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"login": "Iniciar sesión",
|
"login": "Iniciar sesión",
|
||||||
|
|
@ -202,7 +258,12 @@
|
||||||
"new_room": "Nueva Sala",
|
"new_room": "Nueva Sala",
|
||||||
"loading": "Cargando {appName}",
|
"loading": "Cargando {appName}",
|
||||||
"undo": "Deshacer",
|
"undo": "Deshacer",
|
||||||
"join": "Unirse"
|
"join": "Unirse",
|
||||||
|
"user_kick": "Expulsa a este usuario",
|
||||||
|
"user_kick_and_ban": "Expulsar y banear a este usuario",
|
||||||
|
"user_make_moderator": "Hacer moderador",
|
||||||
|
"user_make_admin": "Hacer administrador",
|
||||||
|
"user_revoke_moderator": "Revocar al moderador"
|
||||||
},
|
},
|
||||||
"fallbacks": {
|
"fallbacks": {
|
||||||
"download_name": "Descargar",
|
"download_name": "Descargar",
|
||||||
|
|
@ -225,7 +286,6 @@
|
||||||
"swipe_to_cancel": "Desliza para cancelar"
|
"swipe_to_cancel": "Desliza para cancelar"
|
||||||
},
|
},
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"create_room": "Crear Grupo",
|
|
||||||
"view_details": "Ver detalles",
|
"view_details": "Ver detalles",
|
||||||
"this_room": "Esta sala"
|
"this_room": "Esta sala"
|
||||||
},
|
},
|
||||||
|
|
@ -241,7 +301,110 @@
|
||||||
"you_are": "Tu eres",
|
"you_are": "Tu eres",
|
||||||
"logout": "Cerrar sesión",
|
"logout": "Cerrar sesión",
|
||||||
"want_more": "¿Quieren más?",
|
"want_more": "¿Quieren más?",
|
||||||
"powered_by": "Esta sala funciona con {producto}. ¡Obtenga más información en {productLink} o continúe y cree otra sala!",
|
"powered_by": "Esta sala funciona con {product}. ¡Obtenga más información en {productLink} o continúe y cree otra sala!",
|
||||||
"new_room": "+ Nueva habitación"
|
"new_room": "Nueva sala de chat"
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"save": "Guardar",
|
||||||
|
"password_didnot_match": "La contraseña no coincide",
|
||||||
|
"time": {
|
||||||
|
"recently": "ahora mismo",
|
||||||
|
"minutes": "hace 1 minuto | hace {n} minutos",
|
||||||
|
"hours": "hace 1 hora | hace {n} horas",
|
||||||
|
"days": "hace 1 día | hace {n} días"
|
||||||
|
},
|
||||||
|
"show_less": "Mostrar menos",
|
||||||
|
"show_more": "Mostrar más",
|
||||||
|
"password_hint": "Mínimo de 12 caracteres que contengan al menos un número, una mayúscula y una minúscula",
|
||||||
|
"close": "cerrar",
|
||||||
|
"notify": "Notificar",
|
||||||
|
"add_reaction": "Añadir una reacción",
|
||||||
|
"click_to_remove": "Haz clic para eliminar"
|
||||||
|
},
|
||||||
|
"poll_create": {
|
||||||
|
"create": "Publicar",
|
||||||
|
"poll_submit": "Enviar",
|
||||||
|
"answer_label_n": "Respuesta",
|
||||||
|
"poll_status_open": "Encuesta abierta",
|
||||||
|
"view_results": "Ver los resultados",
|
||||||
|
"answer_label_1": "Respuesta*",
|
||||||
|
"please_complete": "Por favor, rellene",
|
||||||
|
"creating": "Creando la encuesta",
|
||||||
|
"answer_required": "La respuesta no puede estar vacía. Por favor, introduzca algún texto o elimine esta opción.",
|
||||||
|
"poll_undisclosed": "Cerrada: los usuarios verán los resultados cuando se cierre la encuesta.",
|
||||||
|
"question_label": "Formule su pregunta*",
|
||||||
|
"add_answer": "Añadir una respuesta",
|
||||||
|
"results_shared": "Resultados compartidos con la sala.",
|
||||||
|
"poll_status_open_not_voted": "La encuesta está abierta - vote para ver los resultados",
|
||||||
|
"close_poll": "Cerrar la encuesta",
|
||||||
|
"title": "Crear una nueva encuesta",
|
||||||
|
"failed": "No se ha podido crear la encuesta, inténtelo más tarde.",
|
||||||
|
"num_answered": "{count} respuestas",
|
||||||
|
"create_poll_menu_option": "Crear una encuesta",
|
||||||
|
"question_required": "¡Tienes que introducir una pregunta!",
|
||||||
|
"poll_disclosed": "Abierta: los resultados actuales se muestran en todo momento.",
|
||||||
|
"tip_title": "CONSEJO PROFESIONAL",
|
||||||
|
"tip_text": "Los miembros verán los resultados de la encuesta después de responder. Cierra la encuesta cuando hayas terminado para mostrar los resultados a todos los presentes.",
|
||||||
|
"poll_status_disclosed": "Los resultados se mostrarán cuando se cierre la encuesta.",
|
||||||
|
"poll_status_closed": "Encuesta cerrada"
|
||||||
|
},
|
||||||
|
"emoji": {
|
||||||
|
"categories": {
|
||||||
|
"objects": "Objetos",
|
||||||
|
"nature": "Naturaleza",
|
||||||
|
"symbols": "Símbolos",
|
||||||
|
"places": "Lugares",
|
||||||
|
"activity": "Actividad",
|
||||||
|
"flags": "Indicadores",
|
||||||
|
"foods": "Alimentos",
|
||||||
|
"peoples": "Personas",
|
||||||
|
"frequently": "Utilizado con frecuencia"
|
||||||
|
},
|
||||||
|
"search": "Buscar..."
|
||||||
|
},
|
||||||
|
"file_mode": {
|
||||||
|
"sending_progress": "Enviando…",
|
||||||
|
"sending": "Enviando",
|
||||||
|
"files": "Archivos",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"add_a_message": "Añadir un mensaje",
|
||||||
|
"send_more_files": "Enviar más archivos",
|
||||||
|
"choose_files": "Seleccione los archivos",
|
||||||
|
"secure_file_send": "envío seguro de los archivos",
|
||||||
|
"any_file_format_accepted": "Se admite cualquier formato para un archivo",
|
||||||
|
"files_sent_with_note": "¡1 archivo enviado con una nota! | ¡ {count} archivos enviados con una nota!",
|
||||||
|
"files_sent": "¡1 archivo enviado! | ¡ {count} archivos enviados!"
|
||||||
|
},
|
||||||
|
"getlink": {
|
||||||
|
"continue": "Continuar",
|
||||||
|
"title": "Conseguir un enlace directo",
|
||||||
|
"info": "Los enlaces directos ofrecen a la gente una línea segura de comunicación contigo. Para empezar, elige un nombre para mostrar cuando la gente entre en un chat contigo.",
|
||||||
|
"share_qr": "Compartir el QR",
|
||||||
|
"next": "Siguiente",
|
||||||
|
"scan_title": "Escanee este código para iniciar un chat privado",
|
||||||
|
"ready_to_share": "¡Ya está listo para compartir! Se abrirá una nueva sala privada cada vez que alguien abra el enlace.",
|
||||||
|
"qr_image_copied": "Imagen copiada en el portapapeles",
|
||||||
|
"hello": "Hola {user},\nAquí está su enlace directo",
|
||||||
|
"username": "Introduce un nombre de usuario (ej: waku)",
|
||||||
|
"different_link": "Obtener un enlace distinto"
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"dialog": {
|
||||||
|
"enable": "Activar",
|
||||||
|
"body": "¡No vuelvas a perderte un mensaje o una conversación importante! Recibe una notificación cada vez que alguien te envíe un mensaje o responda a tu chat.",
|
||||||
|
"title": "¡Manténgase conectado con las notificaciones para el chat!"
|
||||||
|
},
|
||||||
|
"title": "Nuevo mensaje recibido",
|
||||||
|
"blocked_message": "Notificaciones bloqueadas. Por favor, restablezca los permisos"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"fetched_n_of_total_events": "{count} de {total} eventos recuperados",
|
||||||
|
"export_filename": "Chat exportado el {date}",
|
||||||
|
"exported_date": "Exportado el {date}",
|
||||||
|
"fetched_n_events": "{count} evento(s) recuperado(s)",
|
||||||
|
"processed_n_of_total_events": "Medios procesados para {count} de {total} eventos"
|
||||||
|
},
|
||||||
|
"logout": {
|
||||||
|
"confirm_text": "¿Seguro que quieres cerrar la sesión?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
"global": {
|
|
||||||
},
|
|
||||||
"menu": {
|
"menu": {
|
||||||
"back": "TAKAISIN",
|
"back": "TAKAISIN",
|
||||||
"start_private_chat": "Yksityinen keskustelu tämän käyttäjän kanssa",
|
"start_private_chat": "Yksityinen keskustelu tämän käyttäjän kanssa",
|
||||||
|
|
@ -81,12 +79,10 @@
|
||||||
"leave": "Poistu",
|
"leave": "Poistu",
|
||||||
"text_invite": "Tämä huone on lukittu. Et pääse takaisin ilman erillistä lupaa."
|
"text_invite": "Tämä huone on lukittu. Et pääse takaisin ilman erillistä lupaa."
|
||||||
},
|
},
|
||||||
"logout": {
|
|
||||||
},
|
|
||||||
"message": {
|
"message": {
|
||||||
"you": "Sinä",
|
"you": "Sinä",
|
||||||
"user_created_room": "{user} loi huoneen",
|
"user_created_room": "{user} loi huoneen",
|
||||||
"user_left": "{käyttäjä} poistui keskustelusta",
|
"user_left": "{user} poistui keskustelusta",
|
||||||
"user_said": "{user} sanoi:",
|
"user_said": "{user} sanoi:",
|
||||||
"download_progress": "{percentage} % ladattu",
|
"download_progress": "{percentage} % ladattu",
|
||||||
"unread_messages": "Lukemattomat viestit",
|
"unread_messages": "Lukemattomat viestit",
|
||||||
|
|
@ -158,6 +154,7 @@
|
||||||
"view_details": "Näytä tiedot"
|
"view_details": "Näytä tiedot"
|
||||||
},
|
},
|
||||||
"voice_recorder": {
|
"voice_recorder": {
|
||||||
"swipe_to_cancel": "Peruuta pyyhkäisemällä"
|
"swipe_to_cancel": "Peruuta pyyhkäisemällä",
|
||||||
|
"not_supported_title": "Ei tuettu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
"global": {
|
|
||||||
},
|
|
||||||
"menu": {
|
"menu": {
|
||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
"start_private_chat": "Discussion privée avec cet utilisateur",
|
"start_private_chat": "Discussion privée avec cet utilisateur",
|
||||||
|
|
@ -42,7 +40,6 @@
|
||||||
"user_changed_join_rules": "{user} a rendu le salon {type}",
|
"user_changed_join_rules": "{user} a rendu le salon {type}",
|
||||||
"user_changed_room_name": "{user} a changé le nom du salon en {name}",
|
"user_changed_room_name": "{user} a changé le nom du salon en {name}",
|
||||||
"user_changed_room_topic": "{user} a changé le thème du salon en {topic}",
|
"user_changed_room_topic": "{user} a changé le thème du salon en {topic}",
|
||||||
"replying_to_event": "RÉPONSE À L’ÉVÈNEMENT : {message}",
|
|
||||||
"your_message": "Votre message…",
|
"your_message": "Votre message…",
|
||||||
"scale_image": "Image à l’échelle",
|
"scale_image": "Image à l’échelle",
|
||||||
"user_is_typing": "{user} écrit",
|
"user_is_typing": "{user} écrit",
|
||||||
|
|
@ -162,8 +159,6 @@
|
||||||
"text_invite": "Ce salon est verrouillé. Vous ne pouvez pas le rejoindre sans une autorisation spéciale.",
|
"text_invite": "Ce salon est verrouillé. Vous ne pouvez pas le rejoindre sans une autorisation spéciale.",
|
||||||
"go_back": "Retour"
|
"go_back": "Retour"
|
||||||
},
|
},
|
||||||
"logout": {
|
|
||||||
},
|
|
||||||
"purge_room": {
|
"purge_room": {
|
||||||
"title": "Supprimer le salon ?",
|
"title": "Supprimer le salon ?",
|
||||||
"info": "Tous les membres et les messages seront supprimés. Cette action ne peut être annulée.",
|
"info": "Tous les membres et les messages seront supprimés. Cette action ne peut être annulée.",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
"global": {
|
|
||||||
},
|
|
||||||
"message": {
|
"message": {
|
||||||
"file_prefix": "File: ",
|
"file_prefix": "File: ",
|
||||||
"unread_messages": "Messaggi non letti",
|
"unread_messages": "Messaggi non letti",
|
||||||
|
|
@ -20,7 +18,6 @@
|
||||||
"user_changed_join_rules": "{user} ha reso la stanza {type}",
|
"user_changed_join_rules": "{user} ha reso la stanza {type}",
|
||||||
"room_joinrule_invite": "solo su invito",
|
"room_joinrule_invite": "solo su invito",
|
||||||
"user_changed_room_name": "{user} ha cambiato il nome della stanza in {name}",
|
"user_changed_room_name": "{user} ha cambiato il nome della stanza in {name}",
|
||||||
"replying_to_event": "RISPOSTA ALL’EVENTO: {message}",
|
|
||||||
"your_message": "Il tuo messaggio…",
|
"your_message": "Il tuo messaggio…",
|
||||||
"scale_image": "Ridimensiona l’immagine",
|
"scale_image": "Ridimensiona l’immagine",
|
||||||
"room_powerlevel_change": "{user} ha cambiato lo statuto di {changes}",
|
"room_powerlevel_change": "{user} ha cambiato lo statuto di {changes}",
|
||||||
|
|
@ -88,7 +85,7 @@
|
||||||
"link_copied": "Collegamento copiato!",
|
"link_copied": "Collegamento copiato!",
|
||||||
"invite_description": "Scegli da un elenco o cerca per identificativo di account",
|
"invite_description": "Scegli da un elenco o cerca per identificativo di account",
|
||||||
"status_creating": "Creazione della stanza",
|
"status_creating": "Creazione della stanza",
|
||||||
"status_avatar_total": "Caricamento dell’avatar: {conteggio} di {totale}",
|
"status_avatar_total": "Caricamento dell’avatar: {count} di {total}",
|
||||||
"status_avatar": "Caricamento dell’avatar: {count}",
|
"status_avatar": "Caricamento dell’avatar: {count}",
|
||||||
"create": "Crea",
|
"create": "Crea",
|
||||||
"public_description": "Ottieni un collegamento da condividere",
|
"public_description": "Ottieni un collegamento da condividere",
|
||||||
|
|
@ -161,8 +158,6 @@
|
||||||
"leave": "Lascia",
|
"leave": "Lascia",
|
||||||
"text_public_lastroom": "Se vuoi unirti di nuovo a questa stanza, puoi farlo con una nuova identità. Per mantenere {user}, {action}."
|
"text_public_lastroom": "Se vuoi unirti di nuovo a questa stanza, puoi farlo con una nuova identità. Per mantenere {user}, {action}."
|
||||||
},
|
},
|
||||||
"logout": {
|
|
||||||
},
|
|
||||||
"purge_room": {
|
"purge_room": {
|
||||||
"info": "Tutti i membri e i messaggi saranno rimossi. Questa azione non può essere annullata.",
|
"info": "Tutti i membri e i messaggi saranno rimossi. Questa azione non può essere annullata.",
|
||||||
"button": "Elimina",
|
"button": "Elimina",
|
||||||
|
|
@ -220,5 +215,8 @@
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"this_room": "Questa stanza",
|
"this_room": "Questa stanza",
|
||||||
"view_details": "Visualizza i dettagli"
|
"view_details": "Visualizza i dettagli"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"name": "Convene"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
"project": {
|
"project": {
|
||||||
"name": "Convene"
|
"name": "Convene"
|
||||||
},
|
},
|
||||||
"global": {
|
|
||||||
},
|
|
||||||
"message": {
|
"message": {
|
||||||
"user_changed_guest_access_open": "{user} tillot gjester å ta del i rommet",
|
"user_changed_guest_access_open": "{user} tillot gjester å ta del i rommet",
|
||||||
"user_powerlevel_change_from_to": "{user} fra {powerOld} til {powerNew}",
|
"user_powerlevel_change_from_to": "{user} fra {powerOld} til {powerNew}",
|
||||||
|
|
@ -62,7 +60,8 @@
|
||||||
"link_copied": "Lenke kopiert.",
|
"link_copied": "Lenke kopiert.",
|
||||||
"copy_invite_link": "Kopier invitasjonslenke",
|
"copy_invite_link": "Kopier invitasjonslenke",
|
||||||
"created_by": "Opprettet av {user}",
|
"created_by": "Opprettet av {user}",
|
||||||
"title": "Romdetaljer"
|
"title": "Romdetaljer",
|
||||||
|
"user": "{user}"
|
||||||
},
|
},
|
||||||
"goodbye": {
|
"goodbye": {
|
||||||
"view_other_rooms": "Vis andre rom",
|
"view_other_rooms": "Vis andre rom",
|
||||||
|
|
@ -127,8 +126,6 @@
|
||||||
"create_account": "opprett en konto",
|
"create_account": "opprett en konto",
|
||||||
"title_public": "Adjø, {user}"
|
"title_public": "Adjø, {user}"
|
||||||
},
|
},
|
||||||
"logout": {
|
|
||||||
},
|
|
||||||
"invite": {
|
"invite": {
|
||||||
"status_inviting": "Inviterer venn {index} av {count}",
|
"status_inviting": "Inviterer venn {index} av {count}",
|
||||||
"send_invites_to": "Send invitasjoner til",
|
"send_invites_to": "Send invitasjoner til",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,15 @@
|
||||||
"show_less": "Mostrar menos",
|
"show_less": "Mostrar menos",
|
||||||
"show_more": "Mostrar mais",
|
"show_more": "Mostrar mais",
|
||||||
"add_reaction": "Adicionar reação",
|
"add_reaction": "Adicionar reação",
|
||||||
"click_to_remove": "Clique para remover"
|
"click_to_remove": "Clique para remover",
|
||||||
|
"time": {
|
||||||
|
"hours": "1 hora atrás | {n} horas atrás",
|
||||||
|
"days": "1 dia atrás | {n} dias atrás",
|
||||||
|
"recently": "agora mesmo",
|
||||||
|
"minutes": "1 minuto atrás | {n} minutos atrás"
|
||||||
|
},
|
||||||
|
"close": "fechar",
|
||||||
|
"notify": "Notificação"
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"title": "Adiciona amigos",
|
"title": "Adiciona amigos",
|
||||||
|
|
@ -101,7 +109,17 @@
|
||||||
"user_was_banned": "{user} foi expulso e banido do chat.",
|
"user_was_banned": "{user} foi expulso e banido do chat.",
|
||||||
"user_was_kicked": "{user} foi expulso do chat.",
|
"user_was_kicked": "{user} foi expulso do chat.",
|
||||||
"user_was_banned_you": "Você foi expulso e banido do chat.",
|
"user_was_banned_you": "Você foi expulso e banido do chat.",
|
||||||
"reaction_count_more": "{reactionCount} mais"
|
"reaction_count_more": "{reactionCount} mais",
|
||||||
|
"seen_by": "Visto por",
|
||||||
|
"file": "Arquivo",
|
||||||
|
"send_attachements_dialog_title": "Deseja enviar os seguintes anexos?",
|
||||||
|
"upload_file_too_large": "O arquivo é grande demais para ser enviado!",
|
||||||
|
"images": "Imagens",
|
||||||
|
"seen_by_count": "Visto por nenhum membro | Visto por 1 membro | Visto por {count} membros",
|
||||||
|
"files": "Arquivos",
|
||||||
|
"download_all": "Baixar tudo",
|
||||||
|
"upload_exceeded_file_limit": "O tamanho máximo do arquivo ({configFormattedUploadSize}) foi excedido. ",
|
||||||
|
"preparing_to_upload": "Preparando para enviar..."
|
||||||
},
|
},
|
||||||
"room": {
|
"room": {
|
||||||
"members": "sem membros | 1 membro | {count} membros",
|
"members": "sem membros | 1 membro | {count} membros",
|
||||||
|
|
@ -126,7 +144,10 @@
|
||||||
"join_public": "Qualquer pessoa pode participar abrindo este link: {link}.",
|
"join_public": "Qualquer pessoa pode participar abrindo este link: {link}.",
|
||||||
"join_invite": "Apenas as pessoas que você convidar podem participar.",
|
"join_invite": "Apenas as pessoas que você convidar podem participar.",
|
||||||
"info_permissions": "Você pode alterar as \"permissões de participação\" a qualquer momento nas configurações da sala.",
|
"info_permissions": "Você pode alterar as \"permissões de participação\" a qualquer momento nas configurações da sala.",
|
||||||
"got_it": "Entendi"
|
"got_it": "Entendi",
|
||||||
|
"no_past_messages": "Bem-vindo! Para a sua segurança, as mensagens anteriores não estão disponíveis.",
|
||||||
|
"direct_info": "Olá {you}. Você está num chat privado com {user}.",
|
||||||
|
"direct_private_chat": "Mensagem direta"
|
||||||
},
|
},
|
||||||
"new_room": {
|
"new_room": {
|
||||||
"new_room": "Nova sala",
|
"new_room": "Nova sala",
|
||||||
|
|
@ -174,7 +195,10 @@
|
||||||
"resend_verification": "Reenviar o e-mail de verificação",
|
"resend_verification": "Reenviar o e-mail de verificação",
|
||||||
"email_not_valid": "O endereço de e-mail não é válido",
|
"email_not_valid": "O endereço de e-mail não é válido",
|
||||||
"sent_verification": "Um e-mail foi enviado para {email}. Use o seu cliente de e-mail para verificar o endereço.",
|
"sent_verification": "Um e-mail foi enviado para {email}. Use o seu cliente de e-mail para verificar o endereço.",
|
||||||
"no_supported_flow": "O aplicativo não pode fazer login no servidor informado"
|
"no_supported_flow": "O aplicativo não pode fazer login no servidor informado",
|
||||||
|
"registration_token": "Insira o token de registro",
|
||||||
|
"send_token": "Enviar o token",
|
||||||
|
"token_not_valid": "Token inválido"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Meu perfil",
|
"title": "Meu perfil",
|
||||||
|
|
@ -216,7 +240,8 @@
|
||||||
"status_logging_in": "Fazendo login...",
|
"status_logging_in": "Fazendo login...",
|
||||||
"status_joining": "Entrando na sala...",
|
"status_joining": "Entrando na sala...",
|
||||||
"join_failed": "Houve uma falha ao entrar na sala.",
|
"join_failed": "Houve uma falha ao entrar na sala.",
|
||||||
"choose_name": "Escolha um nome para usar"
|
"choose_name": "Escolha um nome para usar",
|
||||||
|
"you_have_been_banned": "Você foi banido desta sala."
|
||||||
},
|
},
|
||||||
"leave": {
|
"leave": {
|
||||||
"title_public": "Adeus, {user}",
|
"title_public": "Adeus, {user}",
|
||||||
|
|
@ -271,7 +296,16 @@
|
||||||
"user_moderator": "Moderador",
|
"user_moderator": "Moderador",
|
||||||
"experimental_features": "Recursos experimentais",
|
"experimental_features": "Recursos experimentais",
|
||||||
"download_chat": "Baixar o chat",
|
"download_chat": "Baixar o chat",
|
||||||
"read_only_room_info": "Apenas administradores e moderadores podem postar na sala"
|
"read_only_room_info": "Apenas administradores e moderadores podem postar na sala",
|
||||||
|
"copy_link": "Copiar o link",
|
||||||
|
"make_public": "Tornar público",
|
||||||
|
"room_type": "Tipo de sala",
|
||||||
|
"file_mode_info": "Alterna a interface de bate-papo para um modo de 'baixa de arquivo'",
|
||||||
|
"make_public_warning": "aviso: o histórico completo das mensagens ficará visível para os novos participantes",
|
||||||
|
"room_type_default": "Padrão",
|
||||||
|
"file_mode": "Modo de arquivo",
|
||||||
|
"direct_link": "Meu link direto",
|
||||||
|
"direct_link_desc": "Ele está pronto para ser compartilhado! Uma nova sala com contato direto com você será aberta sempre que alguém abrir o link."
|
||||||
},
|
},
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"this_room": "Esta sala",
|
"this_room": "Esta sala",
|
||||||
|
|
@ -293,7 +327,6 @@
|
||||||
},
|
},
|
||||||
"poll_create": {
|
"poll_create": {
|
||||||
"title": "Criar uma nova enquete",
|
"title": "Criar uma nova enquete",
|
||||||
"intro": "Preencha os detalhes abaixo.",
|
|
||||||
"create": "Publicar",
|
"create": "Publicar",
|
||||||
"creating": "Criando a enquete",
|
"creating": "Criando a enquete",
|
||||||
"poll_disclosed": "Aberto - os resultados atuais são mostrados em todos os momentos.",
|
"poll_disclosed": "Aberto - os resultados atuais são mostrados em todos os momentos.",
|
||||||
|
|
@ -301,7 +334,6 @@
|
||||||
"failed": "Houve uma falha ao criar a enquete. Tente novamente mais tarde.",
|
"failed": "Houve uma falha ao criar a enquete. Tente novamente mais tarde.",
|
||||||
"question_label": "Faça a sua pergunta*",
|
"question_label": "Faça a sua pergunta*",
|
||||||
"question_required": "Você precisa inserir uma pergunta!",
|
"question_required": "Você precisa inserir uma pergunta!",
|
||||||
"answer_label": "Responda sem {index}",
|
|
||||||
"answer_required": "A resposta não pode estar vazia. Insira algum texto ou remova esta opção.",
|
"answer_required": "A resposta não pode estar vazia. Insira algum texto ou remova esta opção.",
|
||||||
"create_poll_menu_option": "Criar uma enquete",
|
"create_poll_menu_option": "Criar uma enquete",
|
||||||
"poll_status_closed": "A enquete foi encerrada",
|
"poll_status_closed": "A enquete foi encerrada",
|
||||||
|
|
@ -326,5 +358,54 @@
|
||||||
"fetched_n_of_total_events": "Obteve {count} de {total} eventos",
|
"fetched_n_of_total_events": "Obteve {count} de {total} eventos",
|
||||||
"processed_n_of_total_events": "Mídia processada para {count} de {total} eventos",
|
"processed_n_of_total_events": "Mídia processada para {count} de {total} eventos",
|
||||||
"export_filename": "Bate-papo exportado {date}"
|
"export_filename": "Bate-papo exportado {date}"
|
||||||
|
},
|
||||||
|
"emoji": {
|
||||||
|
"search": "Buscar...",
|
||||||
|
"categories": {
|
||||||
|
"foods": "Alimentos",
|
||||||
|
"objects": "Objetos",
|
||||||
|
"symbols": "Símbolos",
|
||||||
|
"places": "Locais",
|
||||||
|
"peoples": "Pessoas",
|
||||||
|
"activity": "Atividade",
|
||||||
|
"flags": "Marcadores",
|
||||||
|
"frequently": "Usado com frequência",
|
||||||
|
"nature": "Natural"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file_mode": {
|
||||||
|
"choose_files": "Escolha os arquivos",
|
||||||
|
"secure_file_send": "Envio seguro de arquivos",
|
||||||
|
"sending_progress": "Enviando...",
|
||||||
|
"files_sent": "1 arquivo enviado! | {count} arquivos enviados!",
|
||||||
|
"files_sent_with_note": "1 arquivo enviado com uma nota! | {count} arquivos enviados com uma nota!",
|
||||||
|
"add_a_message": "Adicionar uma mensagem",
|
||||||
|
"sending": "Enviando",
|
||||||
|
"any_file_format_accepted": "Qualquer formato de arquivo é aceito",
|
||||||
|
"files": "Arquivos",
|
||||||
|
"close": "Fechar",
|
||||||
|
"send_more_files": "Enviar mais arquivos"
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"title": "Uma nova mensagem foi recebida",
|
||||||
|
"dialog": {
|
||||||
|
"body": "Nunca mais perca uma mensagem ou uma conversa importante! Seja notificado sempre que alguém lhe enviar uma mensagem ou responder ao seu bate-papo.",
|
||||||
|
"title": "Manter-se conectado com as notificações do chat!",
|
||||||
|
"enable": "Ativar"
|
||||||
|
},
|
||||||
|
"blocked_message": "Notificações bloqueadas. Redefina as permissões"
|
||||||
|
},
|
||||||
|
"getlink": {
|
||||||
|
"title": "Obter um link direto",
|
||||||
|
"scan_title": "Leia este código para iniciar um bate-papo direto",
|
||||||
|
"ready_to_share": "Ele está pronto para ser compartilhado! Uma nova sala será aberta sempre que alguém abrir o link.",
|
||||||
|
"qr_image_copied": "A imagem foi copiada para a área de transferência",
|
||||||
|
"hello": "Olá {user},\nAqui está o seu link direto",
|
||||||
|
"continue": "Continuar",
|
||||||
|
"info": "Os links diretos oferecem às pessoas uma linha segura de comunicação com você. Para começar, escolha um nome que será exibido quando as pessoas entrarem num chat com você.",
|
||||||
|
"share_qr": "Compartilhar um código QR",
|
||||||
|
"username": "Insira um nome (ex: waku)",
|
||||||
|
"different_link": "Obter um link diferente",
|
||||||
|
"next": "Próximo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
"name": "Convene",
|
"name": "Convene",
|
||||||
"tag_line": "Conectați pur și simplu"
|
"tag_line": "Conectați pur și simplu"
|
||||||
},
|
},
|
||||||
"global": {
|
|
||||||
},
|
|
||||||
"menu": {
|
"menu": {
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"done": "Realizat",
|
"done": "Realizat",
|
||||||
|
|
@ -26,7 +24,7 @@
|
||||||
},
|
},
|
||||||
"power_level": {
|
"power_level": {
|
||||||
"restricted": "restricționat",
|
"restricted": "restricționat",
|
||||||
"custom": "personalizat ({nivel})",
|
"custom": "personalizat ({level})",
|
||||||
"default": "implicit",
|
"default": "implicit",
|
||||||
"moderator": "coordonator",
|
"moderator": "coordonator",
|
||||||
"admin": "administrator"
|
"admin": "administrator"
|
||||||
|
|
@ -85,8 +83,6 @@
|
||||||
"text_public": "Puteți oricând să vă alăturați din nou acestei camere dacă știți link-ul.",
|
"text_public": "Puteți oricând să vă alăturați din nou acestei camere dacă știți link-ul.",
|
||||||
"title_public": "La revedere, {user}"
|
"title_public": "La revedere, {user}"
|
||||||
},
|
},
|
||||||
"logout": {
|
|
||||||
},
|
|
||||||
"invite": {
|
"invite": {
|
||||||
"status_error": "Nu ați reușit să invitați unul sau mai mulți prieteni!",
|
"status_error": "Nu ați reușit să invitați unul sau mai mulți prieteni!",
|
||||||
"status_inviting": "Invitați prietenul {index} din {count}",
|
"status_inviting": "Invitați prietenul {index} din {count}",
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,7 @@
|
||||||
"file_prefix": "ගොනුව: ",
|
"file_prefix": "ගොනුව: ",
|
||||||
"you": "ඔබ"
|
"you": "ඔබ"
|
||||||
},
|
},
|
||||||
"login": {
|
"room_info": {
|
||||||
},
|
"user": "{user}"
|
||||||
"join": {
|
|
||||||
},
|
|
||||||
"logout": {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
"project": {
|
"project": {
|
||||||
"name": "Convene"
|
"name": "Convene"
|
||||||
},
|
},
|
||||||
"global": {
|
|
||||||
},
|
|
||||||
"language_is_rtl": true,
|
|
||||||
"menu": {
|
"menu": {
|
||||||
"ok": "تامام",
|
"ok": "تامام",
|
||||||
"download": "چۈشۈرۈش",
|
"download": "چۈشۈرۈش",
|
||||||
|
|
@ -24,7 +21,7 @@
|
||||||
"start_private_chat": "قوللانغۇچى بىلەن شەخسى ئۇچۇرلاشماق"
|
"start_private_chat": "قوللانغۇچى بىلەن شەخسى ئۇچۇرلاشماق"
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"upload_progress_with_total": "{number} نىڭ {total} يۈكلەندى",
|
"upload_progress_with_total": "{count} نىڭ {total} يۈكلەندى",
|
||||||
"upload_progress": "يۈكلەندى {count}",
|
"upload_progress": "يۈكلەندى {count}",
|
||||||
"download_progress": "{percentage}% چۈشۈرۈلدى",
|
"download_progress": "{percentage}% چۈشۈرۈلدى",
|
||||||
"edited": "تەھرىرلەندى",
|
"edited": "تەھرىرلەندى",
|
||||||
|
|
@ -74,7 +71,7 @@
|
||||||
"purge_redacting_events": "پائالىيەتلەرنى تەھرىرلەش",
|
"purge_redacting_events": "پائالىيەتلەرنى تەھرىرلەش",
|
||||||
"purge_set_room_state": "مۇنازىرەخانىنىڭ شەرتىنى قۇرۇش",
|
"purge_set_room_state": "مۇنازىرەخانىنىڭ شەرتىنى قۇرۇش",
|
||||||
"leave": "كېتىش",
|
"leave": "كېتىش",
|
||||||
"members": "ئەزالار يوق | بىر ئەزا | [نەپەر] ئەزا"
|
"members": "ئەزالار يوق | بىر ئەزا | {count} ئەزا"
|
||||||
},
|
},
|
||||||
"leave": {
|
"leave": {
|
||||||
"text_public_lastroom": "ئەگەر بۇ ئۆيگە يەنە قوشۇلماقچى بولسىڭىز ، يېڭى سالاھىيەت ئاستىدا قاتناشسىڭىز بولىدۇ. {user} ، {action} نى ساقلاش.",
|
"text_public_lastroom": "ئەگەر بۇ ئۆيگە يەنە قوشۇلماقچى بولسىڭىز ، يېڭى سالاھىيەت ئاستىدا قاتناشسىڭىز بولىدۇ. {user} ، {action} نى ساقلاش.",
|
||||||
|
|
@ -86,8 +83,6 @@
|
||||||
"text_public": "ئۇلىنىشنى بىلسىڭىز ھەمىشە بۇ ئۆيگە قايتا كىرەلەيسىز.",
|
"text_public": "ئۇلىنىشنى بىلسىڭىز ھەمىشە بۇ ئۆيگە قايتا كىرەلەيسىز.",
|
||||||
"title_public": "خەير خوش ، {user}"
|
"title_public": "خەير خوش ، {user}"
|
||||||
},
|
},
|
||||||
"logout": {
|
|
||||||
},
|
|
||||||
"join": {
|
"join": {
|
||||||
"join_failed": "مۇنازىرە ئۆيىگە قوشۇلۇش مەغلۇب بولدى.",
|
"join_failed": "مۇنازىرە ئۆيىگە قوشۇلۇش مەغلۇب بولدى.",
|
||||||
"status_joining": "مۇنازىرىگە كىرىش...",
|
"status_joining": "مۇنازىرىگە كىرىش...",
|
||||||
|
|
@ -96,14 +91,13 @@
|
||||||
"join": "مۇنازىرىگە قوشۇلۇڭ",
|
"join": "مۇنازىرىگە قوشۇلۇڭ",
|
||||||
"joining_as": "سىز تۆۋەندىكىدەك قاتنىشىۋاتىسىز:",
|
"joining_as": "سىز تۆۋەندىكىدەك قاتنىشىۋاتىسىز:",
|
||||||
"user_name_label": "قوللانغۇچى ئىسمى",
|
"user_name_label": "قوللانغۇچى ئىسمى",
|
||||||
"title": "{ياتاق ئىسمى} غا خۇش كەپسىز",
|
"title": "{ياتاق ئىسمى} غا خۇش كەپسىز"
|
||||||
"user_name_label": "قوللانغۇچى ئىسمى"
|
|
||||||
},
|
},
|
||||||
"room_welcome": {
|
"room_welcome": {
|
||||||
"info_permissions": "ياتاق تەڭشىكىدە خالىغان ۋاقىتتا «قوشۇلۇش ئىجازەتنامىسى» نى ئۆزگەرتەلەيسىز.",
|
"info_permissions": "ياتاق تەڭشىكىدە خالىغان ۋاقىتتا «قوشۇلۇش ئىجازەتنامىسى» نى ئۆزگەرتەلەيسىز.",
|
||||||
"got_it": "چۈشەندىم",
|
"got_it": "چۈشەندىم",
|
||||||
"join_invite": "سىز تەكلىپ قىلغان كىشىلەرلا قاتناشسا بولىدۇ.",
|
"join_invite": "سىز تەكلىپ قىلغان كىشىلەرلا قاتناشسا بولىدۇ.",
|
||||||
"join_public": "ھەركىم بۇ ئۇلىنىشنى ئېچىش ئارقىلىق قوشۇلالايدۇ: {ئۇلىنىش}.",
|
"join_public": "ھەركىم بۇ ئۇلىنىشنى ئېچىش ئارقىلىق قوشۇلالايدۇ: {link}.",
|
||||||
"room_history_joined": "ئەزالارقوشۇلغاندىن كېيىنلا ئەۋەتىلگەن ئۇچۇرلارنى كۆرەلەيدۇ.",
|
"room_history_joined": "ئەزالارقوشۇلغاندىن كېيىنلا ئەۋەتىلگەن ئۇچۇرلارنى كۆرەلەيدۇ.",
|
||||||
"room_history_is": "مۇنازىرەخانا تارىخى {type}.",
|
"room_history_is": "مۇنازىرەخانا تارىخى {type}.",
|
||||||
"encrypted": "ئۇچۇرلار ئاخىرىغىچە مەخپىيلەشتۈرۈلگەن.",
|
"encrypted": "ئۇچۇرلار ئاخىرىغىچە مەخپىيلەشتۈرۈلگەن.",
|
||||||
|
|
@ -139,8 +133,8 @@
|
||||||
"leave_room": "ئايرىلماق",
|
"leave_room": "ئايرىلماق",
|
||||||
"show_all": "<ھەممىنى كۆرسەتمەك",
|
"show_all": "<ھەممىنى كۆرسەتمەك",
|
||||||
"hide_all": "يوشۇرۇن",
|
"hide_all": "يوشۇرۇن",
|
||||||
"user_you": "قوللانغۇچى ( سىز)",
|
"user_you": "{user} ( سىز)",
|
||||||
"user": "قوللانغۇچى",
|
"user": "{user}",
|
||||||
"members": "ئەزالار",
|
"members": "ئەزالار",
|
||||||
"purge": "ئۆينى ئۆچۈرۈڭ",
|
"purge": "ئۆينى ئۆچۈرۈڭ",
|
||||||
"link_copied": "ئۇلىنىش كۆچۈرۈلدى!",
|
"link_copied": "ئۇلىنىش كۆچۈرۈلدى!",
|
||||||
|
|
@ -174,13 +168,13 @@
|
||||||
"title": "دوست قوشۇڭ"
|
"title": "دوست قوشۇڭ"
|
||||||
},
|
},
|
||||||
"profile_info_popup": {
|
"profile_info_popup": {
|
||||||
"new_room": "+ يېڭى ئۆي",
|
"new_room": "يېڭى ئۆي",
|
||||||
"powered_by": "بۇ مۇنازىرە ئۆيى {product} ئىشلىتىلگەن.xx دىن تېخىمۇ كۆپ بىلىمگە ئېرىشىڭ ياكى ئىلگىرىلەپ باشقا ئۆي قۇرۇڭ!",
|
"powered_by": "بۇ مۇنازىرە ئۆيى {product} ئىشلىتىلگەن.xx دىن تېخىمۇ كۆپ بىلىمگە ئېرىشىڭ ياكى ئىلگىرىلەپ باشقا ئۆي قۇرۇڭ!",
|
||||||
"want_more": "تېخىمۇ كۆپ خالامسىز؟",
|
"want_more": "تېخىمۇ كۆپ خالامسىز؟",
|
||||||
"logout": "چېكىنىش",
|
"logout": "چېكىنىش",
|
||||||
"edit_profile": "ئارخىپنى تەھرىرلەش",
|
"edit_profile": "ئارخىپنى تەھرىرلەش",
|
||||||
"identity_temporary": "{displayName}",
|
"identity_temporary": "{displayName}",
|
||||||
"identity": "{displayName}",
|
"identity": "{displayName}",
|
||||||
"you_are": "سىز"
|
"you_are": "سىز"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,7 @@
|
||||||
"hide_all": "隐藏",
|
"hide_all": "隐藏",
|
||||||
"title": "聊天室详情",
|
"title": "聊天室详情",
|
||||||
"version_info": "由守护者计划提供支持.版本:{version}",
|
"version_info": "由守护者计划提供支持.版本:{version}",
|
||||||
"leave_room_info": "注意:此步骤无法撤消。 确保您要注销并永久删除聊天记录。",
|
|
||||||
"leave_room": "离开",
|
"leave_room": "离开",
|
||||||
"view_profile": "查看",
|
|
||||||
"identity_temporary": "您的身份 {displayName} 是临时的。 您可以更改您的姓名或设置密码来保留它。",
|
|
||||||
"identity": "您以 {displayName} 的身份登录。",
|
|
||||||
"my_profile": "我的简历",
|
|
||||||
"show_all": "显示所有 >",
|
"show_all": "显示所有 >",
|
||||||
"user_you": "{user} (你)",
|
"user_you": "{user} (你)",
|
||||||
"user": "{user}",
|
"user": "{user}",
|
||||||
|
|
@ -183,7 +178,6 @@
|
||||||
"swipe_to_cancel": "滑动取消"
|
"swipe_to_cancel": "滑动取消"
|
||||||
},
|
},
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"create_room": "创建群组",
|
|
||||||
"view_details": "查看详情",
|
"view_details": "查看详情",
|
||||||
"this_room": "这个聊天室"
|
"this_room": "这个聊天室"
|
||||||
},
|
},
|
||||||
|
|
@ -252,7 +246,6 @@
|
||||||
"join_permissions": "加入权限",
|
"join_permissions": "加入权限",
|
||||||
"name_room": "聊天室名称",
|
"name_room": "聊天室名称",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"done": "完毕",
|
|
||||||
"new_room": "新的聊天室",
|
"new_room": "新的聊天室",
|
||||||
"room_topic": "如果您愿意,请添加说明",
|
"room_topic": "如果您愿意,请添加说明",
|
||||||
"create": "创建",
|
"create": "创建",
|
||||||
|
|
@ -266,7 +259,6 @@
|
||||||
"info": "欢迎!关于您的群组,您需要了解以下几点:",
|
"info": "欢迎!关于您的群组,您需要了解以下几点:",
|
||||||
"join_invite": "只有您邀请的人可以加入。",
|
"join_invite": "只有您邀请的人可以加入。",
|
||||||
"join_public": "任何人都可以加入通过打开此链接: {link}。",
|
"join_public": "任何人都可以加入通过打开此链接: {link}。",
|
||||||
"welcome": "欢迎!",
|
|
||||||
"room_history_joined": "只有加入后,人们才可以看到发送的信息。",
|
"room_history_joined": "只有加入后,人们才可以看到发送的信息。",
|
||||||
"room_history_is": "聊天室纪录是{type}.",
|
"room_history_is": "聊天室纪录是{type}.",
|
||||||
"encrypted": "信息是端到端加密的。"
|
"encrypted": "信息是端到端加密的。"
|
||||||
|
|
@ -294,7 +286,11 @@
|
||||||
"show_less": "显示较少",
|
"show_less": "显示较少",
|
||||||
"show_more": "展示更多",
|
"show_more": "展示更多",
|
||||||
"add_reaction": "添加反应",
|
"add_reaction": "添加反应",
|
||||||
"click_to_remove": "点击删除"
|
"click_to_remove": "点击删除",
|
||||||
|
"time": {
|
||||||
|
"recently": "刚才"
|
||||||
|
},
|
||||||
|
"close": "关闭"
|
||||||
},
|
},
|
||||||
"logout": {
|
"logout": {
|
||||||
"confirm_text": "您确定要注销吗?"
|
"confirm_text": "您确定要注销吗?"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
no-gutters
|
no-gutters
|
||||||
align-content="center"
|
align-content="center"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
|
v-show="icon === 'notifications_active' ? this.windowNotificationPermission() !== 'granted' : true"
|
||||||
>
|
>
|
||||||
<v-col cols="auto" class="me-2">
|
<v-col cols="auto" class="me-2">
|
||||||
<v-icon :size="iconSize">{{ icon }}</v-icon>
|
<v-icon :size="iconSize">{{ icon }}</v-icon>
|
||||||
|
|
@ -13,6 +14,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ActionRow",
|
name: "ActionRow",
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -35,6 +38,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
windowNotificationPermission
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
<v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon>
|
<v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-root fill-height d-flex flex-column">
|
<div class="chat-root fill-height d-flex flex-column" :style="chatContainerStyle">
|
||||||
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" v-on:view-room-details="viewRoomDetails" v-if="!useFileModeNonAdmin" />
|
<ChatHeaderPrivate class="chat-header flex-grow-0 flex-shrink-0"
|
||||||
|
v-on:header-click="onHeaderClick"
|
||||||
|
v-on:view-room-details="viewRoomDetails"
|
||||||
|
v-on:notify="onNotificationDialog"
|
||||||
|
v-if="!useFileModeNonAdmin && $matrix.isDirectRoom(room)" />
|
||||||
|
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0"
|
||||||
|
v-on:header-click="onHeaderClick"
|
||||||
|
v-on:view-room-details="viewRoomDetails"
|
||||||
|
v-on:notify="onNotificationDialog"
|
||||||
|
v-else-if="!useFileModeNonAdmin" />
|
||||||
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room"
|
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room"
|
||||||
:events="events" :autoplay="!showRecorder"
|
:events="events" :autoplay="!showRecorder"
|
||||||
:timelineSet="timelineSet"
|
:timelineSet="timelineSet"
|
||||||
|
|
@ -15,15 +24,15 @@
|
||||||
<VoiceRecorder class="audio-layout" v-if="useVoiceMode" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
<VoiceRecorder class="audio-layout" v-if="useVoiceMode" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
||||||
|
|
||||||
<FileDropLayout class="file-drop-root" v-if="useFileModeNonAdmin" :room="room"
|
<FileDropLayout class="file-drop-root" v-if="useFileModeNonAdmin" :room="room"
|
||||||
v-on:pick-file="showAttachmentPicker()"
|
v-on:pick-file="showAttachmentPicker()"
|
||||||
v-on:add-file="addAttachment($event)"
|
v-on:add-file="addAttachment($event)"
|
||||||
v-on:remove-file="currentFileInputs.splice($event, 1)"
|
v-on:remove-file="currentFileInputs.splice($event, 1)"
|
||||||
v-on:reset="resetAttachments"
|
v-on:reset="resetAttachments"
|
||||||
:attachments="currentFileInputs"
|
:attachments="currentFileInputs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="!useVoiceMode && !useFileModeNonAdmin" class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer"
|
<div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
|
||||||
v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
|
v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
|
||||||
<div ref="messageOperationsStrut" class="message-operations-strut">
|
<div ref="messageOperationsStrut" class="message-operations-strut">
|
||||||
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
|
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
|
||||||
|
|
@ -33,8 +42,8 @@
|
||||||
v-on:addreply="addReply(selectedEvent)" v-on:edit="edit(selectedEvent)" v-on:redact="redact(selectedEvent)"
|
v-on:addreply="addReply(selectedEvent)" v-on:edit="edit(selectedEvent)" v-on:redact="redact(selectedEvent)"
|
||||||
v-on:download="download(selectedEvent)" v-on:more="
|
v-on:download="download(selectedEvent)" v-on:more="
|
||||||
isEmojiQuickReaction= true
|
isEmojiQuickReaction= true
|
||||||
showMoreMessageOperations($event)
|
showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor})
|
||||||
" :originalEvent="selectedEvent" />
|
" :originalEvent="selectedEvent" :timelineSet="timelineSet" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="avatarOperationsStrut" class="avatar-operations-strut">
|
<div ref="avatarOperationsStrut" class="avatar-operations-strut">
|
||||||
|
|
@ -49,10 +58,11 @@
|
||||||
<resize-observer ref="chatContainerResizer" @notify="handleChatContainerResize" />
|
<resize-observer ref="chatContainerResizer" @notify="handleChatContainerResize" />
|
||||||
|
|
||||||
<CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" />
|
<CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" />
|
||||||
|
<DirectChatWelcomeHeader v-if="showDirectChatWelcomeHeader" v-on:close="closeDirectChatWelcomeHeader" />
|
||||||
|
|
||||||
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
|
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
|
||||||
<!-- DAY Marker, shown for every new day in the timeline -->
|
<!-- DAY Marker, shown for every new day in the timeline -->
|
||||||
<div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker" :title="dayForEvent(event)" />
|
<div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
|
||||||
|
|
||||||
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
|
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
|
||||||
<div class="message-wrapper" v-on:touchstart="
|
<div class="message-wrapper" v-on:touchstart="
|
||||||
|
|
@ -60,20 +70,28 @@
|
||||||
touchStart(e, event);
|
touchStart(e, event);
|
||||||
}
|
}
|
||||||
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
|
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
|
||||||
|
<!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component.
|
||||||
|
We might therefore get calls to v-on:context-menu that has the event set to that single media item, not the top level thread event
|
||||||
|
that is really displayed in the flow. Therefore, we rewrite these events with "{event: event, anchor: $event.anchor}",
|
||||||
|
see below. Otherwise things like context menus won't work as designed.
|
||||||
|
-->
|
||||||
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]"
|
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]"
|
||||||
:timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
|
:timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
|
||||||
v-on:context-menu="showContextMenuForEvent($event)" v-on:own-avatar-clicked="viewProfile"
|
:componentFn="componentForEvent"
|
||||||
v-on:other-avatar-clicked="showAvatarMenuForEvent($event)" v-on:download="download(event)"
|
v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
|
||||||
|
v-on:own-avatar-clicked="viewProfile"
|
||||||
|
v-on:other-avatar-clicked="showAvatarMenuForEvent({event: event, anchor: $event.anchor})"
|
||||||
|
v-on:download="download(event)"
|
||||||
v-on:poll-closed="pollWasClosed(event)"
|
v-on:poll-closed="pollWasClosed(event)"
|
||||||
v-on:more="
|
v-on:more="
|
||||||
isEmojiQuickReaction = true
|
isEmojiQuickReaction = true
|
||||||
showMoreMessageOperations($event)
|
showMoreMessageOperations({event: event, anchor: $event.anchor})
|
||||||
"
|
"
|
||||||
|
v-on:layout-change="onLayoutChange"
|
||||||
/>
|
/>
|
||||||
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
||||||
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
||||||
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"
|
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div>
|
||||||
:title="$t('message.unread_messages')" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -97,6 +115,7 @@
|
||||||
<div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
|
<div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
|
||||||
{{ replyToEvent.getContent().body | latestReply }}
|
{{ replyToEvent.getContent().body | latestReply }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="replyToContentType === 'm.thread'">{{ replyToThreadMessage }}</div>
|
||||||
<div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div>
|
<div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div>
|
||||||
<div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div>
|
<div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div>
|
||||||
<div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
|
<div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
|
||||||
|
|
@ -121,7 +140,7 @@
|
||||||
{{ typingMembersString }}
|
{{ typingMembersString }}
|
||||||
</div>
|
</div>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row class="input-area-inner align-center" v-if="!showRecorder && !$matrix.currentRoomIsReadOnlyForUser">
|
<v-row class="input-area-inner align-center" v-show="!showRecorder" v-if="!$matrix.currentRoomIsReadOnlyForUser">
|
||||||
<v-col class="flex-grow-1 flex-shrink-1 ma-0 pa-0">
|
<v-col class="flex-grow-1 flex-shrink-1 ma-0 pa-0">
|
||||||
<v-textarea height="undefined" ref="messageInput" full-width auto-grow rows="1" v-model="currentInput"
|
<v-textarea height="undefined" ref="messageInput" full-width auto-grow rows="1" v-model="currentInput"
|
||||||
no-resize class="input-area-text" :placeholder="$t('message.your_message')" hide-details
|
no-resize class="input-area-text" :placeholder="$t('message.your_message')" hide-details
|
||||||
|
|
@ -201,50 +220,72 @@
|
||||||
<div v-if="currentFileInputsDialog && !useFileModeNonAdmin">
|
<div v-if="currentFileInputsDialog && !useFileModeNonAdmin">
|
||||||
<v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable>
|
<v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable>
|
||||||
<v-card class="ma-0 pa-0">
|
<v-card class="ma-0 pa-0">
|
||||||
<v-card-title>{{ $t('message.send_attachements_dialog_title') }}</v-card-title>
|
<v-card-text v-if="!currentFileInputs.length">
|
||||||
<v-divider></v-divider>
|
{{ this.$t("message.preparing_to_upload")}}
|
||||||
<template v-if="imageFiles && imageFiles.length">
|
<v-progress-linear
|
||||||
<v-card-title v-if="imageFiles.length > 1"> {{ $t('message.images') }} </v-card-title>
|
indeterminate
|
||||||
<v-card-text :class="{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': imageFiles.length > 1}">
|
class="mb-0"
|
||||||
<div :class="{'col-4': imageFiles.length > 1}" v-for="(currentImageInput, id) in imageFiles" :key="id">
|
></v-progress-linear>
|
||||||
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
|
</v-card-text>
|
||||||
contain class="current-image-input-path" />
|
<template v-else>
|
||||||
<div>
|
<v-card-title>
|
||||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
<div v-if="currentSendErrorExceededFile" class="red--text">{{ currentSendErrorExceededFile }}</div>
|
||||||
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span>
|
<span v-else> {{ $t('message.send_attachements_dialog_title') }} </span>
|
||||||
<span v-else-if="currentImageInput && currentImageInput.dimensions">
|
</v-card-title>
|
||||||
{{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}</span>
|
<v-divider></v-divider>
|
||||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
<template v-if="imageFiles && imageFiles.length">
|
||||||
({{ formatBytes(currentImageInput.scaledSize) }})</span>
|
<v-card-title v-if="imageFiles.length > 1"> {{ $t('message.images') }} </v-card-title>
|
||||||
<v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
|
<v-card-text :class="{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': imageFiles.length > 1}">
|
||||||
v-model="currentImageInput.useScaled" />
|
<div :class="{'col-4': imageFiles.length > 1}" v-for="(currentImageInput, id) in imageFiles" :key="id">
|
||||||
|
<div style="position: relative">
|
||||||
|
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
|
||||||
|
contain class="current-image-input-path" />
|
||||||
|
<v-progress-linear :style="{ position: 'absolute', left: '0', right: '0', bottom: '0', opacity: currentImageInput.sendInfo ? '1' : '0' }" :value="currentImageInput.sendInfo ? currentImageInput.sendInfo.progress : 0"></v-progress-linear>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
||||||
|
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span>
|
||||||
|
<span v-else-if="currentImageInput && currentImageInput.dimensions">
|
||||||
|
{{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
||||||
|
({{ formatBytes(currentImageInput.scaledSize) }})
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
({{ formatBytes(currentImageInput.actualSize) }})
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
|
||||||
|
v-model="currentImageInput.useScaled" :disabled="currentImageInput.sendInfo" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</v-card-text>
|
||||||
</v-card-text>
|
</template>
|
||||||
</template>
|
<template v-if="Array.isArray(currentFileInputs) && currentFileInputs.length">
|
||||||
<template v-if="Array.isArray(currentFileInputs) && currentFileInputs.length">
|
<v-card-title v-if="nonImageFiles.length > 1">{{ $t('message.files') }}</v-card-title>
|
||||||
<v-card-title v-if="nonImageFiles.length > 1">{{ $t('message.files') }}</v-card-title>
|
<v-card-text>
|
||||||
<v-card-text>
|
<div v-for="(currentImageInputPath, id) in currentFileInputs" :key="id">
|
||||||
<div v-for="(currentImageInputPath, id) in currentFileInputs" :key="id">
|
<div v-if="!currentImageInputPath.type.includes('image/')">
|
||||||
<div v-if="!currentImageInputPath.type.includes('image/')">
|
<span> {{ $t('message.file') }}: {{ currentImageInputPath.name }}</span>
|
||||||
<span> {{ $t('message.file') }}: {{ currentImageInputPath.name }}</span>
|
<span> ({{ formatBytes(currentImageInputPath.size) }})</span>
|
||||||
<span> ({{ formatBytes(currentImageInputPath.size) }})</span>
|
<v-progress-linear :style="{ opacity: currentImageInputPath.sendInfo ? '1' : '0' }" :value="currentImageInputPath.sendInfo ? currentImageInputPath.sendInfo.progress : 0"></v-progress-linear>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</v-card-text>
|
||||||
</v-card-text>
|
</template>
|
||||||
</template>
|
<v-divider></v-divider>
|
||||||
<v-divider></v-divider>
|
<v-card-actions>
|
||||||
<v-card-actions>
|
<v-spacer>
|
||||||
<v-spacer>
|
<div v-if="currentSendError">{{ currentSendError }}</div>
|
||||||
<div v-if="currentSendError">{{ currentSendError }}</div>
|
</v-spacer>
|
||||||
<div v-else>{{ currentSendProgress }}</div>
|
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel" :disabled="sendingStatus != sendStatuses.SENDING && sendingStatus != sendStatuses.INITIAL">
|
||||||
</v-spacer>
|
{{ $t("menu.cancel") }}
|
||||||
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel">
|
</v-btn>
|
||||||
{{ $t("menu.cancel") }}
|
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment(undefined)"
|
||||||
</v-btn>
|
v-if="currentSendShowSendButton" :disabled="sendingStatus != sendStatuses.INITIAL">{{ $t("menu.send") }}</v-btn>
|
||||||
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment"
|
</v-card-actions>
|
||||||
v-if="currentSendShowSendButton" :disabled="currentSendOperation != null">{{ $t("menu.send") }}</v-btn>
|
</template>
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -282,6 +323,46 @@
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<CreatePollDialog :show="showCreatePollDialog" @close="showCreatePollDialog = false" />
|
<CreatePollDialog :show="showCreatePollDialog" @close="showCreatePollDialog = false" />
|
||||||
|
|
||||||
|
<!-- Dialog for request Notification and register service worker-->
|
||||||
|
<v-dialog
|
||||||
|
v-model="notificationDialog"
|
||||||
|
persistent
|
||||||
|
class="ma-0 pa-0"
|
||||||
|
:width="$vuetify.breakpoint.smAndUp ? '688px' : '95%'"
|
||||||
|
>
|
||||||
|
<div class="dialog-content text-center">
|
||||||
|
<v-icon size="30">notifications_active</v-icon>
|
||||||
|
<h2 class="dialog-title">
|
||||||
|
{{ $t("notification.dialog.title") }}
|
||||||
|
</h2>
|
||||||
|
<div class="dialog-text">{{ $t("notification.dialog.body") }}</div>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row cols="12">
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-btn
|
||||||
|
depressed
|
||||||
|
text
|
||||||
|
block
|
||||||
|
class="text-button"
|
||||||
|
@click="notificationDialog = false"
|
||||||
|
>{{ $t("global.close") }}</v-btn
|
||||||
|
>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" align="center">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
depressed
|
||||||
|
block
|
||||||
|
class="filled-button"
|
||||||
|
@click.stop="onNotifyRequest"
|
||||||
|
>{{ $t("notification.dialog.enable") }}</v-btn
|
||||||
|
>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -292,9 +373,11 @@ import util, { ROOM_TYPE_VOICE_MODE, ROOM_TYPE_FILE_MODE } from "../plugins/util
|
||||||
import MessageOperations from "./messages/MessageOperations.vue";
|
import MessageOperations from "./messages/MessageOperations.vue";
|
||||||
import AvatarOperations from "./messages/AvatarOperations.vue";
|
import AvatarOperations from "./messages/AvatarOperations.vue";
|
||||||
import ChatHeader from "./ChatHeader";
|
import ChatHeader from "./ChatHeader";
|
||||||
|
import ChatHeaderPrivate from "./ChatHeaderPrivate.vue";
|
||||||
import VoiceRecorder from "./VoiceRecorder";
|
import VoiceRecorder from "./VoiceRecorder";
|
||||||
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
|
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
|
||||||
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
|
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
|
||||||
|
import DirectChatWelcomeHeader from "./DirectChatWelcomeHeader";
|
||||||
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
|
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
|
||||||
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
|
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
|
||||||
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
|
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
|
||||||
|
|
@ -302,8 +385,12 @@ import BottomSheet from "./BottomSheet.vue";
|
||||||
import ImageResize from "image-resize";
|
import ImageResize from "image-resize";
|
||||||
import CreatePollDialog from "./CreatePollDialog.vue";
|
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||||
import chatMixin from "./chatMixin";
|
import chatMixin from "./chatMixin";
|
||||||
|
import sendAttachmentsMixin from "./sendAttachmentsMixin";
|
||||||
import AudioLayout from "./AudioLayout.vue";
|
import AudioLayout from "./AudioLayout.vue";
|
||||||
import FileDropLayout from "./file_mode/FileDropLayout";
|
import FileDropLayout from "./file_mode/FileDropLayout";
|
||||||
|
import { requestNotificationPermission, windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js"
|
||||||
|
import roomTypeMixin from "./roomTypeMixin";
|
||||||
|
import roomMembersMixin from "./roomMembersMixin";
|
||||||
|
|
||||||
const sizeOf = require("image-size");
|
const sizeOf = require("image-size");
|
||||||
const dataUriToBuffer = require("data-uri-to-buffer");
|
const dataUriToBuffer = require("data-uri-to-buffer");
|
||||||
|
|
@ -339,13 +426,15 @@ ScrollPosition.prototype.prepareFor = function (direction) {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Chat",
|
name: "Chat",
|
||||||
mixins: [chatMixin],
|
mixins: [chatMixin, roomTypeMixin, sendAttachmentsMixin, roomMembersMixin],
|
||||||
components: {
|
components: {
|
||||||
ChatHeader,
|
ChatHeader,
|
||||||
|
ChatHeaderPrivate,
|
||||||
MessageOperations,
|
MessageOperations,
|
||||||
VoiceRecorder,
|
VoiceRecorder,
|
||||||
RoomInfoBottomSheet,
|
RoomInfoBottomSheet,
|
||||||
CreatedRoomWelcomeHeader,
|
CreatedRoomWelcomeHeader,
|
||||||
|
DirectChatWelcomeHeader,
|
||||||
NoHistoryRoomWelcomeHeader,
|
NoHistoryRoomWelcomeHeader,
|
||||||
MessageOperationsBottomSheet,
|
MessageOperationsBottomSheet,
|
||||||
StickerPickerBottomSheet,
|
StickerPickerBottomSheet,
|
||||||
|
|
@ -370,10 +459,9 @@ export default {
|
||||||
|
|
||||||
scrollPosition: null,
|
scrollPosition: null,
|
||||||
currentFileInputs: null,
|
currentFileInputs: null,
|
||||||
currentSendOperation: null,
|
|
||||||
currentSendProgress: null,
|
|
||||||
currentSendShowSendButton: true,
|
currentSendShowSendButton: true,
|
||||||
currentSendError: null,
|
currentSendError: null,
|
||||||
|
currentSendErrorExceededFile: null,
|
||||||
showEmojiPicker: false,
|
showEmojiPicker: false,
|
||||||
selectedEvent: null,
|
selectedEvent: null,
|
||||||
editedEvent: null,
|
editedEvent: null,
|
||||||
|
|
@ -411,7 +499,10 @@ export default {
|
||||||
lastRR: null,
|
lastRR: null,
|
||||||
|
|
||||||
/** If we just created this room, show a small welcome header with info */
|
/** If we just created this room, show a small welcome header with info */
|
||||||
showCreatedRoomWelcomeHeader: false,
|
hideCreatedRoomWelcomeHeader: false,
|
||||||
|
|
||||||
|
/** For direct chats, show a small welcome header with info about the other party */
|
||||||
|
hideDirectChatWelcomeHeader: false,
|
||||||
|
|
||||||
/** An array of recent emojis. Used in the "message operations" popup. */
|
/** An array of recent emojis. Used in the "message operations" popup. */
|
||||||
recentEmojis: [],
|
recentEmojis: [],
|
||||||
|
|
@ -433,7 +524,8 @@ export default {
|
||||||
Symbols: this.$t("emoji.categories.symbols"),
|
Symbols: this.$t("emoji.categories.symbols"),
|
||||||
Places: this.$t("emoji.categories.places")
|
Places: this.$t("emoji.categories.places")
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
notificationDialog: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -443,7 +535,7 @@ export default {
|
||||||
if (contentArr[0] === "") {
|
if (contentArr[0] === "") {
|
||||||
contentArr.shift();
|
contentArr.shift();
|
||||||
}
|
}
|
||||||
return contentArr[0].replace(/^> (<.*> )?/g, "");
|
return (contentArr && contentArr.length > 0) ? contentArr[0].replace(/^> (<.*> )?/g, "") : "";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -471,10 +563,10 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
nonImageFiles() {
|
nonImageFiles() {
|
||||||
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file.type.includes("image/"))
|
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file?.type.includes("image/"))
|
||||||
},
|
},
|
||||||
imageFiles() {
|
imageFiles() {
|
||||||
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => file.type.includes("image/"))
|
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => file?.type.includes("image/"))
|
||||||
},
|
},
|
||||||
isCurrentFileInputsAnArray() {
|
isCurrentFileInputsAnArray() {
|
||||||
return Array.isArray(this.currentFileInputs)
|
return Array.isArray(this.currentFileInputs)
|
||||||
|
|
@ -595,13 +687,13 @@ export default {
|
||||||
useVoiceMode: {
|
useVoiceMode: {
|
||||||
get: function () {
|
get: function () {
|
||||||
if (!this.$config.experimental_voice_mode) return false;
|
if (!this.$config.experimental_voice_mode) return false;
|
||||||
return util.roomDisplayType(this.room) === ROOM_TYPE_VOICE_MODE;
|
return (util.roomDisplayTypeOverride(this.room) || this.roomDisplayType) === ROOM_TYPE_VOICE_MODE;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
useFileModeNonAdmin: {
|
useFileModeNonAdmin: {
|
||||||
get: function() {
|
get: function() {
|
||||||
if (!this.$config.experimental_file_mode) return false;
|
if (!this.$config.experimental_file_mode) return false;
|
||||||
return util.roomDisplayType(this.room) === ROOM_TYPE_FILE_MODE && !this.canCreatePoll; // TODO - Check user or admin
|
return (util.roomDisplayTypeOverride(this.room) || this.roomDisplayType) === ROOM_TYPE_FILE_MODE && !this.canCreatePoll; // TODO - Check user or admin
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -632,6 +724,88 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.events;
|
return this.events;
|
||||||
|
},
|
||||||
|
|
||||||
|
roomCreatedByUsRecently() {
|
||||||
|
const createEvent = this.room && this.room.currentState.getStateEvents("m.room.create", "");
|
||||||
|
if (createEvent) {
|
||||||
|
const creatorId = createEvent.getContent().creator;
|
||||||
|
return (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
isDirectRoom() {
|
||||||
|
return this.room && this.room.getJoinRule() == "invite" && this.joinedAndInvitedMembers.length == 2;
|
||||||
|
},
|
||||||
|
|
||||||
|
isPublicRoom() {
|
||||||
|
return this.room && this.room.getJoinRule() == "public";
|
||||||
|
},
|
||||||
|
|
||||||
|
showCreatedRoomWelcomeHeader() {
|
||||||
|
return !this.hideCreatedRoomWelcomeHeader && this.roomCreatedByUsRecently && !this.isDirectRoom;
|
||||||
|
},
|
||||||
|
|
||||||
|
showDirectChatWelcomeHeader() {
|
||||||
|
return !this.hideDirectChatWelcomeHeader && this.roomCreatedByUsRecently && this.isDirectRoom;
|
||||||
|
},
|
||||||
|
|
||||||
|
chatContainerStyle() {
|
||||||
|
if (this.$config.chat_backgrounds && this.room && this.roomId) {
|
||||||
|
const roomType = this.isDirectRoom ? "direct" : this.isPublicRoom ? "public" : "invite";
|
||||||
|
let backgrounds = this.$config.chat_backgrounds[roomType] || this.$config.chat_backgrounds["all"];
|
||||||
|
if (backgrounds) {
|
||||||
|
const numBackgrounds = backgrounds.length;
|
||||||
|
|
||||||
|
// If we have several backgrounds set, use the room ID to calculate
|
||||||
|
// an int hash value, then take mod of that to select a background to use.
|
||||||
|
// That way, we always get the same one, since room IDs don't change.
|
||||||
|
|
||||||
|
// From: https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
|
||||||
|
const hashCode = function (s) {
|
||||||
|
var hash = 0,
|
||||||
|
i, chr;
|
||||||
|
if (s.length === 0) return hash;
|
||||||
|
for (i = 0; i < s.length; i++) {
|
||||||
|
chr = s.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + chr;
|
||||||
|
hash |= 0; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from: https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
|
||||||
|
const validUrl = function (s) {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(s, window.location);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "data:";
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = Math.abs(hashCode(this.roomId)) % numBackgrounds;
|
||||||
|
const background = backgrounds[index];
|
||||||
|
if (background && validUrl(background)) {
|
||||||
|
return "background-image: url(" + background + ");background-repeat: repeat";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we are replying to a (media) thread, this is the hint we show when replying.
|
||||||
|
*/
|
||||||
|
replyToThreadMessage() {
|
||||||
|
if (this.replyToEvent && this.timelineSet) {
|
||||||
|
return this.$t("message.sent_media", {count: this.timelineSet.relations
|
||||||
|
.getAllChildEventsForEvent(this.replyToEvent.getId())
|
||||||
|
.filter((e) => util.downloadableTypes().includes(e.getContent().msgtype)).length});
|
||||||
|
}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -640,6 +814,8 @@ export default {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(value, oldValue) {
|
handler(value, oldValue) {
|
||||||
if (value && !oldValue) {
|
if (value && !oldValue) {
|
||||||
|
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
|
||||||
|
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
|
||||||
console.log("Loading finished!");
|
console.log("Loading finished!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -661,7 +837,8 @@ export default {
|
||||||
this.timelineWindow = null;
|
this.timelineWindow = null;
|
||||||
this.typingMembers = [];
|
this.typingMembers = [];
|
||||||
this.initialLoadDone = false;
|
this.initialLoadDone = false;
|
||||||
this.showCreatedRoomWelcomeHeader = false;
|
this.hideDirectChatWelcomeHeader = false;
|
||||||
|
this.hideCreatedRoomWelcomeHeader = false;
|
||||||
|
|
||||||
// Stop RR timer
|
// Stop RR timer
|
||||||
this.stopRRTimer();
|
this.stopRRTimer();
|
||||||
|
|
@ -681,7 +858,7 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.initialLoadDone = true;
|
this.setInitialLoadDone();
|
||||||
return; // no room
|
return; // no room
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -708,8 +885,10 @@ export default {
|
||||||
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
|
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
|
||||||
top = rectAnchor.top - rectChat.top - 50;
|
top = rectAnchor.top - rectChat.top - 50;
|
||||||
left = rectAnchor.left - rectChat.left - 75;
|
left = rectAnchor.left - rectChat.left - 75;
|
||||||
if (left + rectOps.width >= rectChat.right) {
|
if (left + rectOps.width + 10 >= rectChat.right) {
|
||||||
left = rectChat.right - rectOps.width - 10; // No overflow
|
left = rectChat.right - rectOps.width - 10; // No overflow
|
||||||
|
} else if (left < 0) {
|
||||||
|
left = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -726,17 +905,28 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onRoomJoined(initialEventId) {
|
/**
|
||||||
// Was this room just created (by you)? Show a small info header in
|
* Set initialLoadDone to 'true'. First process all events, setting threadParent and replyEvent if needed.
|
||||||
// that case!
|
*/
|
||||||
const createEvent = this.room.currentState.getStateEvents("m.room.create", "");
|
setInitialLoadDone() {
|
||||||
if (createEvent) {
|
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
|
||||||
const creatorId = createEvent.getContent().creator;
|
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
|
||||||
if (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */) {
|
this.initialLoadDone = true;
|
||||||
this.showCreatedRoomWelcomeHeader = true;
|
console.log("Loading finished!");
|
||||||
}
|
},
|
||||||
|
windowNotificationPermission,
|
||||||
|
onNotificationDialog() {
|
||||||
|
if(this.windowNotificationPermission() === 'denied') {
|
||||||
|
alert(this.$t("notification.blocked_message"));
|
||||||
|
} else if(this.windowNotificationPermission() === 'default') {
|
||||||
|
this.notificationDialog = true;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onNotifyRequest() {
|
||||||
|
requestNotificationPermission()
|
||||||
|
this.notificationDialog = false;
|
||||||
|
},
|
||||||
|
onRoomJoined(initialEventId) {
|
||||||
// Listen to events
|
// Listen to events
|
||||||
this.$matrix.on("Room.timeline", this.onEvent);
|
this.$matrix.on("Room.timeline", this.onEvent);
|
||||||
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
||||||
|
|
@ -778,10 +968,24 @@ export default {
|
||||||
console.log("ERROR " + err);
|
console.log("ERROR " + err);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
self.initialLoadDone = true;
|
// const [timelineEvents, threadedEvents, unknownRelations] =
|
||||||
if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
|
// this.room.partitionThreadedEvents(self.events);
|
||||||
self.scrollToEvent(initialEventId);
|
// this.$matrix.matrixClient.processAggregatedTimelineEvents(this.room, timelineEvents);
|
||||||
} else if (this.showCreatedRoomWelcomeHeader) {
|
// //room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||||
|
// this.$matrix.matrixClient.processThreadEvents(this.room, threadedEvents, true);
|
||||||
|
// unknownRelations.forEach((event) => this.room.relations.aggregateChildEvent(event));
|
||||||
|
|
||||||
|
this.setInitialLoadDone();
|
||||||
|
if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
|
||||||
|
const event = this.room.findEventById(initialEventId);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (event && event.parentThread) {
|
||||||
|
self.scrollToEvent(event.parentThread.getId());
|
||||||
|
} else {
|
||||||
|
self.scrollToEvent(initialEventId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (this.showCreatedRoomWelcomeHeader || this.showDirectChatWelcomeHeader) {
|
||||||
self.onScroll();
|
self.onScroll();
|
||||||
}
|
}
|
||||||
self.restartRRTimer();
|
self.restartRRTimer();
|
||||||
|
|
@ -795,7 +999,7 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
// Error. Done loading.
|
// Error. Done loading.
|
||||||
this.events = this.timelineWindow.getEvents();
|
this.events = this.timelineWindow.getEvents();
|
||||||
this.initialLoadDone = true;
|
this.setInitialLoadDone();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -929,12 +1133,83 @@ export default {
|
||||||
|
|
||||||
this.restartRRTimer();
|
this.restartRRTimer();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setParentThread(event) {
|
||||||
|
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
|
||||||
|
if (parentEvent) {
|
||||||
|
Vue.set(parentEvent, "isMxThread", true);
|
||||||
|
Vue.set(event, "parentThread", parentEvent);
|
||||||
|
} else {
|
||||||
|
// Try to load from server.
|
||||||
|
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId).then((tl) => {
|
||||||
|
if (tl) {
|
||||||
|
const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId);
|
||||||
|
if (parentEvent) {
|
||||||
|
this.events = this.timelineWindow.getEvents();
|
||||||
|
const fn = () => {
|
||||||
|
Vue.set(parentEvent, "isMxThread", true);
|
||||||
|
Vue.set(event, "parentThread", parentEvent);
|
||||||
|
};
|
||||||
|
if (this.initialLoadDone) {
|
||||||
|
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
|
||||||
|
const element = document.querySelector(sel);
|
||||||
|
if (element) {
|
||||||
|
this.onLayoutChange(fn, element);
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setReplyToEvent(event) {
|
||||||
|
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
|
||||||
|
if (parentEvent) {
|
||||||
|
Vue.set(event, "replyEvent", parentEvent);
|
||||||
|
} else {
|
||||||
|
// Try to load from server.
|
||||||
|
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.replyEventId)
|
||||||
|
.then((tl) => {
|
||||||
|
if (tl) {
|
||||||
|
const parentEvent = tl.getEvents().find((e) => e.getId() === event.replyEventId);
|
||||||
|
if (parentEvent) {
|
||||||
|
this.events = this.timelineWindow.getEvents();
|
||||||
|
const fn = () => {Vue.set(event, "replyEvent", parentEvent);};
|
||||||
|
if (this.initialLoadDone) {
|
||||||
|
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
|
||||||
|
const element = document.querySelector(sel);
|
||||||
|
if (element) {
|
||||||
|
this.onLayoutChange(fn, element);
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(e => console.error(e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onEvent(event) {
|
onEvent(event) {
|
||||||
//console.log("OnEvent", JSON.stringify(event));
|
//console.log("OnEvent", JSON.stringify(event));
|
||||||
if (event.getRoomId() !== this.roomId) {
|
if (event.getRoomId() !== this.roomId) {
|
||||||
return; // Not for this room
|
return; // Not for this room
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.initialLoadDone && event.threadRootId && !event.parentThread) {
|
||||||
|
this.setParentThread(event);
|
||||||
|
}
|
||||||
|
if (this.initialLoadDone && event.replyEventId && !event.replyEvent) {
|
||||||
|
this.setReplyToEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
const loadingDone = this.initialLoadDone;
|
const loadingDone = this.initialLoadDone;
|
||||||
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
|
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
|
||||||
|
|
||||||
|
|
@ -942,7 +1217,7 @@ export default {
|
||||||
this.paginateBackIfNeeded();
|
this.paginateBackIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingDone && event.forwardLooking && !event.isRelation()) {
|
if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread )) {
|
||||||
// If we are at bottom, scroll to see new events...
|
// If we are at bottom, scroll to see new events...
|
||||||
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
|
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
|
||||||
const container = this.chatContainer;
|
const container = this.chatContainer;
|
||||||
|
|
@ -954,7 +1229,7 @@ export default {
|
||||||
this.handleScrolledToBottom(scrollToSeeNew);
|
this.handleScrolledToBottom(scrollToSeeNew);
|
||||||
|
|
||||||
// If kick or ban event, redirect to "goodbye"...
|
// If kick or ban event, redirect to "goodbye"...
|
||||||
if (event.getType() === "m.room.member" &&
|
if (event.getType() === "m.room.member" &&
|
||||||
event.getStateKey() == this.$matrix.currentUserId &&
|
event.getStateKey() == this.$matrix.currentUserId &&
|
||||||
(event.getPrevContent() || {}).membership == "join" &&
|
(event.getPrevContent() || {}).membership == "join" &&
|
||||||
(
|
(
|
||||||
|
|
@ -1015,15 +1290,19 @@ export default {
|
||||||
this.$refs.attachment.click();
|
this.$refs.attachment.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
optimizeImage(e,event,file) {
|
optimizeImage(evt,file) {
|
||||||
file.image = e.target.result;
|
let fileObj = {}
|
||||||
file.dimensions = null;
|
fileObj.image = evt.target.result;
|
||||||
|
fileObj.dimensions = null;
|
||||||
|
fileObj.type = file.type;
|
||||||
|
fileObj.actualSize = file.size;
|
||||||
|
fileObj.actualFile = file
|
||||||
try {
|
try {
|
||||||
file.dimensions = sizeOf(dataUriToBuffer(e.target.result));
|
fileObj.dimensions = sizeOf(dataUriToBuffer(evt.target.result));
|
||||||
|
|
||||||
// Need to resize?
|
// Need to resize?
|
||||||
const w = file.dimensions.width;
|
const w = fileObj.dimensions.width;
|
||||||
const h = file.dimensions.height;
|
const h = fileObj.dimensions.height;
|
||||||
if (w > 640 || h > 640) {
|
if (w > 640 || h > 640) {
|
||||||
var aspect = w / h;
|
var aspect = w / h;
|
||||||
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
|
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
|
||||||
|
|
@ -1035,19 +1314,19 @@ export default {
|
||||||
outputType: "blob",
|
outputType: "blob",
|
||||||
});
|
});
|
||||||
imageResize
|
imageResize
|
||||||
.play(event.target)
|
.play(evt.target.result)
|
||||||
.then((img) => {
|
.then((img) => {
|
||||||
Vue.set(
|
Vue.set(
|
||||||
file,
|
fileObj,
|
||||||
"scaled",
|
"scaled",
|
||||||
new File([img], file.name, {
|
new File([img], file.name, {
|
||||||
type: img.type,
|
type: img.type,
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
Vue.set(file, "useScaled", true);
|
Vue.set(fileObj, "useScaled", true);
|
||||||
Vue.set(file, "scaledSize", img.size);
|
Vue.set(fileObj, "scaledSize", img.size);
|
||||||
Vue.set(file, "scaledDimensions", {
|
Vue.set(fileObj, "scaledDimensions", {
|
||||||
width: newWidth,
|
width: newWidth,
|
||||||
height: newHeight,
|
height: newHeight,
|
||||||
});
|
});
|
||||||
|
|
@ -1059,24 +1338,19 @@ export default {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get image dimensions: " + error);
|
console.error("Failed to get image dimensions: " + error);
|
||||||
}
|
}
|
||||||
return file
|
return fileObj
|
||||||
},
|
},
|
||||||
handleFileReader(event, file) {
|
handleFileReader(file) {
|
||||||
if (file) {
|
if (file) {
|
||||||
|
let optimizedFileObj;
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (evt) => {
|
||||||
if (file.type.startsWith("image/")) {
|
if (file.type.startsWith("image/")) {
|
||||||
this.optimizeImage(e, event, file)
|
optimizedFileObj = this.optimizeImage(evt, file)
|
||||||
|
} else {
|
||||||
|
optimizedFileObj = file
|
||||||
}
|
}
|
||||||
this.$matrix.matrixClient.getMediaConfig().then((config) => {
|
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, optimizedFileObj] : [optimizedFileObj];
|
||||||
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, file] : [file];
|
|
||||||
if (config["m.upload.size"] && file.size > config["m.upload.size"]) {
|
|
||||||
this.currentSendError = this.$t("message.upload_file_too_large");
|
|
||||||
this.currentSendShowSendButton = false;
|
|
||||||
} else {
|
|
||||||
this.currentSendShowSendButton = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
@ -1085,66 +1359,62 @@ export default {
|
||||||
* Handle picked attachment
|
* Handle picked attachment
|
||||||
*/
|
*/
|
||||||
handlePickedAttachment(event) {
|
handlePickedAttachment(event) {
|
||||||
Object.values(event.target.files).forEach(file => this.handleFileReader(event, file));
|
this.currentFileInputs = []
|
||||||
|
const uploadedFiles = Object.values(event.target.files);
|
||||||
|
|
||||||
|
this.$matrix.matrixClient.getMediaConfig().then((config) => {
|
||||||
|
const configUploadSize = config["m.upload.size"];
|
||||||
|
const configFormattedUploadSize = this.formatBytes(configUploadSize);
|
||||||
|
|
||||||
|
uploadedFiles.every(file => {
|
||||||
|
if (configUploadSize && file.size > configUploadSize) {
|
||||||
|
this.currentSendError = this.$t("message.upload_file_too_large");
|
||||||
|
this.currentSendErrorExceededFile = this.$t("message.upload_exceeded_file_limit", { configFormattedUploadSize });
|
||||||
|
this.currentSendShowSendButton = false;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.currentSendShowSendButton = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadedFiles.forEach(file => this.handleFileReader(file));
|
||||||
|
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
showStickerPicker() {
|
showStickerPicker() {
|
||||||
this.$refs.stickerPickerSheet.open();
|
this.$refs.stickerPickerSheet.open();
|
||||||
},
|
},
|
||||||
|
|
||||||
onUploadProgress(p) {
|
|
||||||
if (p.total) {
|
|
||||||
this.currentSendProgress = this.$t("message.upload_progress_with_total", {
|
|
||||||
count: p.loaded || 0,
|
|
||||||
total: p.total,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.currentSendProgress = this.$t("message.upload_progress", {
|
|
||||||
count: p.loaded || 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendAttachment(withText) {
|
sendAttachment(withText) {
|
||||||
this.$refs.attachment.value = null;
|
this.$refs.attachment.value = null;
|
||||||
if (this.isCurrentFileInputsAnArray) {
|
if (this.isCurrentFileInputsAnArray) {
|
||||||
let inputFiles = this.currentFileInputs.map(entry => {
|
const text = withText || "";
|
||||||
if (entry.scaled && entry.useScaled) {
|
const promise = this.sendAttachments(text, this.currentFileInputs);
|
||||||
// Send scaled version of image instead!
|
promise.then(() => {
|
||||||
return entry.scaled;
|
this.currentFileInputs = null;
|
||||||
}
|
this.sendingStatus = this.sendStatuses.INITIAL;
|
||||||
return entry;
|
|
||||||
})
|
})
|
||||||
const promises = inputFiles.map(inputFile => util.sendImage(this.$matrix.matrixClient, this.roomId, inputFile, this.onUploadProgress));
|
.catch((err) => {
|
||||||
|
if (err.name === "AbortError" || err === "Abort") {
|
||||||
Promise.all(promises).then(() => {
|
this.currentSendError = null;
|
||||||
this.currentSendOperation = null;
|
this.currentSendErrorExceededFile = null;
|
||||||
this.currentFileInputs = null;
|
} else {
|
||||||
this.currentSendProgress = null;
|
this.currentSendError = err.LocaleString();
|
||||||
if (withText) {
|
this.currentSendErrorExceededFile = err.LocaleString();
|
||||||
this.sendMessage(withText);
|
}
|
||||||
}
|
});
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err.name === "AbortError" || err === "Abort") {
|
|
||||||
this.currentSendError = null;
|
|
||||||
} else {
|
|
||||||
this.currentSendError = err.LocaleString();
|
|
||||||
}
|
|
||||||
this.currentSendOperation = null;
|
|
||||||
this.currentSendProgress = null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelSendAttachment() {
|
cancelSendAttachment() {
|
||||||
this.$refs.attachment.value = null;
|
this.$refs.attachment.value = null;
|
||||||
if (this.currentSendOperation) {
|
this.cancelSendAttachments();
|
||||||
this.currentSendOperation.abort();
|
|
||||||
}
|
|
||||||
this.currentSendOperation = null;
|
|
||||||
this.currentFileInputs = null;
|
this.currentFileInputs = null;
|
||||||
this.currentSendProgress = null;
|
|
||||||
this.currentSendError = null;
|
this.currentSendError = null;
|
||||||
|
this.currentSendErrorExceededFile = null;
|
||||||
|
this.sendingStatus = this.sendStatuses.INITIAL;
|
||||||
},
|
},
|
||||||
|
|
||||||
addAttachment(file) {
|
addAttachment(file) {
|
||||||
|
|
@ -1155,6 +1425,28 @@ export default {
|
||||||
this.cancelSendAttachment();
|
this.cancelSendAttachment();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by message components that need to change their layout. This will avoid "jumping" in the UI, because
|
||||||
|
* we remember scroll position, apply the layout change, then restore the scroll.
|
||||||
|
* NOTE: we use "parentElement" below, because it is expected to be called with "element" set to the message component
|
||||||
|
* and the message component in turn being wrapped by a "message-wrapper" element (see html above).
|
||||||
|
* @param {} action A function that performs desired layout changes.
|
||||||
|
* @param {*} element Root element for the chat message.
|
||||||
|
*/
|
||||||
|
onLayoutChange(action, element) {
|
||||||
|
if (!element || !element.parentElemen || this.useVoiceMode || this.useFileModeNonAdmin) {
|
||||||
|
action();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const container = this.chatContainer;
|
||||||
|
this.scrollPosition.prepareFor(element.parentElement.offsetTop >= container.scrollTop ? "down" : "up");
|
||||||
|
action();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// restore scroll position!
|
||||||
|
this.scrollPosition.restore();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
handleScrolledToTop() {
|
handleScrolledToTop() {
|
||||||
if (
|
if (
|
||||||
this.timelineWindow &&
|
this.timelineWindow &&
|
||||||
|
|
@ -1165,7 +1457,7 @@ export default {
|
||||||
this.timelineWindow
|
this.timelineWindow
|
||||||
.paginate(EventTimeline.BACKWARDS, 10, true)
|
.paginate(EventTimeline.BACKWARDS, 10, true)
|
||||||
.then((success) => {
|
.then((success) => {
|
||||||
if (success) {
|
if (success && this.scrollPosition) {
|
||||||
this.scrollPosition.prepareFor("up");
|
this.scrollPosition.prepareFor("up");
|
||||||
this.events = this.timelineWindow.getEvents();
|
this.events = this.timelineWindow.getEvents();
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
@ -1193,7 +1485,7 @@ export default {
|
||||||
.then((success) => {
|
.then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
this.events = this.timelineWindow.getEvents();
|
this.events = this.timelineWindow.getEvents();
|
||||||
if (!this.useVoiceMode) {
|
if (!this.useVoiceMode && this.scrollPosition) {
|
||||||
this.scrollPosition.prepareFor("down");
|
this.scrollPosition.prepareFor("down");
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
// restore scroll position!
|
// restore scroll position!
|
||||||
|
|
@ -1219,9 +1511,18 @@ export default {
|
||||||
const container = this.chatContainer;
|
const container = this.chatContainer;
|
||||||
const ref = this.$refs[eventId];
|
const ref = this.$refs[eventId];
|
||||||
if (container && ref) {
|
if (container && ref) {
|
||||||
const targetY = container.clientHeight / 2;
|
const parent = container.getBoundingClientRect();
|
||||||
const sourceY = ref[0].offsetTop;
|
const item = ref[0].getBoundingClientRect();
|
||||||
container.scrollTo(0, sourceY - targetY);
|
let offsetY = (parent.bottom - parent.top) / 2;
|
||||||
|
if (ref[0].clientHeight > offsetY) {
|
||||||
|
offsetY = Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
|
||||||
|
}
|
||||||
|
const targetY = parent.top + offsetY;
|
||||||
|
const currentY = item.top;
|
||||||
|
const y = container.scrollTop + (currentY - targetY);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
container.scrollTo(0, y);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1273,7 +1574,11 @@ export default {
|
||||||
addReply(event) {
|
addReply(event) {
|
||||||
this.replyToEvent = event;
|
this.replyToEvent = event;
|
||||||
this.$refs.messageInput.focus();
|
this.$refs.messageInput.focus();
|
||||||
this.replyToContentType = event.getContent().msgtype || 'm.poll';
|
if (event.parentThread || event.isThreadRoot || event.isMxThread) {
|
||||||
|
this.replyToContentType = 'm.thread';
|
||||||
|
} else {
|
||||||
|
this.replyToContentType = event.getContent().msgtype || 'm.poll';
|
||||||
|
}
|
||||||
this.setReplyToImage(event);
|
this.setReplyToImage(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1295,24 +1600,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
download(event) {
|
download(event) {
|
||||||
util
|
if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
|
||||||
.getAttachment(this.$matrix.matrixClient, event)
|
const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
.then((url) => {
|
children.forEach(child => util.download(this.$matrix.matrixClient, child));
|
||||||
const link = document.createElement("a");
|
} else {
|
||||||
link.href = url;
|
util.download(this.$matrix.matrixClient, event);
|
||||||
link.target = "_blank";
|
}
|
||||||
link.download = event.getContent().body || this.$t("fallbacks.download_name");
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}, 200);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Failed to fetch attachment: ", err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelEditReply() {
|
cancelEditReply() {
|
||||||
|
|
@ -1555,7 +1848,17 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
closeCreateRoomWelcomeHeader() {
|
closeCreateRoomWelcomeHeader() {
|
||||||
this.showCreatedRoomWelcomeHeader = false;
|
this.hideCreatedRoomWelcomeHeader = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// We change the layout when removing the welcome header, so call
|
||||||
|
// onScroll here to handle updates (e.g. remove the "scroll to last" if we now
|
||||||
|
// can see all messages).
|
||||||
|
this.onScroll();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDirectChatWelcomeHeader() {
|
||||||
|
this.hideDirectChatWelcomeHeader = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
// We change the layout when removing the welcome header, so call
|
// We change the layout when removing the welcome header, so call
|
||||||
// onScroll here to handle updates (e.g. remove the "scroll to last" if we now
|
// onScroll here to handle updates (e.g. remove the "scroll to last" if we now
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
cols="auto"
|
cols="auto"
|
||||||
class="chat-header-members text-start ma-0 pa-0"
|
class="chat-header-members text-start ma-0 pa-0"
|
||||||
>
|
>
|
||||||
<v-avatar size="40" class="clickable me-2 chat-header-avatar" color="grey" @click.stop="onAvatarClicked">
|
<v-avatar size="48" class="clickable me-2 chat-header-avatar" color="grey" @click.stop="onAvatarClicked">
|
||||||
<v-img v-if="roomAvatar" :src="roomAvatar" />
|
<v-img v-if="roomAvatar" :src="roomAvatar" />
|
||||||
<span v-else class="white--text headline">{{
|
<span v-else class="white--text headline">{{
|
||||||
room.name.substring(0, 1).toUpperCase()
|
room.name.substring(0, 1).toUpperCase()
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="auto" class="text-end ma-0 pa-0 ms-1 clickable close-button more-menu-button">
|
<v-col cols="auto" class="text-end ma-0 pa-0 ms-1 clickable close-button more-menu-button">
|
||||||
<div :class="{ 'popup-open': showMoreMenu }">
|
<div :class="{ 'popup-open': showMoreMenu }">
|
||||||
<v-btn class="mx-2 box-shadow-none" fab dark small color="transparent" @click.stop="showMoreMenu = true">
|
<v-btn class="mx-2 box-shadow-none" fab dark small color="transparent" @click.stop="onShowMoreMenu">
|
||||||
<v-icon size="15">$vuetify.icons.ic_more</v-icon>
|
<v-icon size="15">$vuetify.icons.ic_more</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -177,6 +177,11 @@ export default {
|
||||||
this.$emit("view-room-details", { event: this.event });
|
this.$emit("view-room-details", { event: this.event });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
items.push({
|
||||||
|
icon: 'notifications_active', text: this.$t('global.notify'), handler: () => {
|
||||||
|
this.$emit("notify");
|
||||||
|
}
|
||||||
|
});
|
||||||
items.push({
|
items.push({
|
||||||
icon: '$vuetify.icons.ic_member-leave', text: this.$t('leave.leave'), handler: () => {
|
icon: '$vuetify.icons.ic_member-leave', text: this.$t('leave.leave'), handler: () => {
|
||||||
this.leaveRoom();
|
this.leaveRoom();
|
||||||
|
|
@ -227,6 +232,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
onShowMoreMenu() {
|
||||||
|
if(this.publicRoomLink == null) {
|
||||||
|
this.roomJoinRule = this.getRoomJoinRule();
|
||||||
|
}
|
||||||
|
this.showMoreMenu = true
|
||||||
|
},
|
||||||
setHasShownMissedItemsHint() {
|
setHasShownMissedItemsHint() {
|
||||||
this.$store.commit('setHasShownMissedItemsHint', "1");
|
this.$store.commit('setHasShownMissedItemsHint', "1");
|
||||||
this.showMissedItemsInfo = false;
|
this.showMissedItemsInfo = false;
|
||||||
|
|
|
||||||
164
src/components/ChatHeaderPrivate.vue
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
<template>
|
||||||
|
<v-container fluid v-if="room">
|
||||||
|
<v-row class="chat-header-row flex-nowrap">
|
||||||
|
<v-col
|
||||||
|
cols="auto"
|
||||||
|
class="chat-header-members text-start ma-0 pa-0"
|
||||||
|
>
|
||||||
|
<v-avatar size="48" class="clickable me-2 chat-header-avatar rounded-circle" color="grey" @click.stop="onAvatarClicked">
|
||||||
|
<v-img v-if="privatePartyAvatar(40)" :src="privatePartyAvatar(40)" />
|
||||||
|
<span v-else class="white--text headline">{{
|
||||||
|
privateParty.name.substring(0, 1).toUpperCase()
|
||||||
|
}}</span>
|
||||||
|
</v-avatar>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col class="chat-header-name ma-0 pa-0 flex-shrink-1 flex-grow-1 flex-nowrap" @click.stop="onHeaderClicked">
|
||||||
|
<div class="room-title-row">
|
||||||
|
<div class="room-name-inline text-truncate" :title="privateParty.name">
|
||||||
|
{{ privateParty.name }}
|
||||||
|
</div>
|
||||||
|
<v-icon v-if="$matrix.joinedAndInvitedRooms.length > 1" class="icon-dropdown" size="11">$vuetify.icons.ic_dropdown</v-icon>
|
||||||
|
<div v-if="$matrix.joinedAndInvitedRooms.length > 1 && notifications" :class="{ 'notification-alert': true, 'popup-open': showMissedItemsInfo }">
|
||||||
|
<v-icon class="icon-circle" size="11">circle</v-icon>
|
||||||
|
<!-- MISSED ITEMS POPUP -->
|
||||||
|
<!-- <div class="missed-items-popup-background" v-if="showMissedItemsInfo" @click.stop="setHasShownMissedItemsHint()"></div> -->
|
||||||
|
<div class="missed-items-popup" v-if="showMissedItemsInfo" @click.stop="setHasShownMissedItemsHint()">
|
||||||
|
<div class="text">{{ notificationsText }}</div>
|
||||||
|
<div class="button clickable" @click.stop="setHasShownMissedItemsHint()">{{$t('menu.ok')}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="num-members">
|
||||||
|
<div v-if="roomIsEncrypted" class="private-chat"><v-icon color="#616161">$vuetify.icons.ic_lock</v-icon>{{ $t("room_welcome.direct_private_chat") }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col v-if="$matrix.joinedRooms.length > 1" cols="auto" class="text-end ma-0 pa-0 ms-1">
|
||||||
|
<v-avatar :class="{ 'avatar-32': true, 'clickable': true, 'popup-open': showProfileInfo }" size="26"
|
||||||
|
color="#e0e0e0" @click.stop="showProfileInfo = true">
|
||||||
|
<img v-if="userAvatar" :src="userAvatar" />
|
||||||
|
<span v-else class="white--text">{{ userAvatarLetter }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto" class="text-end ma-0 pa-0 ms-1">
|
||||||
|
<v-btn id="btn-purge-room" v-if="userCanPurgeRoom" class="mx-2 box-shadow-none" fab dark small color="red"
|
||||||
|
@click.stop="showPurgeConfirmation = true">
|
||||||
|
<v-icon light>$vuetify.icons.ic_moderator-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<template v-else>
|
||||||
|
<v-btn v-if="$matrix.joinedRooms.length == 1" id="btn-leave-room" class="box-shadow-none leave-button" color="red" @click.stop="leaveRoom">
|
||||||
|
<v-icon color="white">$vuetify.icons.ic_member-leave</v-icon>{{ $t('room.leave') }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn id="btn-leave-room" class="mx-2 box-shadow-none" fab dark small color="red" @click.stop="leaveRoom" v-else>
|
||||||
|
<v-icon color="white">$vuetify.icons.ic_member-leave</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-col>
|
||||||
|
<v-col v-if="$matrix.joinedRooms.length > 1" cols="auto" class="text-end ma-0 pa-0 ms-1 clickable close-button more-menu-button">
|
||||||
|
<div :class="{ 'popup-open': showMoreMenu }">
|
||||||
|
<v-btn class="mx-2 box-shadow-none" fab dark small color="transparent" @click.stop="onShowMoreMenu">
|
||||||
|
<v-icon size="15">$vuetify.icons.ic_more</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- "REALLY LEAVE?" dialog -->
|
||||||
|
<LeaveRoomDialog :show="showLeaveConfirmation" :room="room" @close="showLeaveConfirmation = false" />
|
||||||
|
|
||||||
|
<!-- PROFILE INFO POPUP -->
|
||||||
|
<ProfileInfoPopup :show="showProfileInfo" @close="showProfileInfo = false" />
|
||||||
|
|
||||||
|
<!-- MORE MENU POPUP -->
|
||||||
|
<MoreMenuPopup :show="showMoreMenu" :menuItems="moreMenuItems" @close="showMoreMenu = false"
|
||||||
|
v-on:leave="showLeaveConfirmation = true" />
|
||||||
|
|
||||||
|
<!-- PURGE ROOM POPUP -->
|
||||||
|
<PurgeRoomDialog :show="showPurgeConfirmation" :room="room" @close="showPurgeConfirmation = false" />
|
||||||
|
|
||||||
|
<RoomExport :room="room" v-if="downloadingChat" v-on:close="downloadingChat = false" />
|
||||||
|
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LeaveRoomDialog from "../components/LeaveRoomDialog";
|
||||||
|
import ProfileInfoPopup from "../components/ProfileInfoPopup";
|
||||||
|
import MoreMenuPopup from "../components/MoreMenuPopup";
|
||||||
|
import PurgeRoomDialog from "../components/PurgeRoomDialog";
|
||||||
|
import RoomExport from "../components/RoomExport";
|
||||||
|
|
||||||
|
import ChatHeader from "./ChatHeader.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ChatHeaderPrivate",
|
||||||
|
extends: ChatHeader,
|
||||||
|
components: {
|
||||||
|
LeaveRoomDialog,
|
||||||
|
ProfileInfoPopup,
|
||||||
|
MoreMenuPopup,
|
||||||
|
PurgeRoomDialog,
|
||||||
|
RoomExport
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
|
||||||
|
.popup-open {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-open::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
// Need to move the "more items" arrow to the left, since it's too close to the edge
|
||||||
|
// and would interfere with the dialog rounding...
|
||||||
|
.more-menu-button & {
|
||||||
|
left: calc(50% - 4px);
|
||||||
|
}
|
||||||
|
content: " ";
|
||||||
|
top: 42px;
|
||||||
|
margin-left: -10px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: currentColor;
|
||||||
|
z-index: 400;
|
||||||
|
pointer-events: none;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-name: fadein;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.private-chat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.private-chat .v-icon {
|
||||||
|
width: 9px;
|
||||||
|
height: 11px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -7,11 +7,17 @@
|
||||||
<v-row cols="12" class="qr-container ma-3">
|
<v-row cols="12" class="qr-container ma-3">
|
||||||
<v-col cols="auto">
|
<v-col cols="auto">
|
||||||
<canvas
|
<canvas
|
||||||
@click.stop="showFullScreenQR = true"
|
v-longTap:250="[
|
||||||
|
()=> {
|
||||||
|
showFullScreenQR = true;
|
||||||
|
},
|
||||||
|
(el) => { $emit('long-tap', el); }
|
||||||
|
]"
|
||||||
ref="roomQr"
|
ref="roomQr"
|
||||||
class="qr"
|
class="qr"
|
||||||
id="room-qr"
|
id="room-qr"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
<slot name="share"></slot>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col align-self="center" class="public-link">
|
<v-col align-self="center" class="public-link">
|
||||||
<div class="link">{{ locationLink }}</div>
|
<div class="link">{{ locationLink }}</div>
|
||||||
|
|
@ -32,7 +38,7 @@
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-expand-transition>
|
</v-expand-transition>
|
||||||
<QRCodePopup :show="showFullScreenQR" :message="locationLink" @close="showFullScreenQR = false" />
|
<QRCodePopup :show="showFullScreenQR" :message="locationLink" :title="popupTitle" @close="showFullScreenQR = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -51,7 +57,12 @@ export default {
|
||||||
i18nCopyLinkKey: {
|
i18nCopyLinkKey: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'copy_link'
|
default: 'copy_link'
|
||||||
|
},
|
||||||
|
i18nQrPopupTitleKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'room_info.scan_code'
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -59,6 +70,11 @@ export default {
|
||||||
showFullScreenQR: false,
|
showFullScreenQR: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
popupTitle() {
|
||||||
|
return this.$t(this.i18nQrPopupTitleKey);
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
copyRoomLink() {
|
copyRoomLink() {
|
||||||
if(this.locationLinkCopied) return
|
if(this.locationLinkCopied) return
|
||||||
|
|
@ -87,7 +103,7 @@ export default {
|
||||||
{
|
{
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
margin: 1,
|
margin: 1,
|
||||||
width: 60,
|
width: canvas.getBoundingClientRect().width,
|
||||||
},
|
},
|
||||||
function (error) {
|
function (error) {
|
||||||
if (error) console.error(error);
|
if (error) console.error(error);
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,15 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-card v-if="availableRoomTypes.length > 1" v-show="showOptions" class="room-option account ma-0" flat>
|
<v-card v-if="availableRoomTypes.length > 1" v-show="showOptions" class="room-option account ma-0" flat>
|
||||||
<v-card-text class="with-right-label">
|
<v-card-text>
|
||||||
<div>
|
<div class="d-flex flex-wrap text-left">
|
||||||
<div class="option-title">{{ $t('room_info.room_type') }}</div>
|
<div class="col-12 col-md-6 mr-auto pa-0">
|
||||||
|
<div class="option-title">{{ $t('room_info.room_type') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 pa-0">
|
||||||
|
<RoomTypeSelector v-model="roomType" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RoomTypeSelector v-model="roomType" />
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
<v-card v-if="$config.experimental_read_only_room" v-show="showOptions" class="room-option account ma-0" flat>
|
<v-card v-if="$config.experimental_read_only_room" v-show="showOptions" class="room-option account ma-0" flat>
|
||||||
|
|
@ -333,7 +337,7 @@ export default {
|
||||||
visibility: "private", // Not listed!
|
visibility: "private", // Not listed!
|
||||||
name: this.roomName,
|
name: this.roomName,
|
||||||
preset: "public_chat",
|
preset: "public_chat",
|
||||||
initial_state:
|
initial_state:
|
||||||
this.unencryptedRoom ? [
|
this.unencryptedRoom ? [
|
||||||
{
|
{
|
||||||
type: "m.room.history_visibility",
|
type: "m.room.history_visibility",
|
||||||
|
|
@ -342,7 +346,7 @@ export default {
|
||||||
history_visibility: "shared"
|
history_visibility: "shared"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
] :
|
] :
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "m.room.encryption",
|
type: "m.room.encryption",
|
||||||
|
|
|
||||||
65
src/components/DirectChatWelcomeHeader.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div class="created-room-welcome-header">
|
||||||
|
<div class="mt-2" v-if="privateParty">{{ $t("room_welcome.direct_info", { you: $matrix.currentUserDisplayName, user: privateParty.name }) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import roomInfoMixin from "./roomInfoMixin";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CreatedRoomWelcomeHeader",
|
||||||
|
mixins: [roomInfoMixin],
|
||||||
|
computed: {
|
||||||
|
roomHistoryDescription() {
|
||||||
|
const visibility = this.$matrix.getRoomHistoryVisibility(this.room);
|
||||||
|
switch (visibility) {
|
||||||
|
case "world_readable":
|
||||||
|
return this.$t("room_welcome.room_history_is", {
|
||||||
|
type: this.$t("message.room_history_world_readable"),
|
||||||
|
});
|
||||||
|
case "shared":
|
||||||
|
return this.$t("room_welcome.room_history_is", {
|
||||||
|
type: this.$t("message.room_history_shared"),
|
||||||
|
});
|
||||||
|
case "invited":
|
||||||
|
return this.$t("room_welcome.room_history_is", {
|
||||||
|
type: this.$t("message.room_history_invited"),
|
||||||
|
});
|
||||||
|
case "joined":
|
||||||
|
return this.$t("room_welcome.room_history_joined");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
publicRoomLinkCopied: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copyPublicLink() {
|
||||||
|
const self = this;
|
||||||
|
this.$copyText(this.publicRoomLink).then(
|
||||||
|
function (ignored) {
|
||||||
|
// Success!
|
||||||
|
self.publicRoomLinkCopied = true;
|
||||||
|
setInterval(() => {
|
||||||
|
// Hide again
|
||||||
|
self.publicRoomLinkCopied = false;
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
function (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
251
src/components/GetLink.vue
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
<template>
|
||||||
|
<div class="pa-4 getlink-root fill-height">
|
||||||
|
<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" ref="form">
|
||||||
|
<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-text-field>
|
||||||
|
|
||||||
|
<!-- <div class="error--text" v-if="loadingLoginFlows">Loading login flows...</div> -->
|
||||||
|
|
||||||
|
<div class="error--text" v-if="hasError">{{ this.message }}</div>
|
||||||
|
|
||||||
|
<interactive-auth ref="interactiveAuth" />
|
||||||
|
|
||||||
|
<v-btn id="btn-login" :disabled="!isValid || loading" 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>
|
||||||
|
<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">
|
||||||
|
<template v-slot:share>
|
||||||
|
<div v-if="shareSupported" class="clickable getlink-share" @click="shareLink">
|
||||||
|
<div>{{ $t("getlink.share_qr") }}</div>
|
||||||
|
<v-img src="@/assets/icons/ic_share.svg" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</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 this.defaultData();
|
||||||
|
},
|
||||||
|
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 this.$router.getDMLink(this.$matrix.currentUser, this.$config);
|
||||||
|
},
|
||||||
|
shareSupported() {
|
||||||
|
return !!navigator.share;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
user: {
|
||||||
|
handler() {
|
||||||
|
// Reset manual errors
|
||||||
|
this.userErrorMessage = null;
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
defaultData() {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
getDifferentLink() {
|
||||||
|
this.$store.dispatch("logout");
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// Reset
|
||||||
|
const obj = this.defaultData();
|
||||||
|
Object.keys(obj).forEach(k => this[k] = obj[k]);
|
||||||
|
this.$refs.form.reset();
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goToLoginPage() {
|
||||||
|
this.$navigation.push({ name: "Login", params: { showCreateRoomOption: false, redirect: "GetLink" } }, 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);
|
||||||
|
|
||||||
|
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.loadLoginFlows().then(() => {
|
||||||
|
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;
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCreateRoom() {
|
||||||
|
this.$navigation.push({ name: "CreateRoom" });
|
||||||
|
},
|
||||||
|
onUsernameEnter() {
|
||||||
|
this.handleLogin();
|
||||||
|
},
|
||||||
|
loadLoginFlows() {
|
||||||
|
var user = Object.assign({}, this.user);
|
||||||
|
user.normalize();
|
||||||
|
const server = user.home_server || this.$config.defaultServer;
|
||||||
|
if (server !== this.currentLoginServer) {
|
||||||
|
this.currentLoginServer = server;
|
||||||
|
this.loadingLoginFlows = true;
|
||||||
|
|
||||||
|
const matrixClient = sdk.createClient({ baseUrl: server });
|
||||||
|
return 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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>
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<v-card-text class="pa-0">
|
<v-card-text class="pa-0">
|
||||||
<RoomList
|
<RoomList
|
||||||
showInvites
|
showInvites
|
||||||
showCreate
|
:showCreate="!$config.hide_add_room_on_home"
|
||||||
v-on:newroom="createRoom"
|
v-on:newroom="createRoom"
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<v-container
|
||||||
|
v-else
|
||||||
|
fluid
|
||||||
|
fill-height
|
||||||
|
class="loading-indicator transparent"
|
||||||
|
>
|
||||||
|
<v-row align="center" justify="center">
|
||||||
|
<v-col class="text-center">
|
||||||
|
<interactive-auth ref="interactiveAuth" v-if="waitingForRoomCreation" />
|
||||||
|
<v-progress-circular v-else
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -157,6 +175,7 @@ export default {
|
||||||
loadingMessage: null,
|
loadingMessage: null,
|
||||||
waitingForInfo: true,
|
waitingForInfo: true,
|
||||||
waitingForMembership: false,
|
waitingForMembership: false,
|
||||||
|
waitingForRoomCreation: false,
|
||||||
availableAvatars: [],
|
availableAvatars: [],
|
||||||
selectedProfile: null,
|
selectedProfile: null,
|
||||||
showEditDisplaynameDialog: false,
|
showEditDisplaynameDialog: false,
|
||||||
|
|
@ -236,6 +255,7 @@ export default {
|
||||||
this.roomName = this.removeHomeServer(this.roomId);
|
this.roomName = this.removeHomeServer(this.roomId);
|
||||||
|
|
||||||
this.waitingForInfo = true;
|
this.waitingForInfo = true;
|
||||||
|
this.waitingForRoomCreation = false;
|
||||||
const self = this;
|
const self = this;
|
||||||
this.waitingForMembership = true;
|
this.waitingForMembership = true;
|
||||||
if (this.currentUser) {
|
if (this.currentUser) {
|
||||||
|
|
@ -304,19 +324,10 @@ export default {
|
||||||
});
|
});
|
||||||
} else if (this.roomId.startsWith("@")) {
|
} else if (this.roomId.startsWith("@")) {
|
||||||
// Direct chat with user
|
// Direct chat with user
|
||||||
this.$matrix
|
this.waitingForRoomCreation = true;
|
||||||
.getPublicUserInfo(this.roomId)
|
this.$nextTick(() => {
|
||||||
.then((info) => {
|
this.handleJoin();
|
||||||
console.log("Got user info:", info);
|
});
|
||||||
this.roomName = info.displayname;
|
|
||||||
this.roomAvatar = info.avatar;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Failed to get user info: ", err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.waitingForInfo = false;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Private room, try to get name
|
// Private room, try to get name
|
||||||
const room = this.$matrix.getRoom(this.roomId);
|
const room = this.$matrix.getRoom(this.roomId);
|
||||||
|
|
@ -421,6 +432,8 @@ export default {
|
||||||
console.log("Failed to join room", err);
|
console.log("Failed to join room", err);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.loadingMessage = this.$t("join.join_failed");
|
this.loadingMessage = this.$t("join.join_failed");
|
||||||
|
this.waitingForInfo = false;
|
||||||
|
this.waitingForMembership = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,22 +117,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onLeaveRoom() {
|
onLeaveRoom() {
|
||||||
const lastRoom = this.onlyJoinedToThisRoom();
|
this.$matrix.leaveRoomAndNavigate(this.room.roomId)
|
||||||
//this.$matrix.matrixClient.forget(this.room.roomId, true, undefined)
|
|
||||||
const roomId = this.room.roomId;
|
|
||||||
this.$matrix
|
|
||||||
.leaveRoom(roomId)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showDialog = false;
|
this.showDialog = false;
|
||||||
console.log("Left room");
|
console.log("Left room");
|
||||||
if (lastRoom) {
|
|
||||||
this.$navigation.push({ name: "Goodbye" }, -1);
|
|
||||||
} else {
|
|
||||||
this.$navigation.push(
|
|
||||||
{ name: "Home", params: { roomId: null } },
|
|
||||||
-1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log("Error leaving", err);
|
console.log("Error leaving", err);
|
||||||
|
|
|
||||||
|
|
@ -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="showCreateRoom">{{ $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="showCreateRoom"
|
||||||
>{{ $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, "", ""),
|
||||||
|
|
@ -136,6 +151,9 @@ export default {
|
||||||
showCloseButton() {
|
showCloseButton() {
|
||||||
return this.$navigation && this.$navigation.canPop();
|
return this.$navigation && this.$navigation.canPop();
|
||||||
},
|
},
|
||||||
|
showCreateRoom() {
|
||||||
|
return this.showCreateRoomOption && !this.$config.hide_add_room_on_home;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.loggedIn) {
|
if (this.loggedIn) {
|
||||||
|
|
@ -171,7 +189,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",
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
<v-card flat>
|
<v-card flat>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
|
||||||
<v-container class="mt-0 pa-0 pt-3 action-row-container-no-dividers">
|
<v-container class="mt-0 pa-0 pt-3 pb-3 action-row-container-no-dividers">
|
||||||
<ActionRow v-for="item in menuItems" :key="item.name" :icon="item.icon" :iconSize="16" :text="item.text" @click="$emit('close');item.handler()" />
|
<ActionRow v-for="item in menuItems" :key="item.name" :icon="item.icon" :iconSize="16" :text="item.text" @click="$emit('close');item.handler()" />
|
||||||
|
|
||||||
<v-row class="profile-row clickable" @click="viewProfile" no-gutters align-content="center">
|
<v-row v-if="showProfile" class="profile-row clickable" @click="viewProfile" no-gutters align-content="center">
|
||||||
<v-col cols="auto" class="me-2">
|
<v-col cols="auto" class="me-2">
|
||||||
<v-avatar class="avatar-32" size="32" color="#e0e0e0" @click.stop="viewProfile">
|
<v-avatar class="avatar-32" size="32" color="#e0e0e0" @click.stop="viewProfile">
|
||||||
<img v-if="userAvatar" :src="userAvatar" />
|
<img v-if="userAvatar" :src="userAvatar" />
|
||||||
|
|
@ -40,6 +40,12 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
showProfile: {
|
||||||
|
type: Boolean,
|
||||||
|
default: function () {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
menuItems: {
|
menuItems: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: function() {
|
default: function() {
|
||||||
|
|
@ -116,7 +122,7 @@ export default {
|
||||||
|
|
||||||
.profile-row {
|
.profile-row {
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
padding: 20px 20px !important;
|
padding: 20px 20px 8px 20px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-row:after {
|
.action-row:after {
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<copy-link :locationLink="directMessageLink" >
|
<copy-link :locationLink="directMessageLink" >
|
||||||
<v-card-title class="h2">{{ $t("room_info.contact_link") }}</v-card-title>
|
<v-card-title class="h2">{{ $t("room_info.direct_link") }}</v-card-title>
|
||||||
<v-card-text>{{ $t("room_info.contact_link_desc") }}</v-card-text>
|
<v-card-text>{{ $t("room_info.direct_link_desc") }}</v-card-text>
|
||||||
</copy-link>
|
</copy-link>
|
||||||
|
|
||||||
<v-container class="mt-2 pa-5">
|
<v-container class="mt-2 pa-5">
|
||||||
|
|
@ -243,7 +243,7 @@ export default {
|
||||||
return this.$matrix.currentUser.user_id
|
return this.$matrix.currentUser.user_id
|
||||||
},
|
},
|
||||||
directMessageLink() {
|
directMessageLink() {
|
||||||
return `${window.location.origin + window.location.pathname}#/user/${this.currentUserId}`
|
return this.$router.getDMLink(this.$matrix.currentUser, this.$config);
|
||||||
},
|
},
|
||||||
passwordsMatch() {
|
passwordsMatch() {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
<a :href="'//' + productLink">{{ productLink }}</a>
|
<a :href="'//' + productLink">{{ productLink }}</a>
|
||||||
</template>
|
</template>
|
||||||
</i18n>
|
</i18n>
|
||||||
<div class="text-end">
|
<div class="text-end" v-if="!$config.hide_add_room_on_home">
|
||||||
<v-btn id="btn-new-room" class="new_room" text @click="createRoom">
|
<v-btn id="btn-new-room" class="new_room" text @click="createRoom">
|
||||||
{{ $t("profile_info_popup.new_room") }}
|
{{ $t("profile_info_popup.new_room") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-dialog
|
<v-dialog
|
||||||
|
persistent
|
||||||
v-model="showDialog"
|
v-model="showDialog"
|
||||||
v-show="room" class="ma-0 pa-0"
|
v-show="room" class="ma-0 pa-0"
|
||||||
:width="$vuetify.breakpoint.smAndUp ? '688px' : '95%'"
|
:width="$vuetify.breakpoint.smAndUp ? '688px' : '95%'"
|
||||||
|
|
@ -54,8 +55,8 @@
|
||||||
class="d-inline-block me-2"
|
class="d-inline-block me-2"
|
||||||
src="@/assets/icons/timer.svg"
|
src="@/assets/icons/timer.svg"
|
||||||
/>{{ $t("purge_room.n_seconds", { seconds: timeout }) }}
|
/>{{ $t("purge_room.n_seconds", { seconds: timeout }) }}
|
||||||
<h2 class="dialog-title">{{ $t("purge_room.self_destruct") }}</h2>
|
<h2 class="dialog-title mb-0">{{ $t("purge_room.self_destruct") }}</h2>
|
||||||
<div class="dialog-text">
|
<div class="dialog-text text-center mb-5">
|
||||||
{{ $t("purge_room.notified") }}
|
{{ $t("purge_room.notified") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-text">
|
<div class="dialog-text">
|
||||||
|
|
@ -64,7 +65,7 @@
|
||||||
</template>
|
</template>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<v-row cols="12">
|
<v-row cols="12">
|
||||||
<v-col cols="12">
|
<v-col cols="6">
|
||||||
<v-btn
|
<v-btn
|
||||||
id="btn-purge-room-undo"
|
id="btn-purge-room-undo"
|
||||||
depressed
|
depressed
|
||||||
|
|
@ -76,6 +77,11 @@
|
||||||
>{{ $t("menu.undo") }}</v-btn
|
>{{ $t("menu.undo") }}</v-btn
|
||||||
>
|
>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-btn depressed block class="outlined-button" @click="onDoPurgeRoom">
|
||||||
|
{{ $t("menu.delete_now") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -91,6 +97,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import roomInfoMixin from "./roomInfoMixin";
|
import roomInfoMixin from "./roomInfoMixin";
|
||||||
|
import { STATE_EVENT_ROOM_DELETION_NOTICE } from "../plugins/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LeaveRoomDialog",
|
name: "LeaveRoomDialog",
|
||||||
|
|
@ -118,9 +125,13 @@ export default {
|
||||||
this.showDialog = newVal;
|
this.showDialog = newVal;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
showDialog() {
|
showDialog(val, oldVal) {
|
||||||
if (!this.showDialog) {
|
if (!val && oldVal) {
|
||||||
|
this.undo();
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
|
} else if (val && !oldVal) {
|
||||||
|
// Showing, reset
|
||||||
|
this.status = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -136,7 +147,7 @@ export default {
|
||||||
// Cancel the state event for deletion
|
// Cancel the state event for deletion
|
||||||
this.$matrix.matrixClient.sendStateEvent(
|
this.$matrix.matrixClient.sendStateEvent(
|
||||||
this.room.roomId,
|
this.room.roomId,
|
||||||
"im.keanu.room_deletion_notice",
|
STATE_EVENT_ROOM_DELETION_NOTICE,
|
||||||
{ status: "cancel" }
|
{ status: "cancel" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -146,21 +157,22 @@ export default {
|
||||||
// Send custom state event!
|
// Send custom state event!
|
||||||
this.$matrix.matrixClient.sendStateEvent(
|
this.$matrix.matrixClient.sendStateEvent(
|
||||||
this.room.roomId,
|
this.room.roomId,
|
||||||
"im.keanu.room_deletion_notice",
|
STATE_EVENT_ROOM_DELETION_NOTICE,
|
||||||
{ status: "delete" }
|
{ status: "delete" }
|
||||||
);
|
);
|
||||||
|
|
||||||
this.timeout = 10;
|
this.timeout = 7;
|
||||||
this.timeoutTimer = setInterval(() => {
|
this.timeoutTimer = setInterval(() => {
|
||||||
this.timeout = this.timeout - 1;
|
this.timeout = this.timeout - 1;
|
||||||
if (this.timeout == 0) {
|
if (this.timeout == 0) {
|
||||||
clearInterval(this.timeoutTimer);
|
|
||||||
this.timeoutTimer = null;
|
|
||||||
this.onDoPurgeRoom();
|
this.onDoPurgeRoom();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
onDoPurgeRoom() {
|
onDoPurgeRoom() {
|
||||||
|
this.timeout = 0
|
||||||
|
clearInterval(this.timeoutTimer);
|
||||||
|
this.timeoutTimer = null;
|
||||||
this.isPurging = true;
|
this.isPurging = true;
|
||||||
this.$matrix
|
this.$matrix
|
||||||
.purgeRoom(this.room.roomId, this.onPurgeStatus)
|
.purgeRoom(this.room.roomId, this.onPurgeStatus)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="d-flex justify-center">
|
<div class="d-flex justify-center">
|
||||||
<canvas ref="qr" class="qr" id="qr" :style="qrStyle"></canvas>
|
<canvas ref="qr" class="qr" id="qr" :style="qrStyle"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ $t("room_info.scan_code") }}</div>
|
<div>{{ title }}</div>
|
||||||
</div>
|
</div>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -32,6 +32,12 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: function () {
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@
|
||||||
<transition name="slow-fade">
|
<transition name="slow-fade">
|
||||||
<div
|
<div
|
||||||
v-if="mounted"
|
v-if="mounted"
|
||||||
class="goodbye-profile"
|
class="goodbye-profile clickable"
|
||||||
|
@click.stop="viewOtherRooms"
|
||||||
>
|
>
|
||||||
<div class="d-inline-block me-2 white--text">
|
<div class="d-inline-block me-2 white--text">
|
||||||
{{ $t("profile_info_popup.you_are") }}
|
{{ $t("profile_info_popup.you_are") }}
|
||||||
|
|
@ -66,7 +67,6 @@
|
||||||
class="avatar-32 d-inline-block"
|
class="avatar-32 d-inline-block"
|
||||||
size="32"
|
size="32"
|
||||||
color="#e0e0e0"
|
color="#e0e0e0"
|
||||||
@click.stop="showProfileInfo = true"
|
|
||||||
>
|
>
|
||||||
<img v-if="userAvatar" :src="userAvatar" />
|
<img v-if="userAvatar" :src="userAvatar" />
|
||||||
<span v-else class="white--text">{{ userAvatarLetter }}</span>
|
<span v-else class="white--text">{{ userAvatarLetter }}</span>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-root">
|
<div class="chat-root">
|
||||||
<div class="chat-root d-flex flex-column" ref="exportRoot">
|
<div class="chat-root export d-flex flex-column" ref="exportRoot">
|
||||||
<!-- Header-->
|
<!-- Header-->
|
||||||
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
|
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
|
||||||
<v-row class="chat-header-row flex-nowrap">
|
<v-row class="chat-header-row flex-nowrap">
|
||||||
|
|
@ -18,18 +18,17 @@
|
||||||
<div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer">
|
<div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer">
|
||||||
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
|
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
|
||||||
<!-- DAY Marker, shown for every new day in the timeline -->
|
<!-- DAY Marker, shown for every new day in the timeline -->
|
||||||
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker" :title="dateForEvent(event)" />
|
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker">
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="text">{{ dayForEvent(event) }}</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
|
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
|
||||||
<div class="message-wrapper">
|
<div class="message-wrapper">
|
||||||
<component
|
<component :is="componentForEvent(event, true)" :room="room" :originalEvent="event"
|
||||||
:is="componentForEvent(event, true)"
|
:nextEvent="events[index + 1]" :timelineSet="timelineSet" :componentFn="componentForEventForExport"
|
||||||
:room="room"
|
ref="exportedEvent" v-on:layout-change="onLayoutChange" />
|
||||||
:originalEvent="event"
|
|
||||||
:nextEvent="events[index + 1]"
|
|
||||||
:timelineSet="timelineSet"
|
|
||||||
ref="exportedEvent"
|
|
||||||
/>
|
|
||||||
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
||||||
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,6 +53,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from "vue";
|
||||||
import MessageIncomingText from "./messages/MessageIncomingText.vue";
|
import MessageIncomingText from "./messages/MessageIncomingText.vue";
|
||||||
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
|
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
|
||||||
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
||||||
|
|
@ -98,6 +98,7 @@ import util from "../plugins/utils";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import { EventTimelineSet } from "matrix-js-sdk";
|
import { EventTimelineSet } from "matrix-js-sdk";
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "RoomExport",
|
name: "RoomExport",
|
||||||
|
|
@ -146,7 +147,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
room: {
|
room: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: function() {
|
default: function () {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -181,6 +182,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
componentForEventForExport(event) {
|
||||||
|
return this.componentForEvent(event, true);
|
||||||
|
},
|
||||||
cancelExport() {
|
cancelExport() {
|
||||||
this.cancelled = true;
|
this.cancelled = true;
|
||||||
},
|
},
|
||||||
|
|
@ -254,6 +258,21 @@ export default {
|
||||||
this.timelineSet.addEventsToTimeline(events.reverse(), true, this.timelineSet.getLiveTimeline(), "");
|
this.timelineSet.addEventsToTimeline(events.reverse(), true, this.timelineSet.getLiveTimeline(), "");
|
||||||
this.events = events;
|
this.events = events;
|
||||||
|
|
||||||
|
// Need to set thread root events and replyEvents so stuff is rendered correctly.
|
||||||
|
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => {
|
||||||
|
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
|
||||||
|
if (parentEvent) {
|
||||||
|
Vue.set(parentEvent, "isMxThread", true);
|
||||||
|
Vue.set(event, "parentThread", parentEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => {
|
||||||
|
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
|
||||||
|
if (parentEvent) {
|
||||||
|
Vue.set(event, "replyEvent", parentEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Wait a tick so UI is updated.
|
// Wait a tick so UI is updated.
|
||||||
return new Promise((resolve, ignoredReject) => {
|
return new Promise((resolve, ignoredReject) => {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
@ -264,129 +283,192 @@ export default {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// UI updated, start processing events
|
// UI updated, start processing events
|
||||||
zip = new JSZip();
|
zip = new JSZip();
|
||||||
|
var avatarFolder = zip.folder("avatars");
|
||||||
var imageFolder = zip.folder("images");
|
var imageFolder = zip.folder("images");
|
||||||
var audioFolder = zip.folder("audio");
|
var audioFolder = zip.folder("audio");
|
||||||
var videoFolder = zip.folder("video");
|
var videoFolder = zip.folder("video");
|
||||||
|
|
||||||
var downloadPromises = [];
|
var downloadPromises = [];
|
||||||
let components = this.$refs.exportedEvent;
|
let components = this.$refs.exportedEvent;
|
||||||
for (const comp of components) {
|
for (const parentComp of components) {
|
||||||
let componentClass = comp.$vnode.tag.split("-").reverse()[0];
|
let childComponents = [parentComp];
|
||||||
switch (componentClass) {
|
|
||||||
case "MessageIncomingImageExport":
|
|
||||||
case "MessageOutgoingImageExport":
|
|
||||||
// TODO - maybe consider what media to download based on the file size we already have?
|
|
||||||
// info = comp.event.getContent().info;
|
|
||||||
// if (info && info.size && currentMediaSize + info.size > maxMediaSize) {
|
|
||||||
// // No need to even download.
|
|
||||||
// console.log("Dont download!");
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
downloadPromises.push(
|
// Some components, i.e. the media threads, have subcomponents
|
||||||
util
|
// that we want to export. So pickup subcomponents here as well.
|
||||||
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
if (parentComp.$refs && parentComp.$refs.exportedEvent) {
|
||||||
.then((blob) => {
|
if (Array.isArray(parentComp.$refs.exportedEvent)) {
|
||||||
return new Promise((resolve, ignoredReject) => {
|
for (const child of parentComp.$refs.exportedEvent) {
|
||||||
let mime = blob.type;
|
childComponents.push(child);
|
||||||
var extension = ".png";
|
}
|
||||||
switch (mime) {
|
} else {
|
||||||
case "image/jpeg":
|
childComponents.push(parentComp.$refs.exportedEvent);
|
||||||
case "image/jpg":
|
}
|
||||||
extension = ".jpg";
|
}
|
||||||
break;
|
for (const comp of childComponents) {
|
||||||
case "image/gif":
|
|
||||||
extension = ".gif";
|
// Avatars need downloading?
|
||||||
|
if (comp.$el) {
|
||||||
|
const avatars = comp.$el.getElementsByClassName("v-avatar");
|
||||||
|
if (avatars && avatars.length > 0) {
|
||||||
|
const member = this.room.getMember(comp.event.getSender());
|
||||||
|
if (member) {
|
||||||
|
const fileName = comp.event.getSender() + ".png";
|
||||||
|
|
||||||
|
const setSource = (fileName) => {
|
||||||
|
for (let avatarIndex = 0; avatarIndex < avatars.length; avatarIndex++) {
|
||||||
|
const avatarElement = avatars[avatarIndex];
|
||||||
|
const images = avatarElement.getElementsByTagName("img");
|
||||||
|
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
|
||||||
|
const img = images[imageIndex];
|
||||||
|
img.onerror = undefined;
|
||||||
|
img.src = './avatars/' + fileName;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!avatarFolder.file(fileName)) {
|
||||||
|
const url = member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true);
|
||||||
|
if (url) {
|
||||||
|
avatarFolder.file(fileName, "empty");
|
||||||
|
downloadPromises.push(
|
||||||
|
axios.get(url, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
if (result.data) {
|
||||||
|
avatarFolder.file(fileName, result.data);
|
||||||
|
setSource(fileName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Download error: ", err);
|
||||||
|
avatarFolder.remove(fileName);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSource(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let componentClass = comp.$vnode.tag.split("-").reverse()[0];
|
||||||
|
switch (componentClass) {
|
||||||
|
case "MessageIncomingImageExport":
|
||||||
|
case "MessageOutgoingImageExport":
|
||||||
|
// TODO - maybe consider what media to download based on the file size we already have?
|
||||||
|
// info = comp.event.getContent().info;
|
||||||
|
// if (info && info.size && currentMediaSize + info.size > maxMediaSize) {
|
||||||
|
// // No need to even download.
|
||||||
|
// console.log("Dont download!");
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
downloadPromises.push(
|
||||||
|
util
|
||||||
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
|
.then((blob) => {
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
|
let mime = blob.type;
|
||||||
|
var extension = ".png";
|
||||||
|
switch (mime) {
|
||||||
|
case "image/jpeg":
|
||||||
|
case "image/jpg":
|
||||||
|
extension = ".jpg";
|
||||||
|
break;
|
||||||
|
case "image/gif":
|
||||||
|
extension = ".gif";
|
||||||
|
}
|
||||||
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
|
currentMediaSize += blob.size;
|
||||||
|
|
||||||
|
let fileName = comp.event.getId() + extension;
|
||||||
|
imageFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
|
||||||
|
let blobUrl = URL.createObjectURL(blob);
|
||||||
|
comp.src = blobUrl;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// Update source
|
||||||
|
let elements = comp.$el.getElementsByClassName("v-image__image");
|
||||||
|
let element = elements && elements[0];
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundImage = 'url("./images/' + fileName + '")';
|
||||||
|
element.classList.remove("v-image__image--preload");
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(blobUrl); // Give the blob back
|
||||||
|
this.processedEvents += 1;
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((ignoredErr) => {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "MessageIncomingAudioExport":
|
||||||
|
case "MessageOutgoingAudioExport":
|
||||||
|
downloadPromises.push(
|
||||||
|
util
|
||||||
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
|
.then((blob) => {
|
||||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
currentMediaSize += blob.size;
|
currentMediaSize += blob.size;
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
let fileName = comp.event.getId() + extension;
|
//let mime = blob.type;
|
||||||
imageFolder.file(fileName, blob); // TODO calc bytes
|
var extension = ".mp3";
|
||||||
|
let fileName = comp.event.getId() + extension;
|
||||||
let blobUrl = URL.createObjectURL(blob);
|
audioFolder.file(fileName, blob); // TODO calc bytes
|
||||||
comp.src = blobUrl;
|
let elements = comp.$el.getElementsByTagName("audio");
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
// Update source
|
|
||||||
let elements = comp.$el.getElementsByClassName("v-image__image");
|
|
||||||
let element = elements && elements[0];
|
let element = elements && elements[0];
|
||||||
if (element) {
|
if (element) {
|
||||||
element.style.backgroundImage = 'url("./images/' + fileName + '")';
|
element.src = "./audio/" + fileName;
|
||||||
element.classList.remove("v-image__image--preload");
|
|
||||||
}
|
}
|
||||||
URL.revokeObjectURL(blobUrl); // Give the blob back
|
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
})
|
.catch((ignoredErr) => {
|
||||||
.catch((ignoredErr) => {
|
this.processedEvents += 1;
|
||||||
this.processedEvents += 1;
|
})
|
||||||
})
|
);
|
||||||
);
|
break;
|
||||||
break;
|
case "MessageIncomingVideoExport":
|
||||||
case "MessageIncomingAudioExport":
|
case "MessageOutgoingVideoExport":
|
||||||
case "MessageOutgoingAudioExport":
|
downloadPromises.push(
|
||||||
downloadPromises.push(
|
util
|
||||||
util
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
.then((blob) => {
|
||||||
.then((blob) => {
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
currentMediaSize += blob.size;
|
||||||
currentMediaSize += blob.size;
|
return new Promise((resolve, ignoredReject) => {
|
||||||
return new Promise((resolve, ignoredReject) => {
|
//let mime = blob.type;
|
||||||
//let mime = blob.type;
|
var extension = ".mp4";
|
||||||
var extension = ".mp3";
|
let fileName = comp.event.getId() + extension;
|
||||||
let fileName = comp.event.getId() + extension;
|
videoFolder.file(fileName, blob); // TODO calc bytes
|
||||||
audioFolder.file(fileName, blob); // TODO calc bytes
|
let elements = comp.$el.getElementsByTagName("video");
|
||||||
let elements = comp.$el.getElementsByTagName("audio");
|
let element = elements && elements[0];
|
||||||
let element = elements && elements[0];
|
if (element) {
|
||||||
if (element) {
|
element.src = "./video/" + fileName;
|
||||||
element.src = "./audio/" + fileName;
|
}
|
||||||
}
|
this.processedEvents += 1;
|
||||||
this.processedEvents += 1;
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((ignoredErr) => {
|
|
||||||
this.processedEvents += 1;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "MessageIncomingVideoExport":
|
|
||||||
case "MessageOutgoingVideoExport":
|
|
||||||
downloadPromises.push(
|
|
||||||
util
|
|
||||||
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
|
||||||
.then((blob) => {
|
|
||||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
|
||||||
currentMediaSize += blob.size;
|
|
||||||
return new Promise((resolve, ignoredReject) => {
|
|
||||||
//let mime = blob.type;
|
|
||||||
var extension = ".mp4";
|
|
||||||
let fileName = comp.event.getId() + extension;
|
|
||||||
videoFolder.file(fileName, blob); // TODO calc bytes
|
|
||||||
let elements = comp.$el.getElementsByTagName("video");
|
|
||||||
let element = elements && elements[0];
|
|
||||||
if (element) {
|
|
||||||
element.src = "./video/" + fileName;
|
|
||||||
}
|
|
||||||
this.processedEvents += 1;
|
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((ignoredErr) => {
|
.catch((ignoredErr) => {
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.all(downloadPromises);
|
return Promise.all(downloadPromises);
|
||||||
|
|
@ -410,7 +492,7 @@ export default {
|
||||||
}
|
}
|
||||||
doc +=
|
doc +=
|
||||||
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
||||||
const getCssRules = function(el) {
|
const getCssRules = function (el) {
|
||||||
if (el.classList.contains("op-button")) {
|
if (el.classList.contains("op-button")) {
|
||||||
el.innerHTML = "";
|
el.innerHTML = "";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -441,6 +523,30 @@ export default {
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onLayoutChange(action, ignoredelement) {
|
||||||
|
action();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chat-root.export {
|
||||||
|
.messageIn-thread, .messageOut-thread {
|
||||||
|
/** For media threads, hide all duplicated metadata, like
|
||||||
|
sender, sender avatar, time, quick reactions etc. They are
|
||||||
|
shown for the root thread event */
|
||||||
|
.messageIn {
|
||||||
|
margin-left: 50px !important;
|
||||||
|
}
|
||||||
|
.messageOut {
|
||||||
|
margin-right: 50px !important;
|
||||||
|
}
|
||||||
|
.messageIn, .messageOut {
|
||||||
|
.quick-reaction-container, .senderAndTime, .avatar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<div class="room-name no-upper">{{ $t("room_info.title") }}</div>
|
<div class="room-name no-upper">{{ $t("room_info.title") }}</div>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
v-if="!userCanPurgeRoom"
|
||||||
id="btn-leave-room"
|
id="btn-leave-room"
|
||||||
color="black"
|
color="black"
|
||||||
depressed
|
depressed
|
||||||
|
|
@ -20,6 +21,16 @@
|
||||||
@click.stop="showLeaveConfirmation = true"
|
@click.stop="showLeaveConfirmation = true"
|
||||||
>👋 {{ $t("room_info.leave_room") }}</v-btn
|
>👋 {{ $t("room_info.leave_room") }}</v-btn
|
||||||
>
|
>
|
||||||
|
<!-- PURGE ROOM -->
|
||||||
|
<v-btn
|
||||||
|
v-else
|
||||||
|
id="btn-purge-room"
|
||||||
|
color="red"
|
||||||
|
class="filled-button"
|
||||||
|
@click.stop="showPurgeConfirmation = true"
|
||||||
|
>
|
||||||
|
<v-icon light>$vuetify.icons.ic_moderator-delete</v-icon> {{ $t("room_info.purge") }}
|
||||||
|
</v-btn>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -127,13 +138,17 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-card class="account ma-3" flat v-if="availableRoomTypes.length > 1 || canChangeReadOnly()">
|
<v-card class="account ma-3" flat v-if="(iAmAdmin() && availableRoomTypes.length > 1) || canChangeReadOnly()">
|
||||||
<v-card-title class="h2 with-right-label"><div>{{ $t("room_info.experimental_features") }}</div><div></div></v-card-title>
|
<v-card-title class="h2 with-right-label"><div>{{ $t("room_info.experimental_features") }}</div><div></div></v-card-title>
|
||||||
<v-card-text class="with-right-label" v-if="availableRoomTypes.length > 1">
|
<v-card-text v-if="iAmAdmin() && availableRoomTypes.length > 1">
|
||||||
<div>
|
<div class="d-flex flex-wrap">
|
||||||
<div class="option-title">{{ $t('room_info.room_type') }}</div>
|
<div class="col-12 col-md-6 mr-auto pa-0">
|
||||||
|
<div class="option-title">{{ $t('room_info.room_type') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 pa-0">
|
||||||
|
<RoomTypeSelector v-model="roomType" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RoomTypeSelector v-model="roomType" />
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-text class="with-right-label" v-if="canChangeReadOnly()">
|
<v-card-text class="with-right-label" v-if="canChangeReadOnly()">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -216,20 +231,6 @@
|
||||||
>{{ $t("room_info.export_room") }}</v-btn>
|
>{{ $t("room_info.export_room") }}</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PURGE ROOM -->
|
|
||||||
<div class="members ma-3 pa-3 text-center">
|
|
||||||
<v-btn
|
|
||||||
id="btn-purge-room"
|
|
||||||
v-if="userCanPurgeRoom"
|
|
||||||
color="red"
|
|
||||||
fab
|
|
||||||
class="filled-button"
|
|
||||||
@click.stop="showPurgeConfirmation = true"
|
|
||||||
>
|
|
||||||
<v-icon light>$vuetify.icons.ic_moderator-delete</v-icon> {{ $t("room_info.purge") }}
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="build-version">
|
<div class="build-version">
|
||||||
{{ $t("room_info.version_info", { version: buildVersion }) }}
|
{{ $t("room_info.version_info", { version: buildVersion }) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -260,7 +261,7 @@ import CopyLink from "../components/CopyLink.vue"
|
||||||
import RoomTypeSelector from "./RoomTypeSelector.vue";
|
import RoomTypeSelector from "./RoomTypeSelector.vue";
|
||||||
import roomInfoMixin from "./roomInfoMixin";
|
import roomInfoMixin from "./roomInfoMixin";
|
||||||
import roomTypeMixin from "./roomTypeMixin";
|
import roomTypeMixin from "./roomTypeMixin";
|
||||||
import util, { ROOM_TYPE_DEFAULT, ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE } from "../plugins/utils";
|
import util, { STATE_EVENT_ROOM_TYPE } from "../plugins/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "RoomInfo",
|
name: "RoomInfo",
|
||||||
|
|
@ -340,25 +341,29 @@ export default {
|
||||||
|
|
||||||
roomType: {
|
roomType: {
|
||||||
get: function () {
|
get: function () {
|
||||||
if (this.room && this.room.tags) {
|
// if (this.room && this.room.tags) {
|
||||||
let options = this.room.tags["ui_options"] || {}
|
// let options = this.room.tags["ui_options"] || {}
|
||||||
if (options["voice_mode"]) {
|
// if (options["voice_mode"]) {
|
||||||
return ROOM_TYPE_VOICE_MODE;
|
// return ROOM_TYPE_VOICE_MODE;
|
||||||
} else if (options["file_mode"]) {
|
// } else if (options["file_mode"]) {
|
||||||
return ROOM_TYPE_FILE_MODE;
|
// return ROOM_TYPE_FILE_MODE;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return ROOM_TYPE_DEFAULT;
|
// return ROOM_TYPE_DEFAULT;
|
||||||
|
return util.roomDisplayTypeOverride(this.room) || this.roomDisplayType;
|
||||||
},
|
},
|
||||||
set: function (roomType) {
|
set: function (roomType) {
|
||||||
if (this.room) {
|
if (this.room) {
|
||||||
let tags = this.room.tags || {};
|
// let tags = this.room.tags || {};
|
||||||
let options = tags["ui_options"] || {}
|
// let options = tags["ui_options"] || {}
|
||||||
options["voice_mode"] = (roomType == ROOM_TYPE_VOICE_MODE ? 1 : 0);
|
// options["voice_mode"] = (roomType == ROOM_TYPE_VOICE_MODE ? 1 : 0);
|
||||||
options["file_mode"] = (roomType == ROOM_TYPE_FILE_MODE ? 1 : 0);
|
// options["file_mode"] = (roomType == ROOM_TYPE_FILE_MODE ? 1 : 0);
|
||||||
tags["ui_options"] = options;
|
// tags["ui_options"] = options;
|
||||||
this.room.tags = tags;
|
// this.room.tags = tags;
|
||||||
this.$matrix.matrixClient.setRoomTag(this.room.roomId, "ui_options", options);
|
// this.$matrix.matrixClient.setRoomTag(this.room.roomId, "ui_options", options);
|
||||||
|
if (this.iAmAdmin()) {
|
||||||
|
this.$matrix.matrixClient.sendStateEvent(this.room.roomId, STATE_EVENT_ROOM_TYPE, { type: roomType });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -542,6 +547,12 @@ export default {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
iAmAdmin() {
|
||||||
|
if (this.room) {
|
||||||
|
return this.room.currentState && this.room.currentState.maySendStateEvent("m.room.power_levels", this.$matrix.currentUserId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
// TODO - following power level comparisons assume that default power levels are used in the room!
|
// TODO - following power level comparisons assume that default power levels are used in the room!
|
||||||
isAdmin(member) {
|
isAdmin(member) {
|
||||||
return member.powerLevelNorm > 50;
|
return member.powerLevelNorm > 50;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
:showCloseButton="false"
|
:showCloseButton="false"
|
||||||
>
|
>
|
||||||
<div class="room-info-sheet" ref="roomInfoSheetContent">
|
<div class="room-info-sheet" ref="roomInfoSheetContent">
|
||||||
<room-list v-on:close="close" v-on:newroom="createRoom" :showCreate="true" />
|
<room-list v-on:close="close" v-on:newroom="createRoom" :showCreate="!$config.hide_add_room_on_home" />
|
||||||
</div>
|
</div>
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<!-- invites -->
|
<!-- invites -->
|
||||||
<v-list-item :disabled="roomsProcessing[room.roomId]" v-for="room in invitedRooms" :key="room.roomId"
|
<v-list-item :disabled="roomsProcessing[room.roomId]" v-for="room in invitedRooms" :key="room.roomId"
|
||||||
:value="room.roomId" class="room-list-room">
|
:value="room" class="room-list-room">
|
||||||
<v-list-item-avatar size="42" color="#d9d9d9">
|
<v-list-item-avatar size="42" color="#d9d9d9">
|
||||||
<v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" />
|
<v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" />
|
||||||
<span v-else class="white--text headline">{{
|
<span v-else class="white--text headline">{{
|
||||||
|
|
@ -29,13 +29,13 @@
|
||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
<v-btn id="btn-accept" class="filled-button" depressed color="black" @click.stop="acceptInvitation(room)">{{
|
<v-btn id="btn-accept" class="filled-button" depressed color="black" @click.stop="acceptInvitation(room)">{{
|
||||||
$t("menu.join") }}</v-btn>
|
$t("menu.join") }}</v-btn>
|
||||||
<v-btn id="btn-reject" class="filled-button" color="black" @click.stop="rejectInvitation(room)" text>{{
|
<v-btn v-if="!room.isServiceNoticeRoom" id="btn-reject" class="filled-button" color="black" @click.stop="rejectInvitation(room)" text>{{
|
||||||
$t("menu.ignore") }}</v-btn>
|
$t("menu.ignore") }}</v-btn>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room.roomId" class="room-list-room">
|
<v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room" class="room-list-room">
|
||||||
<v-list-item-avatar size="42" color="#d9d9d9">
|
<v-list-item-avatar size="42" color="#d9d9d9" :class="[{'rounded-circle': isDirect(room)}]">
|
||||||
<v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" />
|
<v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" />
|
||||||
<span v-else class="white--text headline">{{
|
<span v-else class="white--text headline">{{
|
||||||
room.name.substring(0, 1).toUpperCase()
|
room.name.substring(0, 1).toUpperCase()
|
||||||
|
|
@ -181,16 +181,19 @@ export default {
|
||||||
return this.$matrix.isDirectRoom(room);
|
return this.$matrix.isDirectRoom(room);
|
||||||
},
|
},
|
||||||
|
|
||||||
roomChange(roomId) {
|
roomChange(room) {
|
||||||
if (roomId == null || roomId == undefined) {
|
if (room == null || room == undefined) {
|
||||||
// Ignore, this is caused by "new room" etc.
|
// Ignore, this is caused by "new room" etc.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (room.isServiceNoticeRoom && room.selfMembership === "invite") {
|
||||||
|
return; // Nothing should happen when click on invite to server notices room, just the "join" button is enabled.
|
||||||
|
}
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
this.$navigation.push(
|
this.$navigation.push(
|
||||||
{
|
{
|
||||||
name: "Chat",
|
name: "Chat",
|
||||||
params: { roomId: util.sanitizeRoomId(roomId) },
|
params: { roomId: util.sanitizeRoomId(room.roomId) },
|
||||||
},
|
},
|
||||||
-1
|
-1
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@
|
||||||
<v-btn v-show="state == states.IMPORTED" icon @click.stop="previewAudio">
|
<v-btn v-show="state == states.IMPORTED" icon @click.stop="previewAudio">
|
||||||
<v-icon color="white">$vuetify.icons.audio_import_play</v-icon>
|
<v-icon color="white">$vuetify.icons.audio_import_play</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn v-show="state == states.PLAYING" icon @click.stop="pauseAudio">
|
||||||
|
<v-icon color="white">pause</v-icon>
|
||||||
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="4" align="center">
|
<v-col cols="4" align="center">
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|
@ -37,7 +40,7 @@
|
||||||
<v-icon color="white">stop</v-icon>
|
<v-icon color="white">stop</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-else-if="state == states.RECORDED || state == states.IMPORTED"
|
v-else-if="state == states.RECORDED || state == states.IMPORTED || state == states.PLAYING"
|
||||||
id="btn-send"
|
id="btn-send"
|
||||||
class="voice-recorder-btn recorded"
|
class="voice-recorder-btn recorded"
|
||||||
icon
|
icon
|
||||||
|
|
@ -110,12 +113,12 @@
|
||||||
{{ recordingTime }}
|
{{ recordingTime }}
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="3">
|
<v-col cols="3" class="pa-0">
|
||||||
<v-btn id="btn-record-cancel" @click.stop="cancelRecording" text class="swipe-info">{{
|
<v-btn id="btn-record-cancel" @click.stop="cancelRecording" text class="swipe-info">{{
|
||||||
$t("menu.cancel")
|
$t("menu.cancel")
|
||||||
}}</v-btn>
|
}}</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="3">
|
<v-col cols="3" class="pa-0">
|
||||||
<v-btn id="btn-record-stop" @click.stop="stopRecording" icon class="swipe-info"
|
<v-btn id="btn-record-stop" @click.stop="stopRecording" icon class="swipe-info"
|
||||||
><v-icon color="white">stop</v-icon></v-btn
|
><v-icon color="white">stop</v-icon></v-btn
|
||||||
>
|
>
|
||||||
|
|
@ -159,7 +162,8 @@ const State = {
|
||||||
RECORDING: "recording",
|
RECORDING: "recording",
|
||||||
RECORDED: "recorded",
|
RECORDED: "recorded",
|
||||||
ERROR: "error",
|
ERROR: "error",
|
||||||
IMPORTED: "imported"
|
IMPORTED: "imported",
|
||||||
|
PLAYING: "playing"
|
||||||
};
|
};
|
||||||
import util from "../plugins/utils";
|
import util from "../plugins/utils";
|
||||||
import VoiceRecorderLock from "./VoiceRecorderLock";
|
import VoiceRecorderLock from "./VoiceRecorderLock";
|
||||||
|
|
@ -253,6 +257,7 @@ export default {
|
||||||
this.state = State.INITIAL;
|
this.state = State.INITIAL;
|
||||||
this.errorMessage = null;
|
this.errorMessage = null;
|
||||||
this.recordedFile = null;
|
this.recordedFile = null;
|
||||||
|
this.previewPlayer = null;
|
||||||
this.recordingTime = String.fromCharCode(160);
|
this.recordingTime = String.fromCharCode(160);
|
||||||
if (this.usePTT) {
|
if (this.usePTT) {
|
||||||
document.addEventListener("mouseup", this.mouseUp, false);
|
document.addEventListener("mouseup", this.mouseUp, false);
|
||||||
|
|
@ -321,6 +326,11 @@ export default {
|
||||||
this.stopRecordTimer();
|
this.stopRecordTimer();
|
||||||
this.recordingTime = String.fromCharCode(160); // nbsp;
|
this.recordingTime = String.fromCharCode(160); // nbsp;
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
|
this.previewPlayer = null;
|
||||||
|
this.recordedFile = null;
|
||||||
|
if (this.$refs.audio_import) {
|
||||||
|
this.$refs.audio_import.value = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mouseUp(ignoredEvent) {
|
mouseUp(ignoredEvent) {
|
||||||
document.removeEventListener("mouseup", this.mouseUp, false);
|
document.removeEventListener("mouseup", this.mouseUp, false);
|
||||||
|
|
@ -385,7 +395,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
screenLocked() {
|
screenLocked() {
|
||||||
if (document.visibilityState === "hidden" && this.state == State.RECORDING) {
|
if (document.visibilityState === "hidden" && this.state == State.RECORDING) {
|
||||||
this.pauseRecording();
|
this.pauseRecording();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -451,6 +461,12 @@ export default {
|
||||||
},
|
},
|
||||||
send() {
|
send() {
|
||||||
this.$emit("file", { file: this.recordedFile });
|
this.$emit("file", { file: this.recordedFile });
|
||||||
|
if (this.previewPlayer) {
|
||||||
|
this.previewPlayer.pause();
|
||||||
|
}
|
||||||
|
if (this.$refs.audio_import) {
|
||||||
|
this.$refs.audio_import.value = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getFile(send) {
|
getFile(send) {
|
||||||
const duration = Date.now() - this.recordStartedAt;
|
const duration = Date.now() - this.recordStartedAt;
|
||||||
|
|
@ -532,6 +548,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
previewAudio() {
|
previewAudio() {
|
||||||
|
this.state = State.PLAYING;
|
||||||
if (this.recordedFile) {
|
if (this.recordedFile) {
|
||||||
if (!this.previewPlayer) {
|
if (!this.previewPlayer) {
|
||||||
this.previewPlayer = new Audio();
|
this.previewPlayer = new Audio();
|
||||||
|
|
@ -543,6 +560,10 @@ export default {
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(this.recordedFile);
|
reader.readAsDataURL(this.recordedFile);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
pauseAudio() {
|
||||||
|
this.state = State.IMPORTED;
|
||||||
|
this.previewPlayer.pause();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import util from "../plugins/utils";
|
import util, { STATE_EVENT_ROOM_DELETION_NOTICE } from "../plugins/utils";
|
||||||
import MessageIncomingText from "./messages/MessageIncomingText";
|
import MessageIncomingText from "./messages/MessageIncomingText";
|
||||||
import MessageIncomingFile from "./messages/MessageIncomingFile";
|
import MessageIncomingFile from "./messages/MessageIncomingFile";
|
||||||
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
|
||||||
|
|
@ -14,12 +14,15 @@ import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
|
||||||
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
|
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
|
||||||
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
|
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
|
||||||
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
|
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
|
||||||
|
import MessageOutgoingThread from "./messages/MessageOutgoingThread.vue";
|
||||||
import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
|
import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
|
||||||
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
|
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
|
||||||
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
|
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
|
||||||
|
import MessageIncomingThreadExport from "./messages/export/MessageIncomingThreadExport";
|
||||||
import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport";
|
import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport";
|
||||||
import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport";
|
import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport";
|
||||||
import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport";
|
import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport";
|
||||||
|
import MessageOutgoingThreadExport from "./messages/export/MessageOutgoingThreadExport";
|
||||||
import ContactJoin from "./messages/ContactJoin.vue";
|
import ContactJoin from "./messages/ContactJoin.vue";
|
||||||
import ContactLeave from "./messages/ContactLeave.vue";
|
import ContactLeave from "./messages/ContactLeave.vue";
|
||||||
import ContactInvited from "./messages/ContactInvited.vue";
|
import ContactInvited from "./messages/ContactInvited.vue";
|
||||||
|
|
@ -49,8 +52,10 @@ import RoomGuestAccessChanged from "./messages/RoomGuestAccessChanged.vue";
|
||||||
import RoomEncrypted from "./messages/RoomEncrypted.vue";
|
import RoomEncrypted from "./messages/RoomEncrypted.vue";
|
||||||
import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
|
import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
|
||||||
import DebugEvent from "./messages/DebugEvent.vue";
|
import DebugEvent from "./messages/DebugEvent.vue";
|
||||||
|
import roomDisplayOptionsMixin from "./roomDisplayOptionsMixin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [ roomDisplayOptionsMixin ],
|
||||||
components: {
|
components: {
|
||||||
ChatHeader,
|
ChatHeader,
|
||||||
MessageIncomingText,
|
MessageIncomingText,
|
||||||
|
|
@ -66,6 +71,7 @@ export default {
|
||||||
MessageOutgoingAudio,
|
MessageOutgoingAudio,
|
||||||
MessageOutgoingVideo,
|
MessageOutgoingVideo,
|
||||||
MessageOutgoingSticker,
|
MessageOutgoingSticker,
|
||||||
|
MessageOutgoingThread,
|
||||||
MessageOutgoingPoll,
|
MessageOutgoingPoll,
|
||||||
ContactJoin,
|
ContactJoin,
|
||||||
ContactLeave,
|
ContactLeave,
|
||||||
|
|
@ -96,15 +102,6 @@ export default {
|
||||||
CreatePollDialog,
|
CreatePollDialog,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showOnlyUserStatusMessages() {
|
|
||||||
// We say that if you can redact events, you are allowed to create polls.
|
|
||||||
// NOTE!!! This assumes that there is a property named "room" on THIS.
|
|
||||||
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
|
|
||||||
let isModerator =
|
|
||||||
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
|
|
||||||
const show = this.$config.show_status_messages;
|
|
||||||
return show === "never" || (show === "moderators" && !isModerator)
|
|
||||||
},
|
|
||||||
showDayMarkerBeforeEvent(event) {
|
showDayMarkerBeforeEvent(event) {
|
||||||
const idx = this.events.indexOf(event);
|
const idx = this.events.indexOf(event);
|
||||||
if (idx <= 0) {
|
if (idx <= 0) {
|
||||||
|
|
@ -128,6 +125,13 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
componentForEvent(event, isForExport = false) {
|
componentForEvent(event, isForExport = false) {
|
||||||
|
if (!event.isRelation() && !event.isRedaction() && event.isRedacted()) {
|
||||||
|
const redaction = event.getRedactionEvent();
|
||||||
|
if (redaction && redaction.content && redaction.content.reason === "cancel") {
|
||||||
|
return null; // Show nothing, it was canceled!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.getType()) {
|
switch (event.getType()) {
|
||||||
case "m.room.member":
|
case "m.room.member":
|
||||||
if (event.getContent().membership == "join") {
|
if (event.getContent().membership == "join") {
|
||||||
|
|
@ -135,6 +139,9 @@ export default {
|
||||||
// We we already joined, so this must be a display name and/or avatar update!
|
// We we already joined, so this must be a display name and/or avatar update!
|
||||||
return ContactChanged;
|
return ContactChanged;
|
||||||
} else {
|
} else {
|
||||||
|
if (event.getSender() == this.$matrix.currentUserId && !this.showOwnJoins) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return ContactJoin;
|
return ContactJoin;
|
||||||
}
|
}
|
||||||
} else if (event.getContent().membership == "leave") {
|
} else if (event.getContent().membership == "leave") {
|
||||||
|
|
@ -143,7 +150,7 @@ export default {
|
||||||
return ContactKicked;
|
return ContactKicked;
|
||||||
}
|
}
|
||||||
return ContactLeave;
|
return ContactLeave;
|
||||||
} else if (!this.showOnlyUserStatusMessages()) {
|
} else if (this.showAllStatusMessages) {
|
||||||
if (event.getContent().membership == "invite") {
|
if (event.getContent().membership == "invite") {
|
||||||
return ContactInvited;
|
return ContactInvited;
|
||||||
} else if (event.getContent().membership == "ban") {
|
} else if (event.getContent().membership == "ban") {
|
||||||
|
|
@ -154,9 +161,9 @@ export default {
|
||||||
|
|
||||||
case "m.room.message":
|
case "m.room.message":
|
||||||
if (event.getSender() != this.$matrix.currentUserId) {
|
if (event.getSender() != this.$matrix.currentUserId) {
|
||||||
if (event.isThreadRoot) {
|
if (event.isMxThread) {
|
||||||
// Incoming thread, e.g. a file drop!
|
// Incoming thread, e.g. a file drop!
|
||||||
return MessageIncomingThread;
|
return isForExport ? MessageIncomingThreadExport : MessageIncomingThread;
|
||||||
}
|
}
|
||||||
if (event.getContent().msgtype == "m.image") {
|
if (event.getContent().msgtype == "m.image") {
|
||||||
// For SVG, make downloadable
|
// For SVG, make downloadable
|
||||||
|
|
@ -188,6 +195,10 @@ export default {
|
||||||
}
|
}
|
||||||
return MessageIncomingText;
|
return MessageIncomingText;
|
||||||
} else {
|
} else {
|
||||||
|
if (event.isMxThread) {
|
||||||
|
// Outgoing thread
|
||||||
|
return isForExport ? MessageOutgoingThreadExport : MessageOutgoingThread;
|
||||||
|
}
|
||||||
if (event.getContent().msgtype == "m.image") {
|
if (event.getContent().msgtype == "m.image") {
|
||||||
// For SVG, make downloadable
|
// For SVG, make downloadable
|
||||||
if (
|
if (
|
||||||
|
|
@ -220,61 +231,61 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "m.room.create":
|
case "m.room.create":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomCreated;
|
return RoomCreated;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.canonical_alias":
|
case "m.room.canonical_alias":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomAliased;
|
return RoomAliased;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.name":
|
case "m.room.name":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomNameChanged;
|
return RoomNameChanged;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.topic":
|
case "m.room.topic":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomTopicChanged;
|
return RoomTopicChanged;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.avatar":
|
case "m.room.avatar":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomAvatarChanged;
|
return RoomAvatarChanged;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.history_visibility":
|
case "m.room.history_visibility":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomHistoryVisibility;
|
return RoomHistoryVisibility;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.join_rules":
|
case "m.room.join_rules":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomJoinRules;
|
return RoomJoinRules;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.power_levels":
|
case "m.room.power_levels":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomPowerLevelsChanged;
|
return RoomPowerLevelsChanged;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.guest_access":
|
case "m.room.guest_access":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomGuestAccessChanged;
|
return RoomGuestAccessChanged;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m.room.encryption":
|
case "m.room.encryption":
|
||||||
if (!this.showOnlyUserStatusMessages()) {
|
if (this.showAllStatusMessages) {
|
||||||
return RoomEncrypted;
|
return RoomEncrypted;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -287,9 +298,9 @@ export default {
|
||||||
return MessageOutgoingPoll;
|
return MessageOutgoingPoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "im.keanu.room_deletion_notice": {
|
case STATE_EVENT_ROOM_DELETION_NOTICE: {
|
||||||
// Custom event for notice 30 seconds before a room is deleted/purged.
|
// Custom event for notice 30 seconds before a room is deleted/purged.
|
||||||
const deletionNotices = this.room.currentState.getStateEvents("im.keanu.room_deletion_notice");
|
const deletionNotices = this.room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETION_NOTICE);
|
||||||
if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) {
|
if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) {
|
||||||
// This is the latest/last one. Look at the status flag. Show nothing if it is "cancel".
|
// This is the latest/last one. Look at the status flag. Show nothing if it is "cancel".
|
||||||
if (event.getContent().status != "cancel") {
|
if (event.getContent().status != "cancel") {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
sendCurrentTextMessage();
|
sendCurrentTextMessage();
|
||||||
}
|
}
|
||||||
" />
|
" />
|
||||||
<v-btn @click="send" :disabled="!attachments || attachments.length == 0">{{ $t("menu.send") }}</v-btn>
|
<v-btn @click="sendAll" :disabled="!attachments || attachments.length == 0">{{ $t("menu.send") }}</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -54,13 +54,13 @@
|
||||||
v-if="attachments && attachments.length > 0 && (status == mainStatuses.SENDING || status == mainStatuses.SENT)">
|
v-if="attachments && attachments.length > 0 && (status == mainStatuses.SENDING || status == mainStatuses.SENT)">
|
||||||
<div class="attachment-wrapper">
|
<div class="attachment-wrapper">
|
||||||
<div class="file-drop-sent-stack" ref="stackContainer">
|
<div class="file-drop-sent-stack" ref="stackContainer">
|
||||||
<div v-if="status == mainStatuses.SENDING && countSent == 0" class="no-items">
|
<div v-if="status == mainStatuses.SENDING && attachmentsSentCount == 0" class="no-items">
|
||||||
<div class="file-drop-stack-item direct" :style="stackItemTransform(null, -1)"></div>
|
<div class="file-drop-stack-item direct" :style="stackItemTransform(null, -1)"></div>
|
||||||
<div>{{ $t('file_mode.sending_progress') }}</div>
|
<div>{{ $t('file_mode.sending_progress') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-for="(item, index) in sentItems" :key="item.id" class="file-drop-stack-item animated"
|
<div v-else v-for="(info, index) in attachmentsSent" :key="info.id" class="file-drop-stack-item animated"
|
||||||
:style="stackItemTransform(item, index)">
|
:style="stackItemTransform(info, index)">
|
||||||
<v-img v-if="item.attachment && item.attachment.image" :src="item.attachment.image" />
|
<v-img v-if="info.preview" :src="info.preview" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="status == mainStatuses.SENT" class="items-sent" :style="stackItemTransform(null, -1)">
|
<div v-if="status == mainStatuses.SENT" class="items-sent" :style="stackItemTransform(null, -1)">
|
||||||
<v-icon>$vuetify.icons.ic_check_circle</v-icon>
|
<v-icon>$vuetify.icons.ic_check_circle</v-icon>
|
||||||
|
|
@ -69,18 +69,18 @@
|
||||||
|
|
||||||
<!-- Middle section -->
|
<!-- Middle section -->
|
||||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-container">
|
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-container">
|
||||||
<div class="file-drop-sending-item" v-for="(info, index) in sendingItems" :key="index">
|
<div class="file-drop-sending-item" v-for="(info, index) in attachmentsSending" :key="index">
|
||||||
<v-img v-if="info.attachment && info.attachment.image" :src="info.attachment.image" />
|
<v-img v-if="info.preview" :src="info.preview" />
|
||||||
<div v-else class="filename">{{ info.attachment.name }}</div>
|
<div v-else class="filename">{{ info.attachment.name }}</div>
|
||||||
<v-progress-linear :value="info.progress"></v-progress-linear>
|
<v-progress-linear :value="info.progress"></v-progress-linear>
|
||||||
<div class="file-drop-cancel clickable" @click.stop="cancelSendingItem(info)">
|
<div class="file-drop-cancel clickable" @click.stop="cancelSendAttachmentItem(info)">
|
||||||
<v-icon size="14" color="white">close</v-icon>
|
<v-icon size="14" color="white">close</v-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container">
|
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container">
|
||||||
<div class="file-drop-files-sent">{{ $tc((this.messageInput && this.messageInput.length > 0) ?
|
<div class="file-drop-files-sent">{{ $tc((this.messageInput && this.messageInput.length > 0) ?
|
||||||
"file_mode.files_sent_with_note" : "file_mode.files_sent", sentItems.length) }}</div>
|
"file_mode.files_sent_with_note" : "file_mode.files_sent", attachmentsSent.length) }}</div>
|
||||||
<div class="file-drop-section">
|
<div class="file-drop-section">
|
||||||
<v-textarea disabled full-width solo flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
<v-textarea disabled full-width solo flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
||||||
rows="1" hide-details background-color="transparent" />
|
rows="1" hide-details background-color="transparent" />
|
||||||
|
|
@ -95,7 +95,8 @@
|
||||||
color="#4642F1"></v-progress-circular></v-btn>
|
color="#4642F1"></v-progress-circular></v-btn>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sent-input-container">
|
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sent-input-container">
|
||||||
<v-btn @click.stop="reset">{{ $t("file_mode.return_to_home") }}</v-btn>
|
<v-btn @click.stop="reset">{{ $t("file_mode.send_more_files") }}</v-btn>
|
||||||
|
<v-btn class="close" @click.stop="close">{{ $t("file_mode.close") }}</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -104,11 +105,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import messageMixin from "../messages/messageMixin";
|
import messageMixin from "../messages/messageMixin";
|
||||||
import util from "../../plugins/utils";
|
import sendAttachmentsMixin from "../sendAttachmentsMixin";
|
||||||
const prettyBytes = require("pretty-bytes");
|
const prettyBytes = require("pretty-bytes");
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [messageMixin],
|
mixins: [messageMixin, sendAttachmentsMixin],
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
attachments: {
|
attachments: {
|
||||||
|
|
@ -128,13 +129,6 @@ export default {
|
||||||
SENT: 2,
|
SENT: 2,
|
||||||
}),
|
}),
|
||||||
status: 0,
|
status: 0,
|
||||||
statuses: Object.freeze({
|
|
||||||
INITIAL: 0,
|
|
||||||
SENT: 1,
|
|
||||||
CANCELED: 2,
|
|
||||||
FAILED: 3,
|
|
||||||
}),
|
|
||||||
sendInfo: [],
|
|
||||||
dropTarget: false,
|
dropTarget: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -150,20 +144,6 @@ export default {
|
||||||
return this.currentItemIndex >= 0 && this.currentItemIndex < this.attachments.length &&
|
return this.currentItemIndex >= 0 && this.currentItemIndex < this.attachments.length &&
|
||||||
this.attachments[this.currentItemIndex].image
|
this.attachments[this.currentItemIndex].image
|
||||||
},
|
},
|
||||||
countSent() {
|
|
||||||
return this.sendInfo ? this.sendInfo.reduce((a, elem, ignoredidx, ignoredarray) => elem.status == this.statuses.SENT ? a + 1 : a, 0) : 0
|
|
||||||
},
|
|
||||||
sendingItems() {
|
|
||||||
return this.sendInfo ? this.sendInfo.filter(elem => elem.status == this.statuses.INITIAL) : []
|
|
||||||
},
|
|
||||||
sentItems() {
|
|
||||||
this.sortSendinfo();
|
|
||||||
return this.sendInfo ? this.sendInfo.filter(elem => elem.status == this.statuses.SENT) : []
|
|
||||||
},
|
|
||||||
sentItemsReversed() {
|
|
||||||
const array = this.sentItems;
|
|
||||||
return array.map((ignoreditem, idx) => array[array.length - 1 - idx])
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
attachments(newValue, oldValue) {
|
attachments(newValue, oldValue) {
|
||||||
|
|
@ -206,101 +186,23 @@ export default {
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.$emit('reset');
|
this.$emit('reset');
|
||||||
this.sendInfo = [];
|
this.sendingAttachments = [];
|
||||||
this.status = this.mainStatuses.SELECTING;
|
this.status = this.mainStatuses.SELECTING;
|
||||||
this.messageInput = "";
|
this.messageInput = "";
|
||||||
this.currentItemIndex = 0;
|
this.currentItemIndex = 0;
|
||||||
},
|
},
|
||||||
send() {
|
close() {
|
||||||
this.status = this.mainStatuses.SENDING;
|
this.$matrix.leaveRoomAndNavigate(this.room.roomId)
|
||||||
this.sendInfo = this.attachments.map((attachment) => {
|
|
||||||
return {
|
|
||||||
id: attachment.name,
|
|
||||||
status: this.statuses.INITIAL,
|
|
||||||
statusDate: Date.now,
|
|
||||||
attachment: attachment,
|
|
||||||
progress: 0,
|
|
||||||
randomRotation: 0,
|
|
||||||
randomTranslationX: 0,
|
|
||||||
randomTranslationY: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const text = (this.messageInput && this.messageInput.length > 0) ? this.messageInput : this.$t('file_mode.files');
|
|
||||||
util.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text)
|
|
||||||
.then((eventId) => {
|
|
||||||
// Use the eventId as a thread root for all the media
|
|
||||||
let promiseChain = Promise.resolve();
|
|
||||||
const getItemPromise = (index) => {
|
|
||||||
if (index < this.sendInfo.length) {
|
|
||||||
const item = this.sendInfo[index];
|
|
||||||
if (item.status !== this.statuses.INITIAL) {
|
|
||||||
return getItemPromise(++index);
|
|
||||||
}
|
|
||||||
const itemPromise = util.sendImage(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => {
|
|
||||||
if (loaded == total) {
|
|
||||||
item.progress = 100;
|
|
||||||
} else if (total > 0) {
|
|
||||||
item.progress = 100 * loaded / total;
|
|
||||||
}
|
|
||||||
}, eventId)
|
|
||||||
.then(() => {
|
|
||||||
// Look at last item rotation, flipping the sign on this, so looks more like a true stack
|
|
||||||
let signR = 1;
|
|
||||||
let signX = 1;
|
|
||||||
let signY = 1;
|
|
||||||
if (this.sentItems.length > 0) {
|
|
||||||
if (this.sentItems[0].randomRotation >= 0) {
|
|
||||||
signR = -1;
|
|
||||||
}
|
|
||||||
if (this.sentItems[0].randomTranslationX >= 0) {
|
|
||||||
signX = -1;
|
|
||||||
}
|
|
||||||
if (this.sentItems[0].randomTranslationY >= 0) {
|
|
||||||
signY = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item.randomRotation = signR * (2 + Math.random() * 10);
|
|
||||||
item.randomTranslationX = signX * Math.random() * 20;
|
|
||||||
item.randomTranslationY = signY * Math.random() * 20;
|
|
||||||
item.status = this.statuses.SENT;
|
|
||||||
item.statusDate = Date.now;
|
|
||||||
}).catch(ignorederr => {
|
|
||||||
if (item.promise.aborted) {
|
|
||||||
item.status = this.statuses.CANCELED;
|
|
||||||
} else {
|
|
||||||
console.error("ERROR", ignorederr);
|
|
||||||
item.status = this.statuses.FAILED;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
item.promise = itemPromise;
|
|
||||||
return itemPromise.then(() => getItemPromise(++index));
|
|
||||||
}
|
|
||||||
else return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
return promiseChain.then(() => getItemPromise(0));
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.status = this.mainStatuses.SENT;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("ERROR", err);
|
console.log("Error leaving", err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
cancelSendingItem(item) {
|
sendAll() {
|
||||||
if (item.promise && item.status == this.statuses.INITIAL) {
|
this.status = this.mainStatuses.SENDING;
|
||||||
item.promise.abort();
|
this.sendAttachments((this.messageInput && this.messageInput.length > 0) ? this.messageInput : this.$t('file_mode.files'), this.attachments)
|
||||||
}
|
.then(() => {
|
||||||
item.status = this.statuses.CANCELED;
|
this.status = this.mainStatuses.SENT;
|
||||||
},
|
});
|
||||||
checkDone() {
|
|
||||||
if (!this.sendInfo.some(a => a.status == this.statuses.INITIAL)) {
|
|
||||||
this.status = this.mainStatuses.SENT;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sortSendinfo() {
|
|
||||||
this.sendInfo.sort((a, b) => b.statusDate - a.statusDate);
|
|
||||||
},
|
},
|
||||||
stackItemTransform(item, index) {
|
stackItemTransform(item, index) {
|
||||||
const size = 0.6 * (this.$refs.stackContainer ? Math.min(this.$refs.stackContainer.clientWidth, this.$refs.stackContainer.clientHeight) : 176);
|
const size = 0.6 * (this.$refs.stackContainer ? Math.min(this.$refs.stackContainer.clientWidth, this.$refs.stackContainer.clientHeight) : 176);
|
||||||
|
|
|
||||||
143
src/components/file_mode/GalleryItemsView.vue
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div class="fill-screen file-drop-root">
|
||||||
|
|
||||||
|
<div class="chat-header">
|
||||||
|
<v-container fluid class="d-flex justify-space-between align-center">
|
||||||
|
<v-icon @click.stop="$emit('close')" color="white" class="clickable">arrow_back</v-icon>
|
||||||
|
<div class="room-name no-upper">{{ displayDate }}</div>
|
||||||
|
<v-icon @click.stop="showMoreMenu = true" color="white" class="clickable">more_vert</v-icon>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-drop-current-item">
|
||||||
|
<ThumbnailView :item="items[currentItemIndex]" />
|
||||||
|
<div class="download-button clickable" @click.stop="downloadOne">
|
||||||
|
<v-icon color="black">arrow_downward</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-drop-thumbnail-container">
|
||||||
|
<div :class="{ 'file-drop-thumbnail': true, 'clickable': true, 'current': id == currentItemIndex }"
|
||||||
|
@click="currentItemIndex = id" v-for="(currentImageInput, id) in items" :key="id">
|
||||||
|
<v-img v-if="currentImageInput && currentImageInput.src" :src="currentImageInput.src" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MORE MENU POPUP -->
|
||||||
|
<MoreMenuPopup :show="showMoreMenu" :menuItems="moreMenuItems" :showProfile="false" @close="showMoreMenu = false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MoreMenuPopup from "../MoreMenuPopup";
|
||||||
|
import messageMixin from "../messages/messageMixin";
|
||||||
|
import util from "../../plugins/utils";
|
||||||
|
import ThumbnailView from './ThumbnailView.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [messageMixin],
|
||||||
|
components: { MoreMenuPopup, ThumbnailView },
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: function () {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialItem: {
|
||||||
|
type: Object,
|
||||||
|
default: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentItemIndex: 0,
|
||||||
|
showMoreMenu: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.body.classList.add("dark");
|
||||||
|
if (this.initialItem) {
|
||||||
|
this.currentItemIndex = this.items.findIndex((v) => v === this.initialItem);
|
||||||
|
if (this.currentItemIndex < 0) {
|
||||||
|
this.currentItemIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
document.body.classList.remove("dark");
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayDate() {
|
||||||
|
return util.formatRecordStartTime(this.originalEvent.getTs())
|
||||||
|
},
|
||||||
|
moreMenuItems() {
|
||||||
|
let items = [];
|
||||||
|
items.push({
|
||||||
|
icon: '$vuetify.icons.ic_download', text: this.$t("message.download_all"), handler: () => {
|
||||||
|
this.downloadAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
items(newValue, oldValue) {
|
||||||
|
// Added or removed?
|
||||||
|
if (newValue && oldValue && newValue.length > oldValue.length) {
|
||||||
|
this.currentItemIndex = oldValue.length;
|
||||||
|
} else if (newValue) {
|
||||||
|
this.currentItemIndex = newValue.length - 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
downloadOne() {
|
||||||
|
if (this.currentItemIndex >= 0 && this.currentItemIndex < this.items.length) {
|
||||||
|
util.download(this.$matrix.matrixClient, this.items[this.currentItemIndex].event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
downloadAll() {
|
||||||
|
this.items.forEach(item => util.download(this.$matrix.matrixClient, item.event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
position: relative !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-drop-current-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 21px;
|
||||||
|
bottom: 21px;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
border-radius: 17px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-screen {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: black;
|
||||||
|
z-index: 20;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/components/file_mode/ThumbnailView.vue
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<template>
|
||||||
|
<v-responsive v-if="item.event.getContent().msgtype == 'm.video'" :class="{'thumbnail-item': true, 'preview': previewOnly}"
|
||||||
|
@click.stop="$emit('itemclick', {item: item})">
|
||||||
|
<video :src="item.src" :controls="!previewOnly" class="w-100 h-100">
|
||||||
|
{{ $t('fallbacks.video_file') }}
|
||||||
|
</video>
|
||||||
|
</v-responsive>
|
||||||
|
<v-img v-else-if="item.event.getContent().msgtype == 'm.image'" :aspect-ratio="previewOnly ? (16 / 9) : undefined" :class="{'thumbnail-item': true, 'preview': previewOnly}" :src="item.src" :contain="!previewOnly" :cover="previewOnly"
|
||||||
|
@click.stop="$emit('itemclick', {item: item})" />
|
||||||
|
<div v-else :class="{'thumbnail-item': true, 'preview': previewOnly, 'file-item': true}" @click.stop="$emit('itemclick', {item: item})">
|
||||||
|
<v-icon>description</v-icon>
|
||||||
|
{{ $sanitize(item.event.getContent().body) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Item is an object of { event: MXEvent, src: URL }
|
||||||
|
*/
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: function () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previewOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: function() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
|
||||||
|
.thumbnail-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
.v-icon {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -34,8 +34,12 @@ export default {
|
||||||
info: this.install(),
|
info: this.install(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.event.on("Event.localEventIdReplaced", this.onLocalEventIdReplaced);
|
||||||
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$audioPlayer.removeListener(this._uid);
|
this.$audioPlayer.removeListener(this._uid);
|
||||||
|
this.event.off("Event.localEventIdReplaced", this.onLocalEventIdReplaced);
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentTime() {
|
currentTime() {
|
||||||
|
|
@ -62,6 +66,12 @@ export default {
|
||||||
},
|
},
|
||||||
seeked(percent) {
|
seeked(percent) {
|
||||||
this.$audioPlayer.seek(this.event, percent);
|
this.$audioPlayer.seek(this.event, percent);
|
||||||
|
},
|
||||||
|
onLocalEventIdReplaced() {
|
||||||
|
// This happens when we are the sending party and the message has been sent and the local echo has been updated with the new real id.
|
||||||
|
// Since we use the event id to register with the audio player, we need to update.
|
||||||
|
this.$audioPlayer.removeListener(this._uid);
|
||||||
|
this.info = this.$audioPlayer.addListener(this._uid, this.event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<v-avatar class="avatar" ref="avatar" size="32" color="#ededed" @click.stop="otherAvatarClicked($refs.avatar.$el)">
|
<v-avatar class="avatar" ref="avatar" size="32" color="#ededed" @click.stop="otherAvatarClicked($refs.avatar.$el)">
|
||||||
<img v-if="messageEventAvatar(event)" :src="messageEventAvatar(event)" />
|
<img v-if="messageEventAvatar(event)" :src="messageEventAvatar(event)" onerror="this.style.display='none'" />
|
||||||
<span v-else class="white--text headline">{{
|
<span v-else class="white--text headline">{{
|
||||||
eventSenderDisplayName(event).substring(0, 1).toUpperCase()
|
eventSenderDisplayName(event).substring(0, 1).toUpperCase()
|
||||||
}}</span>
|
}}</span>
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<v-icon>more_vert</v-icon>
|
<v-icon>more_vert</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
|
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||||
<SeenBy :room="room" :event="event"/>
|
<SeenBy :room="room" :event="event"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@
|
||||||
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div class="original-message" v-if="inReplyToText">
|
<div class="original-message" v-if="inReplyToText">
|
||||||
<div class="original-message-sender">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<span>{{ $t('message.file_prefix') }}</span>
|
<span>{{ $t('message.file_prefix') }}</span>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@
|
||||||
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div class="original-message" v-if="inReplyToText">
|
<div class="original-message" v-if="inReplyToText">
|
||||||
<div class="original-message-sender">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<i v-if="event.isRedacted()" class="deleted-text">
|
<i v-if="event.isRedacted()" class="deleted-text">
|
||||||
<v-icon :color="this.senderIsAdminOrModerator(this.event)?'white':''" size="small">block</v-icon>
|
<v-icon :color="this.senderIsAdminOrModerator(this.event)?'white':''" size="small">block</v-icon>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
|
<div class="original-message" v-if="inReplyToText">
|
||||||
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
|
<div
|
||||||
|
class="original-message-text"
|
||||||
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<v-container fluid class="imageCollection">
|
<v-container fluid class="imageCollection">
|
||||||
<v-row wrap>
|
<v-row wrap>
|
||||||
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
||||||
<v-img :aspect-ratio="16 / 9" :src="item.src" cover />
|
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
@ -19,42 +27,70 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<GalleryItemsView :originalEvent="originalEvent" :items="items" :initialItem="showItem" v-if="!!showItem" v-on:close="showItem = null" />
|
||||||
</message-incoming>
|
</message-incoming>
|
||||||
|
<component v-else-if="items.length == 1" :is="componentFn(items[0].event)"
|
||||||
|
:originalEvent="items[0].event"
|
||||||
|
v-bind="{...$props, ...$attrs}" v-on="$listeners"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MessageIncoming from "./MessageIncoming.vue";
|
import MessageIncoming from "./MessageIncoming.vue";
|
||||||
import messageMixin from "./messageMixin";
|
import messageMixin from "./messageMixin";
|
||||||
import util from "../../plugins/utils";
|
import util from "../../plugins/utils";
|
||||||
|
import GalleryItemsView from '../file_mode/GalleryItemsView.vue';
|
||||||
|
import ThumbnailView from '../file_mode/ThumbnailView.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: MessageIncoming,
|
extends: MessageIncoming,
|
||||||
components: { MessageIncoming },
|
components: { MessageIncoming, GalleryItemsView, ThumbnailView },
|
||||||
mixins: [messageMixin],
|
mixins: [messageMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
items: []
|
items: [],
|
||||||
|
showItem: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).map(e => {
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
let ret = {
|
if (!this.thread) {
|
||||||
event: e,
|
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||||
src: null,
|
}
|
||||||
};
|
},
|
||||||
ret.promise =
|
beforeDestroy() {
|
||||||
util
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
|
|
||||||
.then((url) => {
|
|
||||||
ret.src = url;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Failed to fetch thumbnail: ", err);
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onRelationsCreated() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
onItemClick(event) {
|
||||||
|
this.showItem = event.item;
|
||||||
|
},
|
||||||
|
processThread() {
|
||||||
|
this.$emit('layout-change', () => {
|
||||||
|
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
||||||
|
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
|
||||||
|
.map(e => {
|
||||||
|
let ret = {
|
||||||
|
event: e,
|
||||||
|
src: null,
|
||||||
|
};
|
||||||
|
ret.promise =
|
||||||
|
util
|
||||||
|
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
|
||||||
|
.then((url) => {
|
||||||
|
ret.src = url;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Failed to fetch thumbnail: ", err);
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
}, this.$el);
|
||||||
|
},
|
||||||
layoutedItems() {
|
layoutedItems() {
|
||||||
if (!this.items || this.items.length == 0) { return [] }
|
if (!this.items || this.items.length == 0) { return [] }
|
||||||
let array = this.items.slice(0);
|
let array = this.items.slice(0);
|
||||||
|
|
@ -84,6 +120,9 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
|
},
|
||||||
|
downloadAll() {
|
||||||
|
this.items.forEach(item => util.download(this.$matrix.matrixClient, item.event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -94,7 +133,6 @@ export default {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -103,10 +141,12 @@ export default {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col {
|
.col {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<img v-if="userAvatar" :src="userAvatar" />
|
<img v-if="userAvatar" :src="userAvatar" />
|
||||||
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
|
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
|
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||||
<SeenBy :room="room" :event="event"/>
|
<SeenBy :room="room" :event="event"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,14 @@
|
||||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div class="original-message" v-if="inReplyToText">
|
<div class="original-message" v-if="inReplyToText">
|
||||||
<div class="original-message-sender">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<span>{{ $t('message.file_prefix') }}</span>
|
<span>{{ $t('message.file_prefix') }}</span>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div class="original-message" v-if="inReplyToText">
|
<div class="original-message" v-if="inReplyToText">
|
||||||
<div class="original-message-sender">
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="original-message-text"
|
class="original-message-text"
|
||||||
v-html="linkify($sanitize(inReplyToText))"
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
|
|
|
||||||
160
src/components/messages/MessageOutgoingThread.vue
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1">
|
||||||
|
<div class="bubble">
|
||||||
|
<div class="original-message" v-if="inReplyToText">
|
||||||
|
<div class="original-message-sender">{{ inReplyToSender }}</div>
|
||||||
|
<div
|
||||||
|
class="original-message-text"
|
||||||
|
v-html="linkify($sanitize(inReplyToText))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
<v-container fluid class="imageCollection">
|
||||||
|
<v-row wrap>
|
||||||
|
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
||||||
|
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
<i v-if="event.isRedacted()" class="deleted-text">
|
||||||
|
<v-icon size="small">block</v-icon>
|
||||||
|
{{ $t('message.outgoing_message_deleted_text') }}
|
||||||
|
</i>
|
||||||
|
<span v-html="linkify($sanitize(messageText))" v-else />
|
||||||
|
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
|
||||||
|
{{ $t('message.edited') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<GalleryItemsView :originalEvent="originalEvent" :items="items" :initialItem="showItem" v-if="!!showItem" v-on:close="showItem = null" />
|
||||||
|
</message-outgoing>
|
||||||
|
<component v-else-if="items.length == 1" :is="componentFn(items[0].event)"
|
||||||
|
:originalEvent="items[0].event"
|
||||||
|
v-bind="{...$props, ...$attrs}" v-on="$listeners"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||||
|
import messageMixin from "./messageMixin";
|
||||||
|
import util from "../../plugins/utils";
|
||||||
|
import GalleryItemsView from '../file_mode/GalleryItemsView.vue';
|
||||||
|
import ThumbnailView from '../file_mode/ThumbnailView.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing, GalleryItemsView, ThumbnailView },
|
||||||
|
mixins: [messageMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
showItem: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
if (!this.thread) {
|
||||||
|
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRelationsCreated() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
onItemClick(event) {
|
||||||
|
this.showItem = event.item;
|
||||||
|
},
|
||||||
|
processThread() {
|
||||||
|
this.$emit('layout-change', () => {
|
||||||
|
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
||||||
|
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
|
||||||
|
.map(e => {
|
||||||
|
let ret = {
|
||||||
|
event: e,
|
||||||
|
src: null,
|
||||||
|
};
|
||||||
|
ret.promise =
|
||||||
|
util
|
||||||
|
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
|
||||||
|
.then((url) => {
|
||||||
|
ret.src = url;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Failed to fetch thumbnail: ", err);
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
}, this.$el);
|
||||||
|
},
|
||||||
|
layoutedItems() {
|
||||||
|
if (!this.items || this.items.length == 0) { return [] }
|
||||||
|
let array = this.items.slice(0);
|
||||||
|
let rows = []
|
||||||
|
while (array.length > 0) {
|
||||||
|
if (array.length >= 7) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
rows.push({ size: 12, item: array[2] });
|
||||||
|
rows.push({ size: 3, item: array[3] });
|
||||||
|
rows.push({ size: 3, item: array[4] });
|
||||||
|
rows.push({ size: 3, item: array[5] });
|
||||||
|
rows.push({ size: 3, item: array[6] });
|
||||||
|
array = array.slice(7);
|
||||||
|
} else if (array.length >= 3) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
rows.push({ size: 12, item: array[2] });
|
||||||
|
array = array.slice(3);
|
||||||
|
} else if (array.length >= 2) {
|
||||||
|
rows.push({ size: 6, item: array[0] });
|
||||||
|
rows.push({ size: 6, item: array[1] });
|
||||||
|
array = array.slice(2);
|
||||||
|
} else {
|
||||||
|
rows.push({ size: 12, item: array[0] });
|
||||||
|
array = array.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bubble {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageCollection {
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming class="messageIn-thread" v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<component v-for="item in items" :is="componentFn(item.event)" :originalEvent="item.event" :key="item.event.getId()"
|
||||||
|
v-bind="{ ...$props }" v-on="$listeners" ref="exportedEvent" />
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageIncoming from "../MessageIncoming.vue";
|
||||||
|
import messageMixin from "./../messageMixin";
|
||||||
|
import util from "../../../plugins/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: MessageIncoming,
|
||||||
|
components: { MessageIncoming },
|
||||||
|
mixins: [messageMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
if (!this.thread) {
|
||||||
|
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRelationsCreated() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
processThread() {
|
||||||
|
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
||||||
|
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
|
||||||
|
.map(e => {
|
||||||
|
let ret = {
|
||||||
|
event: e,
|
||||||
|
src: null,
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bubble {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing class="messageOut-thread" v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<component v-for="item in items" :is="componentFn(item.event)" :originalEvent="item.event" :key="item.event.getId()"
|
||||||
|
v-bind="{ ...$props }" v-on="$listeners" ref="exportedEvent" />
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageOutgoing from "../MessageOutgoing.vue";
|
||||||
|
import messageMixin from "./../messageMixin";
|
||||||
|
import util from "../../../plugins/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
mixins: [messageMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
if (!this.thread) {
|
||||||
|
this.event.on("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRelationsCreated() {
|
||||||
|
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||||
|
this.event.off("Event.relationsCreated", this.onRelationsCreated);
|
||||||
|
},
|
||||||
|
processThread() {
|
||||||
|
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
|
||||||
|
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
|
||||||
|
.map(e => {
|
||||||
|
let ret = {
|
||||||
|
event: e,
|
||||||
|
src: null,
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bubble {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,6 +2,7 @@ import QuickReactions from "./QuickReactions.vue";
|
||||||
import * as linkify from 'linkifyjs';
|
import * as linkify from 'linkifyjs';
|
||||||
import linkifyHtml from 'linkify-html';
|
import linkifyHtml from 'linkify-html';
|
||||||
import utils from "../../plugins/utils"
|
import utils from "../../plugins/utils"
|
||||||
|
import util from "../../plugins/utils";
|
||||||
|
|
||||||
linkify.options.defaults.className = "link";
|
linkify.options.defaults.className = "link";
|
||||||
linkify.options.defaults.target = { url: "_blank" };
|
linkify.options.defaults.target = { url: "_blank" };
|
||||||
|
|
@ -35,28 +36,24 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
componentFn: {
|
||||||
|
type: Function,
|
||||||
|
default: function () {
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
event: {},
|
event: {},
|
||||||
inReplyToEvent: null,
|
thread: null,
|
||||||
inReplyToSender: null,
|
utils,
|
||||||
utils
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const relatesTo = this.validEvent && this.event.getWireContent()["m.relates_to"];
|
},
|
||||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
beforeDestroy() {
|
||||||
// Can we find the original message?
|
this.thread = null;
|
||||||
const originalEventId = relatesTo["m.in_reply_to"].event_id;
|
|
||||||
if (originalEventId && this.timelineSet) {
|
|
||||||
const originalEvent = this.timelineSet.findEventById(originalEventId);
|
|
||||||
if (originalEvent) {
|
|
||||||
this.inReplyToEvent = originalEvent;
|
|
||||||
this.inReplyToSender = this.eventSenderDisplayName(originalEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
originalEvent: {
|
originalEvent: {
|
||||||
|
|
@ -71,6 +68,18 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
thread: {
|
||||||
|
handler(newValue, oldValue) {
|
||||||
|
if (oldValue) {
|
||||||
|
oldValue.off("Relations.add", this.onAddRelation);
|
||||||
|
}
|
||||||
|
if (newValue) {
|
||||||
|
newValue.on("Relations.add", this.onAddRelation);
|
||||||
|
}
|
||||||
|
this.processThread();
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
/**
|
/**
|
||||||
|
|
@ -81,6 +90,16 @@ export default {
|
||||||
return this.event && Object.keys(this.event).length !== 0;
|
return this.event && Object.keys(this.event).length !== 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is a thread event, we return the root here, so all reactions will land on the root event.
|
||||||
|
*/
|
||||||
|
eventForReactions() {
|
||||||
|
if (this.event.parentThread) {
|
||||||
|
return this.event.parentThread;
|
||||||
|
}
|
||||||
|
return this.event;
|
||||||
|
},
|
||||||
|
|
||||||
incoming() {
|
incoming() {
|
||||||
return this.event && this.event.getSender() != this.$matrix.currentUserId;
|
return this.event && this.event.getSender() != this.$matrix.currentUserId;
|
||||||
},
|
},
|
||||||
|
|
@ -97,11 +116,34 @@ export default {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
inReplyToSender() {
|
||||||
|
const originalEvent = this.validEvent && this.event.replyEvent;
|
||||||
|
if (originalEvent) {
|
||||||
|
const sender = this.eventSenderDisplayName(originalEvent);
|
||||||
|
if (originalEvent.isThreadRoot || originalEvent.isMxThread) {
|
||||||
|
return sender || this.$t("message.someone");
|
||||||
|
} else {
|
||||||
|
return this.$t("message.user_said", { user: sender || this.$t("message.someone") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
inReplyToEvent() {
|
||||||
|
return this.validEvent && this.event.replyEvent;
|
||||||
|
},
|
||||||
|
|
||||||
inReplyToText() {
|
inReplyToText() {
|
||||||
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
||||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||||
|
if (this.inReplyToEvent && (this.inReplyToEvent.isThreadRoot || this.inReplyToEvent.isMxThread)) {
|
||||||
|
const children = this.timelineSet.relations
|
||||||
|
.getAllChildEventsForEvent(this.inReplyToEvent.getId())
|
||||||
|
.filter((e) => util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
return this.$t("message.sent_media", { count: children.length });
|
||||||
|
}
|
||||||
const content = this.event.getContent();
|
const content = this.event.getContent();
|
||||||
if ('body' in content) {
|
if ("body" in content) {
|
||||||
const lines = content.body.split("\n").reverse() || [];
|
const lines = content.body.split("\n").reverse() || [];
|
||||||
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
|
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
|
||||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||||
|
|
@ -111,12 +153,10 @@ export default {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inReplyToEvent) {
|
if (this.inReplyToEvent) {
|
||||||
var c = this.inReplyToEvent.getContent();
|
var c = this.inReplyToEvent.getContent();
|
||||||
return c.body;
|
return c.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have the original text (at the moment at least)
|
// We don't have the original text (at the moment at least)
|
||||||
return this.$t("fallbacks.original_text");
|
return this.$t("fallbacks.original_text");
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +167,7 @@ export default {
|
||||||
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
||||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||||
const content = this.event.getContent();
|
const content = this.event.getContent();
|
||||||
if ('body' in content) {
|
if ("body" in content) {
|
||||||
// Remove the new text and strip "> " from the old original text
|
// Remove the new text and strip "> " from the old original text
|
||||||
const lines = content.body.split("\n");
|
const lines = content.body.split("\n");
|
||||||
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
||||||
|
|
@ -164,6 +204,10 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onAddRelation() {
|
||||||
|
console.error("onAddRelation");
|
||||||
|
this.processThread();
|
||||||
|
},
|
||||||
ownAvatarClicked() {
|
ownAvatarClicked() {
|
||||||
this.$emit("own-avatar-clicked", { event: this.event });
|
this.$emit("own-avatar-clicked", { event: this.event });
|
||||||
},
|
},
|
||||||
|
|
@ -277,5 +321,10 @@ export default {
|
||||||
linkify(text) {
|
linkify(text) {
|
||||||
return linkifyHtml(text);
|
return linkifyHtml(text);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this to handle updates to (the) message thread.
|
||||||
|
*/
|
||||||
|
processThread() {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import util from "../../plugins/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -5,8 +6,12 @@ export default {
|
||||||
return !this.incoming && this.event.getContent().msgtype == "m.text";
|
return !this.incoming && this.event.getContent().msgtype == "m.text";
|
||||||
},
|
},
|
||||||
isDownloadable() {
|
isDownloadable() {
|
||||||
|
if ((this.event.isThreadRoot || this.event.isMxThread) && this.timelineSet) {
|
||||||
|
const children = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
return children.length > 0;
|
||||||
|
}
|
||||||
const msgtype = this.event.getContent().msgtype;
|
const msgtype = this.event.getContent().msgtype;
|
||||||
return ['m.video','m.audio','m.image','m.file'].includes(msgtype);
|
return util.downloadableTypes().includes(msgtype);
|
||||||
},
|
},
|
||||||
isRedactable() {
|
isRedactable() {
|
||||||
const room = this.$matrix.matrixClient.getRoom(this.event.getRoomId());
|
const room = this.$matrix.matrixClient.getRoom(this.event.getRoomId());
|
||||||
|
|
|
||||||
18
src/components/roomDisplayOptionsMixin.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
showOwnJoins() {
|
||||||
|
return !this.$matrix.isDirectRoom(this.room);
|
||||||
|
},
|
||||||
|
|
||||||
|
showAllStatusMessages() {
|
||||||
|
// We say that if you can redact events, you are ad admin.
|
||||||
|
// NOTE!!! This assumes that there is a property named "room" on THIS.
|
||||||
|
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
|
||||||
|
let isModerator =
|
||||||
|
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
|
||||||
|
const show = this.$config.show_status_messages;
|
||||||
|
return show !== "never" && (show !== "moderators" || isModerator)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import utils from "../plugins/utils";
|
import utils from "../plugins/utils";
|
||||||
|
import roomTypeMixin from "./roomTypeMixin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [roomTypeMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
roomJoinRule: null,
|
roomJoinRule: null,
|
||||||
|
|
@ -59,7 +61,7 @@ export default {
|
||||||
publicRoomLink() {
|
publicRoomLink() {
|
||||||
if (this.room && this.roomJoinRule == "public") {
|
if (this.room && this.roomJoinRule == "public") {
|
||||||
return this.$router.getRoomLink(
|
return this.$router.getRoomLink(
|
||||||
this.room.getCanonicalAlias(), this.room.roomId, this.room.name, utils.roomDisplayTypeToQueryParam(this.room)
|
this.room.getCanonicalAlias(), this.room.roomId, this.room.name, utils.roomDisplayTypeToQueryParam(this.room, this.roomDisplayType)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -80,6 +82,23 @@ export default {
|
||||||
return isAdmin;
|
return isAdmin;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isPrivate() {
|
||||||
|
return this.$matrix.isDirectRoom(this.room);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is a direct chat with someone, return that member here.
|
||||||
|
* @returns MXMember of the one we are chatting with, or 'undefined'.
|
||||||
|
*/
|
||||||
|
privateParty() {
|
||||||
|
if (this.isPrivate) {
|
||||||
|
const membersButMe = this.room.getMembers().filter(m => m.userId != this.$matrix.currentUserId);
|
||||||
|
if (membersButMe.length == 1) {
|
||||||
|
return membersButMe[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
room: {
|
room: {
|
||||||
|
|
@ -163,5 +182,19 @@ export default {
|
||||||
this.updatePermissions();
|
this.updatePermissions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
privatePartyAvatar(size) {
|
||||||
|
const other = this.privateParty;
|
||||||
|
if (other) {
|
||||||
|
return other.getAvatarUrl(
|
||||||
|
this.$matrix.matrixClient.getHomeserverUrl(),
|
||||||
|
size,
|
||||||
|
size,
|
||||||
|
"scale",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
69
src/components/roomMembersMixin.js
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
joinedAndInvitedMembers: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$matrix.on("Room.timeline", this.roomMembersMixinOnEvent);
|
||||||
|
this.updateMembers();
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
this.$matrix.off("Room.timeline", this.roomMembersMixinOnEvent);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
joinedMembers() {
|
||||||
|
return this.joinedAndInvitedMembers.filter(m => m.membership === "join");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
room: {
|
||||||
|
handler() {
|
||||||
|
this.updateMembers();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
roomMembersMixinOnEvent(event) {
|
||||||
|
if (this.room && this.room.roomId == event.getRoomId()) {
|
||||||
|
// For this room
|
||||||
|
if (event.getType() == "m.room.member") {
|
||||||
|
this.updateMembers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sortMemberFunction(a, b) {
|
||||||
|
const myUserId = this.$matrix.currentUserId;
|
||||||
|
|
||||||
|
// Place ourselves at the top!
|
||||||
|
if (a.userId == myUserId) {
|
||||||
|
return -1;
|
||||||
|
} else if (b.userId == myUserId) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then sort by power level
|
||||||
|
if (a.powerLevel > b.powerLevel) {
|
||||||
|
return -1;
|
||||||
|
} else if (b.powerLevel > a.powerLevel) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then by name
|
||||||
|
const aName = a.user ? a.user.displayName : a.name;
|
||||||
|
const bName = b.user ? b.user.displayName : b.name;
|
||||||
|
return aName.localeCompare(bName);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMembers() {
|
||||||
|
if (this.room) {
|
||||||
|
this.joinedAndInvitedMembers = this.room.getMembers().sort(this.sortMemberFunction);
|
||||||
|
} else {
|
||||||
|
this.joinedAndInvitedMembers = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,47 @@
|
||||||
import { ROOM_TYPE_VOICE_MODE, ROOM_TYPE_FILE_MODE, ROOM_TYPE_DEFAULT } from "../plugins/utils";
|
import { ROOM_TYPE_VOICE_MODE, ROOM_TYPE_FILE_MODE, ROOM_TYPE_DEFAULT, STATE_EVENT_ROOM_TYPE } from "../plugins/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
roomDisplayType: ROOM_TYPE_DEFAULT,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$matrix.on("Room.timeline", this.onRoomTypeMixinEvent);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.$matrix.off("Room.timeline", this.onRoomTypeMixinEvent);
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
room: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.onRoomTypeMixinTypeEvent(newVal.currentState.getStateEvents(STATE_EVENT_ROOM_TYPE, "") || newVal.currentState.getStateEvents("m.room.create", ""));
|
||||||
|
} else {
|
||||||
|
this.roomDisplayType = ROOM_TYPE_DEFAULT;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRoomTypeMixinEvent(e) {
|
||||||
|
if (this.room && this.room.roomId == e.getRoomId() && e && e.getType() == STATE_EVENT_ROOM_TYPE) {
|
||||||
|
this.onRoomTypeMixinTypeEvent(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRoomTypeMixinTypeEvent(e) {
|
||||||
|
if (e) {
|
||||||
|
const roomType = e.getContent().type;
|
||||||
|
// Validate value, or return default
|
||||||
|
if ([ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE].includes(roomType)) {
|
||||||
|
this.roomDisplayType = roomType;
|
||||||
|
} else {
|
||||||
|
this.roomDisplayType = ROOM_TYPE_DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
availableRoomTypes() {
|
availableRoomTypes() {
|
||||||
let types = [{ title: this.$t("room_info.room_type_default"), description: "", value: ROOM_TYPE_DEFAULT }];
|
let types = [{ title: this.$t("room_info.room_type_default"), description: "", value: ROOM_TYPE_DEFAULT }];
|
||||||
|
|
|
||||||
159
src/components/sendAttachmentsMixin.js
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import util from "../plugins/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
sendStatuses: Object.freeze({
|
||||||
|
INITIAL: 0,
|
||||||
|
SENDING: 1,
|
||||||
|
SENT: 2,
|
||||||
|
CANCELED: 3,
|
||||||
|
FAILED: 4,
|
||||||
|
}),
|
||||||
|
sendingStatus: 0,
|
||||||
|
sendingPromise: null,
|
||||||
|
sendingRootEventId: null,
|
||||||
|
sendingAttachments: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
attachmentsSentCount() {
|
||||||
|
return this.sendingAttachments ? this.sendingAttachments.reduce((a, elem, ignoredidx, ignoredarray) => elem.status == this.sendStatuses.SENT ? a + 1 : a, 0) : 0
|
||||||
|
},
|
||||||
|
attachmentsSending() {
|
||||||
|
return this.sendingAttachments ? this.sendingAttachments.filter(elem => elem.status == this.sendStatuses.INITIAL || elem.status == this.sendStatuses.SENDING) : []
|
||||||
|
},
|
||||||
|
attachmentsSent() {
|
||||||
|
this.sortSendingAttachments();
|
||||||
|
return this.sendingAttachments ? this.sendingAttachments.filter(elem => elem.status == this.sendStatuses.SENT) : []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendAttachments(text, attachments) {
|
||||||
|
this.sendingStatus = this.sendStatuses.SENDING;
|
||||||
|
|
||||||
|
this.sendingAttachments = attachments.map((attachment) => {
|
||||||
|
let file = (() => {
|
||||||
|
// other than file type image
|
||||||
|
if(attachment instanceof File) {
|
||||||
|
return attachment;
|
||||||
|
} else {
|
||||||
|
if (attachment.scaled && attachment.useScaled) {
|
||||||
|
// Send scaled version of image instead!
|
||||||
|
return attachment.scaled;
|
||||||
|
} else {
|
||||||
|
// Send actual file image when not scaled!
|
||||||
|
return attachment.actualFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
let sendInfo = {
|
||||||
|
id: attachment.name,
|
||||||
|
status: this.sendStatuses.INITIAL,
|
||||||
|
statusDate: Date.now,
|
||||||
|
attachment: file,
|
||||||
|
preview: attachment.image,
|
||||||
|
progress: 0,
|
||||||
|
randomRotation: 0,
|
||||||
|
randomTranslationX: 0,
|
||||||
|
randomTranslationY: 0
|
||||||
|
};
|
||||||
|
attachment.sendInfo = sendInfo;
|
||||||
|
return sendInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sendingPromise = util.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text)
|
||||||
|
.then((eventId) => {
|
||||||
|
this.sendingRootEventId = eventId;
|
||||||
|
|
||||||
|
// Use the eventId as a thread root for all the media
|
||||||
|
let promiseChain = Promise.resolve();
|
||||||
|
const getItemPromise = (index) => {
|
||||||
|
if (index < this.sendingAttachments.length) {
|
||||||
|
const item = this.sendingAttachments[index];
|
||||||
|
if (item.status !== this.sendStatuses.INITIAL) {
|
||||||
|
return getItemPromise(++index);
|
||||||
|
}
|
||||||
|
const itemPromise = util.sendImage(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => {
|
||||||
|
if (loaded == total) {
|
||||||
|
item.progress = 100;
|
||||||
|
} else if (total > 0) {
|
||||||
|
item.progress = 100 * loaded / total;
|
||||||
|
}
|
||||||
|
}, eventId)
|
||||||
|
.then(() => {
|
||||||
|
// Look at last item rotation, flipping the sign on this, so looks more like a true stack
|
||||||
|
let signR = 1;
|
||||||
|
let signX = 1;
|
||||||
|
let signY = 1;
|
||||||
|
if (this.attachmentsSent.length > 0) {
|
||||||
|
if (this.attachmentsSent[0].randomRotation >= 0) {
|
||||||
|
signR = -1;
|
||||||
|
}
|
||||||
|
if (this.attachmentsSent[0].randomTranslationX >= 0) {
|
||||||
|
signX = -1;
|
||||||
|
}
|
||||||
|
if (this.attachmentsSent[0].randomTranslationY >= 0) {
|
||||||
|
signY = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.randomRotation = signR * (2 + Math.random() * 10);
|
||||||
|
item.randomTranslationX = signX * Math.random() * 20;
|
||||||
|
item.randomTranslationY = signY * Math.random() * 20;
|
||||||
|
item.status = this.sendStatuses.SENT;
|
||||||
|
item.statusDate = Date.now;
|
||||||
|
}).catch(ignorederr => {
|
||||||
|
if (item.promise.aborted) {
|
||||||
|
item.status = this.sendStatuses.CANCELED;
|
||||||
|
} else {
|
||||||
|
console.error("ERROR", ignorederr);
|
||||||
|
item.status = this.sendStatuses.FAILED;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
item.promise = itemPromise;
|
||||||
|
return itemPromise.then(() => getItemPromise(++index));
|
||||||
|
}
|
||||||
|
else return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
return promiseChain.then(() => getItemPromise(0));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.sendingStatus = this.sendStatuses.SENT;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("ERROR", err);
|
||||||
|
});
|
||||||
|
return this.sendingPromise;
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelSendAttachments() {
|
||||||
|
this.sendingAttachments.toReversed().forEach(item => {
|
||||||
|
this.cancelSendAttachmentItem(item);
|
||||||
|
});
|
||||||
|
this.sendingStatus = this.sendStatuses.CANCELED;
|
||||||
|
if (this.sendingRootEventId && this.room) {
|
||||||
|
// Redact the root event.
|
||||||
|
this.$matrix.matrixClient
|
||||||
|
.redactEvent(this.room.roomId, this.sendingRootEventId, undefined, { reason: "cancel" })
|
||||||
|
.then(() => {
|
||||||
|
console.log("Message redacted");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Redaction failed: ", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelSendAttachmentItem(item) {
|
||||||
|
if (item.promise && item.status != this.sendStatuses.INITIAL) {
|
||||||
|
item.promise.abort();
|
||||||
|
}
|
||||||
|
item.status = this.sendStatuses.CANCELED;
|
||||||
|
},
|
||||||
|
|
||||||
|
sortSendingAttachments() {
|
||||||
|
this.sendingAttachments.sort((a, b) => b.statusDate - a.statusDate);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -101,7 +101,7 @@ Vue.directive('longTap', {
|
||||||
*/
|
*/
|
||||||
const touchTimerElapsed = function () {
|
const touchTimerElapsed = function () {
|
||||||
el.longTapHandled = true;
|
el.longTapHandled = true;
|
||||||
el.longTapCallbacks[1] && el.longTapCallbacks[1].call();
|
el.longTapCallbacks[1] && el.longTapCallbacks[1].call(el, el);
|
||||||
el.longTapTimer = null;
|
el.longTapTimer = null;
|
||||||
el.classList.remove("waiting-for-long-tap");
|
el.classList.remove("waiting-for-long-tap");
|
||||||
};
|
};
|
||||||
|
|
@ -127,7 +127,7 @@ Vue.directive('longTap', {
|
||||||
el.longTapTimer = null;
|
el.longTapTimer = null;
|
||||||
if (!el.longTapHandled) {
|
if (!el.longTapHandled) {
|
||||||
// Not canceled or long tapped. Just a single tap. Do we have a handler?
|
// Not canceled or long tapped. Just a single tap. Do we have a handler?
|
||||||
el.longTapCallbacks[0] && el.longTapCallbacks[0].call();
|
el.longTapCallbacks[0] && el.longTapCallbacks[0].call(el, el);
|
||||||
}
|
}
|
||||||
el.classList.remove("waiting-for-long-tap");
|
el.classList.remove("waiting-for-long-tap");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,12 @@ export default class User {
|
||||||
}
|
}
|
||||||
return user_id;
|
return user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the domain out of the home_server, so if that one is e.g.
|
||||||
|
// "https://yourdomain.com:8008" then we return "yourdomain.com"
|
||||||
|
static serverDomain(home_server) {
|
||||||
|
const parts = home_server.split("://");
|
||||||
|
const serverAndPort = parts[parts.length - 1].split(/:|\//);
|
||||||
|
return serverAndPort[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
23
src/plugins/notificationAndServiceWorker.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
export function registerServiceWorker() {
|
||||||
|
if("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.register("/sw.js");
|
||||||
|
} else {
|
||||||
|
console.log("No Service Worker support!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestNotificationPermission() {
|
||||||
|
if("PushManager" in window) {
|
||||||
|
window.Notification.requestPermission();
|
||||||
|
} else {
|
||||||
|
console.log("No Push API Support!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function windowNotificationPermission() {
|
||||||
|
return window.Notification.permission
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notificationCount() {
|
||||||
|
return this.$matrix.notificationCount
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ class Stickers {
|
||||||
}
|
}
|
||||||
|
|
||||||
isStickerShortcode(messageBody) {
|
isStickerShortcode(messageBody) {
|
||||||
if (messageBody && messageBody.startsWith(":") && messageBody.startsWith(":") && messageBody.length >= 5) {
|
if (messageBody && typeof messageBody === "string" && messageBody.startsWith(":") && messageBody.startsWith(":") && messageBody.length >= 5) {
|
||||||
const image = this.getStickerImage(messageBody);
|
const image = this.getStickerImage(messageBody);
|
||||||
return image != undefined && image != null;
|
return image != undefined && image != null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,15 @@ import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
|
||||||
import dataUriToBuffer from "data-uri-to-buffer";
|
import dataUriToBuffer from "data-uri-to-buffer";
|
||||||
import ImageResize from "image-resize";
|
import ImageResize from "image-resize";
|
||||||
|
|
||||||
|
export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
|
||||||
|
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";
|
||||||
|
|
||||||
export const ROOM_TYPE_DEFAULT = "im.keanu.room_type_default";
|
export const ROOM_TYPE_DEFAULT = "im.keanu.room_type_default";
|
||||||
export const ROOM_TYPE_VOICE_MODE = "im.keanu.room_type_voice";
|
export const ROOM_TYPE_VOICE_MODE = "im.keanu.room_type_voice";
|
||||||
export const ROOM_TYPE_FILE_MODE = "im.keanu.room_type_file";
|
export const ROOM_TYPE_FILE_MODE = "im.keanu.room_type_file";
|
||||||
|
|
||||||
|
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
|
||||||
|
|
||||||
const sizeOf = require("image-size");
|
const sizeOf = require("image-size");
|
||||||
|
|
||||||
var dayjs = require('dayjs');
|
var dayjs = require('dayjs');
|
||||||
|
|
@ -482,19 +487,19 @@ class Util {
|
||||||
/**
|
/**
|
||||||
* Return what "mode" to use for the given room.
|
* Return what "mode" to use for the given room.
|
||||||
*
|
*
|
||||||
* The default value is given by the room itself. If the "type" of the
|
* The default value is given by the room itself (as state events, see roomTypeMixin).
|
||||||
* room is set to 'im.keanu.room_type_voice' then we default to voice mode,
|
* This method just returns if the user has overridden this in room settings (this
|
||||||
* else if set to 'im.keanu.room_type_file' we default to file mode.
|
* fact will be persisted as a user specific tag on the room). Note: currently override
|
||||||
* The user can then override this default by changing the "room type"
|
* is disabled in the UI...
|
||||||
* in room settings (it will be persisted as a user specific tag on the room)
|
|
||||||
*/
|
*/
|
||||||
roomDisplayType(roomOrNull) {
|
roomDisplayTypeOverride(roomOrNull) {
|
||||||
if (roomOrNull) {
|
if (roomOrNull) {
|
||||||
const room = roomOrNull;
|
const room = roomOrNull;
|
||||||
|
|
||||||
// Have we changed our local view mode of this room?
|
// Have we changed our local view mode of this room?
|
||||||
const tags = room.tags;
|
const tags = room.tags;
|
||||||
if (tags && tags["ui_options"]) {
|
if (tags && tags["ui_options"]) {
|
||||||
|
console.error("We have a tag!");
|
||||||
if (tags["ui_options"]["voice_mode"] === 1) {
|
if (tags["ui_options"]["voice_mode"] === 1) {
|
||||||
return ROOM_TYPE_VOICE_MODE;
|
return ROOM_TYPE_VOICE_MODE;
|
||||||
} else if (tags["ui_options"]["file_mode"] === 1) {
|
} else if (tags["ui_options"]["file_mode"] === 1) {
|
||||||
|
|
@ -504,30 +509,16 @@ class Util {
|
||||||
return ROOM_TYPE_DEFAULT;
|
return ROOM_TYPE_DEFAULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Was the room created with a voice mode type?
|
|
||||||
const createEvent = room.currentState.getStateEvents(
|
|
||||||
"m.room.create",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
if (createEvent) {
|
|
||||||
const roomType = createEvent.getContent().type;
|
|
||||||
|
|
||||||
// Validate value, or return default
|
|
||||||
if ([ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE].includes(roomType)) {
|
|
||||||
return roomType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ROOM_TYPE_DEFAULT;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the room type for the current room
|
* Return the room type for the current room
|
||||||
* @param {*} roomOrNull
|
* @param {*} roomOrNull
|
||||||
*/
|
*/
|
||||||
roomDisplayTypeToQueryParam(roomOrNull) {
|
roomDisplayTypeToQueryParam(roomOrNull, roomDisplayType) {
|
||||||
const roomType = this.roomDisplayType(roomOrNull);
|
const roomType = this.roomDisplayTypeOverride(roomOrNull) || roomDisplayType;
|
||||||
if (roomType === ROOM_TYPE_FILE_MODE) {
|
if (roomType === ROOM_TYPE_FILE_MODE) {
|
||||||
// Send "file" here, so the receiver of the invite link knows to display the "file drop" join page
|
// Send "file" here, so the receiver of the invite link knows to display the "file drop" join page
|
||||||
// instead of the standard one.
|
// instead of the standard one.
|
||||||
|
|
@ -583,12 +574,16 @@ class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitizeUserId(userId) {
|
sanitizeUserId(userId) {
|
||||||
if (userId && userId.match(/^@.+$/)) {
|
if (userId && userId.match(/^([0-9a-z-.=_/]+|@[0-9a-z-.=_/]+:.+)$/)) {
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
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) {
|
||||||
|
|
@ -901,6 +896,31 @@ class Util {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadableTypes() {
|
||||||
|
return ['m.video','m.audio','m.image','m.file'];
|
||||||
|
}
|
||||||
|
|
||||||
|
download(matrixClient, event) {
|
||||||
|
this
|
||||||
|
.getAttachment(matrixClient, event)
|
||||||
|
.then((url) => {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.target = "_blank";
|
||||||
|
link.download = event.getContent().body || this.$t("fallbacks.download_name");
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 200);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Failed to fetch attachment: ", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default new Util();
|
export default new Util();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ import Join from '../components/Join.vue'
|
||||||
import Login from '../components/Login.vue'
|
import Login from '../components/Login.vue'
|
||||||
import Profile from '../components/Profile.vue'
|
import Profile from '../components/Profile.vue'
|
||||||
import CreateRoom from '../components/CreateRoom.vue'
|
import CreateRoom from '../components/CreateRoom.vue'
|
||||||
|
import GetLink from '../components/GetLink.vue'
|
||||||
|
import User from '../models/user'
|
||||||
import util from '../plugins/utils'
|
import util from '../plugins/utils'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
@ -54,6 +55,11 @@ const routes = [
|
||||||
title: 'Create room'
|
title: 'Create room'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/getlink',
|
||||||
|
name: 'GetLink',
|
||||||
|
component: GetLink,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
|
|
@ -91,7 +97,7 @@ const router = new VueRouter({
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const publicPages = ['/login', '/createroom'];
|
const publicPages = ['/login', '/createroom', '/getlink'];
|
||||||
var authRequired = !publicPages.includes(to.path);
|
var authRequired = !publicPages.includes(to.path);
|
||||||
const loggedIn = router.app.$store.state.auth.user;
|
const loggedIn = router.app.$store.state.auth.user;
|
||||||
|
|
||||||
|
|
@ -120,15 +126,33 @@ router.beforeEach((to, from, next) => {
|
||||||
}
|
}
|
||||||
} else if (to.name == 'User') {
|
} else if (to.name == 'User') {
|
||||||
if (to.params.userId) {
|
if (to.params.userId) {
|
||||||
const roomId = util.sanitizeUserId(to.params.userId);
|
let roomId = util.sanitizeUserId(to.params.userId);
|
||||||
router.app.$matrix.setCurrentRoomId(roomId);
|
if (roomId && !roomId.startsWith("@")) {
|
||||||
authRequired = false;
|
// Not a full username. Assume local name on this server.
|
||||||
|
return router.app.$config.promise.then((config) => {
|
||||||
|
const user = new User(config.defaultServer, roomId, "");
|
||||||
|
user.normalize();
|
||||||
|
roomId = "@" + roomId + ":" + User.serverDomain(user.home_server);
|
||||||
|
router.app.$matrix.setCurrentRoomId(roomId);
|
||||||
|
}).catch(err => console.error(err)).finally(() => next());
|
||||||
|
} else {
|
||||||
|
router.app.$matrix.setCurrentRoomId(roomId);
|
||||||
|
authRequired = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (to.name == 'Invite') {
|
} else if (to.name == 'Invite') {
|
||||||
if (to.params.roomId) {
|
if (to.params.roomId) {
|
||||||
const roomId = util.sanitizeRoomId(to.params.roomId);
|
const roomId = util.sanitizeRoomId(to.params.roomId);
|
||||||
router.app.$matrix.setCurrentRoomId(roomId);
|
router.app.$matrix.setCurrentRoomId(roomId);
|
||||||
}
|
}
|
||||||
|
} else if (to.name == 'CreateRoom') {
|
||||||
|
return router.app.$config.promise.then((config) => {
|
||||||
|
if (config.hide_add_room_on_home) {
|
||||||
|
next('/');
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}).catch(err => { console.error(err); next('/'); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// trying to access a restricted page + not logged in
|
// trying to access a restricted page + not logged in
|
||||||
|
|
@ -160,4 +184,13 @@ router.getRoomLink = function (alias, roomId, roomName, mode) {
|
||||||
return window.location.origin + window.location.pathname + "#/room/" + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
|
return window.location.origin + window.location.pathname + "#/room/" + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.getDMLink = function (user, config) {
|
||||||
|
let userId = user.user_id;
|
||||||
|
if (user.home_server === config.defaultServer) {
|
||||||
|
// Using default server, don't include it in the link
|
||||||
|
userId = User.localPart(user.user_id);
|
||||||
|
}
|
||||||
|
return `${window.location.origin + window.location.pathname}#/user/${encodeURIComponent(userId)}`
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
export default {
|
export default {
|
||||||
install(Vue, defaultServerFromLocation, onloaded) {
|
install(Vue, defaultServerFromLocation, onloaded) {
|
||||||
var config = Vue.observable(require('@/assets/config.json'));
|
var config = Vue.observable(require('@/assets/config.json'));
|
||||||
|
Vue.set(config, "loaded", false);
|
||||||
const getRuntimeConfig = () => {
|
const getRuntimeConfig = () => {
|
||||||
return fetch('./config.json').then((res) => res.json()).catch(err => {
|
return fetch('./config.json?ms=' + Date.now()).then((res) => res.json()).catch(err => {
|
||||||
console.error("Failed to get config:", err);
|
console.error("Failed to get config:", err);
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
@ -17,6 +18,7 @@ export default {
|
||||||
if (!json.defaultServer) {
|
if (!json.defaultServer) {
|
||||||
Vue.set(config, "defaultServer", defaultServerFromLocation);
|
Vue.set(config, "defaultServer", defaultServerFromLocation);
|
||||||
}
|
}
|
||||||
|
Vue.set(config, "loaded", true);
|
||||||
|
|
||||||
// Tell callback we are done loading runtime config
|
// Tell callback we are done loading runtime config
|
||||||
if (onloaded) {
|
if (onloaded) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import olm from "@matrix-org/olm/olm";
|
||||||
global.Olm = olm;
|
global.Olm = olm;
|
||||||
import * as sdk from "matrix-js-sdk";
|
import * as sdk from "matrix-js-sdk";
|
||||||
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
|
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
|
||||||
import util from "../plugins/utils";
|
import util, { STATE_EVENT_ROOM_DELETED } from "../plugins/utils";
|
||||||
import User from "../models/user";
|
import User from "../models/user";
|
||||||
|
|
||||||
const LocalStorageCryptoStore =
|
const LocalStorageCryptoStore =
|
||||||
|
|
@ -87,6 +87,12 @@ export default {
|
||||||
return room.selfMembership === "invite";
|
return room.selfMembership === "invite";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
joinedAndInvitedRooms() {
|
||||||
|
return this.rooms.filter((room) => {
|
||||||
|
return room.selfMembership === "join" || room.selfMembership === "invite";
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -113,7 +119,7 @@ export default {
|
||||||
console.log("create crypto store");
|
console.log("create crypto store");
|
||||||
return new LocalStorageCryptoStore(this.$store.getters.storage);
|
return new LocalStorageCryptoStore(this.$store.getters.storage);
|
||||||
},
|
},
|
||||||
login(user, registrationFlowHandler) {
|
login(user, registrationFlowHandler, createUser = false) {
|
||||||
const tempMatrixClient = sdk.createClient({baseUrl: user.home_server, idBaseUrl: this.$config.identityServer});
|
const tempMatrixClient = sdk.createClient({baseUrl: user.home_server, idBaseUrl: this.$config.identityServer});
|
||||||
var promiseLogin;
|
var promiseLogin;
|
||||||
|
|
||||||
|
|
@ -121,14 +127,14 @@ export default {
|
||||||
if (user.access_token) {
|
if (user.access_token) {
|
||||||
// Logged in on "real" account
|
// Logged in on "real" account
|
||||||
promiseLogin = Promise.resolve(user);
|
promiseLogin = Promise.resolve(user);
|
||||||
} else if (user.is_guest && (!user.user_id || user.registration_session)) {
|
} else if (createUser || (user.is_guest && (!user.user_id || user.registration_session))) {
|
||||||
// Generate random username and password. We don't user REAL matrix
|
// Generate random username and password. We don't user REAL matrix
|
||||||
// guest accounts because 1. They are not allowed to post media, 2. They
|
// guest accounts because 1. They are not allowed to post media, 2. They
|
||||||
// can not use avatars and 3. They can not seamlessly be upgraded to real accounts.
|
// can not use avatars and 3. They can not seamlessly be upgraded to real accounts.
|
||||||
//
|
//
|
||||||
// Instead, we use an ILAG approach, Improved Landing as Guest.
|
// Instead, we use an ILAG approach, Improved Landing as Guest.
|
||||||
const userId = user.registration_session ? user.user_id : util.randomUser(this.$config.userIdPrefix);
|
const userId = (createUser || user.registration_session) ? user.user_id : util.randomUser(this.$config.userIdPrefix);
|
||||||
const pass = user.registration_session ? user.password : util.randomPass();
|
const pass = (createUser || user.registration_session) ? user.password : util.randomPass();
|
||||||
|
|
||||||
const extractAndSaveUser = (response) => {
|
const extractAndSaveUser = (response) => {
|
||||||
var u = Object.assign({}, response);
|
var u = Object.assign({}, response);
|
||||||
|
|
@ -376,11 +382,38 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case STATE_EVENT_ROOM_DELETED:
|
||||||
|
{
|
||||||
|
const room = this.matrixClient.getRoom(event.getRoomId());
|
||||||
|
if (room && room.currentState) {
|
||||||
|
// Before we do anything, make sure the sender is an admin!
|
||||||
|
// Also, do not react if WE are the sender, since we are probably
|
||||||
|
// busy doing the rest of the purging process...
|
||||||
|
if (room.currentState.maySendStateEvent("m.room.power_levels", event.getSender())) {
|
||||||
|
if (event.getSender() !== this.currentUserId) {
|
||||||
|
this.leaveRoomAndNavigate(room.roomId).then(() => {
|
||||||
|
this.matrixClient.store.removeRoom(room.roomId);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
this.updateNotificationCount();
|
this.updateNotificationCount();
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoom(ignoredroom) {
|
onRoom(room) {
|
||||||
|
if (room.selfMembership === "invite") {
|
||||||
|
this.matrixClient.getRoomTags(room.roomId).then(reply => {
|
||||||
|
if (Object.keys(reply.tags).includes("m.server_notice")) {
|
||||||
|
Vue.set(room, "isServiceNoticeRoom", true);
|
||||||
|
}
|
||||||
|
}).catch((error => {
|
||||||
|
console.error(error);
|
||||||
|
}))
|
||||||
|
}
|
||||||
this.reloadRooms();
|
this.reloadRooms();
|
||||||
this.updateNotificationCount();
|
this.updateNotificationCount();
|
||||||
},
|
},
|
||||||
|
|
@ -526,6 +559,31 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leave the room, and if this is the last room we are in, navigate to the "goodbye" page.
|
||||||
|
* Otherwise, navigate to home.
|
||||||
|
* @param roomId
|
||||||
|
*/
|
||||||
|
leaveRoomAndNavigate(roomId) {
|
||||||
|
const joinedRooms = this.joinedRooms;
|
||||||
|
const isLastRoomWeAreJoinedTo = (
|
||||||
|
joinedRooms &&
|
||||||
|
joinedRooms.length == 1 &&
|
||||||
|
joinedRooms[0].roomId == roomId
|
||||||
|
);
|
||||||
|
return this.leaveRoom(roomId)
|
||||||
|
.then(() => {
|
||||||
|
if (isLastRoomWeAreJoinedTo) {
|
||||||
|
this.$navigation.push({ name: "Goodbye" }, -1);
|
||||||
|
} else {
|
||||||
|
this.$navigation.push(
|
||||||
|
{ name: "Home", params: { roomId: null } },
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
kickUser(roomId, userId) {
|
kickUser(roomId, userId) {
|
||||||
if (this.matrixClient && roomId && userId) {
|
if (this.matrixClient && roomId && userId) {
|
||||||
this.matrixClient.kick(roomId, userId, "");
|
this.matrixClient.kick(roomId, userId, "");
|
||||||
|
|
@ -700,6 +758,13 @@ export default {
|
||||||
history_visibility: "joined",
|
history_visibility: "joined",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
return this.matrixClient.sendStateEvent(
|
||||||
|
roomId,
|
||||||
|
STATE_EVENT_ROOM_DELETED,
|
||||||
|
{ status: "deleted" }
|
||||||
|
);
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
//console.log("Purge: create timeline");
|
//console.log("Purge: create timeline");
|
||||||
return timelineWindow.load(null, 100);
|
return timelineWindow.load(null, 100);
|
||||||
|
|
@ -783,6 +848,13 @@ export default {
|
||||||
|
|
||||||
return kickFirstMember(allMembers);
|
return kickFirstMember(allMembers);
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
return withRetry(() => this.matrixClient.sendStateEvent(
|
||||||
|
roomId,
|
||||||
|
STATE_EVENT_ROOM_DELETED,
|
||||||
|
{ status: "deleted" }
|
||||||
|
));
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
statusCallback(null);
|
statusCallback(null);
|
||||||
this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
|
this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
|
||||||
|
|
@ -883,7 +955,7 @@ export default {
|
||||||
* @param {*} userId
|
* @param {*} userId
|
||||||
*/
|
*/
|
||||||
isDirectRoomWith(room, userId) {
|
isDirectRoomWith(room, userId) {
|
||||||
if (room.getJoinRule() == "invite" && room.getMembers().length == 2) {
|
if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) {
|
||||||
let other = room.getMember(userId);
|
let other = room.getMember(userId);
|
||||||
if (other) {
|
if (other) {
|
||||||
if (room.getMyMembership() == "invite" && other.membership == "join") {
|
if (room.getMyMembership() == "invite" && other.membership == "join") {
|
||||||
|
|
@ -906,7 +978,7 @@ export default {
|
||||||
isDirectRoom(room) {
|
isDirectRoom(room) {
|
||||||
// TODO - Use the is_direct accountData flag (m.direct). WE (as the client)
|
// TODO - Use the is_direct accountData flag (m.direct). WE (as the client)
|
||||||
// apprently need to set this...
|
// apprently need to set this...
|
||||||
if (room.getJoinRule() == "invite" && room.getMembers().length == 2) {
|
if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,18 @@ export default new Vuex.Store({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
createUser({ commit }, { user, registrationFlowHandler }) {
|
||||||
|
return this._vm.$matrix.login(user, registrationFlowHandler, true).then(
|
||||||
|
user => {
|
||||||
|
commit('loginSuccess', user);
|
||||||
|
return Promise.resolve(user);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
commit('loginFailure');
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
logout({ commit }) {
|
logout({ commit }) {
|
||||||
this._vm.$matrix.logout();
|
this._vm.$matrix.logout();
|
||||||
commit('logout');
|
commit('logout');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
|
//const fs = require('fs')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transpileDependencies: ["vuetify"],
|
transpileDependencies: ["vuetify"],
|
||||||
|
|
@ -47,5 +48,15 @@ module.exports = {
|
||||||
|
|
||||||
devServer: {
|
devServer: {
|
||||||
//https: true,
|
//https: true,
|
||||||
},
|
|
||||||
|
/***
|
||||||
|
* For testing notification via service worker in Mobile
|
||||||
|
* Run your site locally with secure HTTPS using mkcert
|
||||||
|
* https://web.dev/how-to-use-local-https/#running-your-site-locally-with-https-using-mkcert-recommended
|
||||||
|
*/
|
||||||
|
// https: {
|
||||||
|
// key: fs.readFileSync('./your-local-ip-address-key.pem'),
|
||||||
|
// cert: fs.readFileSync('./your-local-ip-address.pem'),
|
||||||
|
// }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||