Merge branch 'dev'
This commit is contained in:
commit
f570d2ba90
66 changed files with 2498 additions and 1580 deletions
17
README.md
17
README.md
|
|
@ -38,13 +38,26 @@ npm run build
|
|||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
### Customize build configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
||||
|
||||
## Theming
|
||||
You can do simple theming by setting values in the configuration file, see below.
|
||||
|
||||
# Sticker short codes - To enable sticker short codes, follow these steps:
|
||||
## Configuration file
|
||||
The app loads runtime configutation from the server at "./config.json" and merges that with the default values in "assets/config.json".
|
||||
The following values can be set via the config file:
|
||||
|
||||
* **logo** - An url or base64-encoded image data url that represents the app logotype.
|
||||
* **accentColor** - The accent color of the app UI.
|
||||
* **show_status_messages** - Whether to show only user joins/leaves and display name updates, or the full range of room status updates. Possible values are "never" (only the above), "moderators" (moderators will see all status updates) or "always" (everyone will see all status updates). Defaults to "always".
|
||||
|
||||
|
||||
### Sticker short codes - To enable sticker short codes, follow these steps:
|
||||
* Run the "create sticker config" script using "npm run create-sticker-config <path-to-sticker-packs>"
|
||||
* 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.
|
||||
|
||||
### Attributions
|
||||
Sounds from [Notification Sounds](https://notificationsounds.com)
|
||||
741
package-lock.json
generated
741
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -30,7 +30,7 @@
|
|||
"linkify-html": "^4.1.0",
|
||||
"linkifyjs": "^4.1.0",
|
||||
"material-design-icons-iconfont": "^6.1",
|
||||
"matrix-js-sdk": "^19.7.0",
|
||||
"matrix-js-sdk": "^23.4.0",
|
||||
"md-gum-polyfill": "^1.0.0",
|
||||
"mic-recorder-to-mp3": "^2.2.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.log("Error creating client", error);
|
||||
if (error.data.errcode ==='M_FORBIDDEN' && this.currentUser.is_guest) {
|
||||
if (error.data && ((error.data.errcode ==='M_FORBIDDEN' && this.currentUser.is_guest) || error.data.errcode ==='M_USER_DEACTIVATED')) {
|
||||
// Guest account and password don't work. We are in a strange state, probably because
|
||||
// of server cleanup of accounts or similar. Wipe account and restart...
|
||||
this.$store.commit("setUser", null);
|
||||
|
|
@ -145,10 +145,10 @@ export default {
|
|||
},
|
||||
|
||||
favicon() {
|
||||
var favicon = 'favicon.ico';
|
||||
var favicon = this.$config.logo ? this.$config.logo : 'favicon.ico';
|
||||
if (this.$route.meta.includeFavicon) {
|
||||
if (this.$matrix.currentRoom) {
|
||||
favicon = this.$matrix.currentRoom.avatar || 'favicon.ico';
|
||||
favicon = this.$matrix.currentRoom.avatar || favicon;
|
||||
}
|
||||
}
|
||||
return favicon;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@
|
|||
"languageSupportEmail": "support@guardianproject.info",
|
||||
"productLink": "letsconvene.im",
|
||||
"defaultServer": "https://neo.keanu.im",
|
||||
"identityServer_unset": "",
|
||||
"rtl": false,
|
||||
"accentColor_unset": "",
|
||||
"logo_unset": "",
|
||||
"analytics": [
|
||||
{
|
||||
"enabled": true,
|
||||
|
|
@ -40,5 +43,8 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"experimental_voice_mode": true
|
||||
"experimental_voice_mode": true,
|
||||
"experimental_read_only_room": true,
|
||||
"experimental_public_room": true,
|
||||
"show_status_messages": "never"
|
||||
}
|
||||
1
src/assets/css/_mixins.scss
Normal file
1
src/assets/css/_mixins.scss
Normal file
|
|
@ -0,0 +1 @@
|
|||
// Sass Mixins
|
||||
|
|
@ -8,10 +8,12 @@ $admin-fg: white;
|
|||
body {
|
||||
--v-background-color: white;
|
||||
--v-foreground-color: black;
|
||||
--v-secondary-color: #242424;
|
||||
--v-divider-color: #eeeeee;
|
||||
&.dark {
|
||||
--v-background-color: black;
|
||||
--v-foreground-color: white;
|
||||
--v-secondary-color: #c0c0c0;
|
||||
--v-divider-color: rgba(221, 221, 221, 0.1);
|
||||
}
|
||||
}
|
||||
|
|
@ -45,15 +47,32 @@ body {
|
|||
border-bottom: 1px solid var(--v-divider-color);
|
||||
.chat-header-row {
|
||||
margin: 0;
|
||||
padding: 4px 10px;
|
||||
padding: 4px 10px 4px 28px;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
button {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
.v-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.chat-header-avatar {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.room-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.chat-header-members,
|
||||
.chat-header-name {
|
||||
overflow: hidden;
|
||||
width: 0px; // Set to 0 for flexbox autosize
|
||||
cursor: pointer;
|
||||
}
|
||||
.chat-header-members {
|
||||
overflow: hidden;
|
||||
}
|
||||
.num-members {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 400;
|
||||
|
|
@ -73,9 +92,8 @@ body {
|
|||
margin-bottom: $chat-standard-padding-xs;
|
||||
}
|
||||
|
||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
.close-button .v-icon {
|
||||
color: var(--v-secondary-color);
|
||||
}
|
||||
|
||||
.icon-dropdown {
|
||||
|
|
@ -84,33 +102,66 @@ body {
|
|||
|
||||
.notification-alert {
|
||||
display: inline-block;
|
||||
background-color: #ff3300;
|
||||
background-color: $alert-bg-color;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
&.popup-open::after {
|
||||
top: 20px;
|
||||
color: #246bfd;
|
||||
}
|
||||
.missed-items-popup {
|
||||
position: absolute;
|
||||
bottom: -17px;
|
||||
left: -20px;
|
||||
transform: translateY(100%);
|
||||
background: #246bfd;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 22px 18px 23px 18px;
|
||||
z-index: 300;
|
||||
user-select: none;
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.button {
|
||||
margin-left: 50px;
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 11.5411px;
|
||||
line-height: 140%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
letter-spacing: 0.34px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
.missed-items-popup-background {
|
||||
content: " ";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
z-index: 250;
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.room-list-notification-count {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 40px;
|
||||
right: initial;
|
||||
color: white;
|
||||
background-color: black;
|
||||
font-size: 10px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid white;
|
||||
text-align: center;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
[dir="rtl"] & {
|
||||
right: 40px;
|
||||
left: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-root {
|
||||
|
|
@ -128,11 +179,6 @@ body {
|
|||
background-color: $chat-background;
|
||||
overflow: hidden;
|
||||
|
||||
.chat-room-invitations {
|
||||
padding: 10px;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
margin: 0;
|
||||
padding-top: $chat-standard-padding-s;
|
||||
|
|
@ -224,7 +270,7 @@ body {
|
|||
padding: 0;
|
||||
min-width: 48px;
|
||||
|
||||
&.input-more-icon {
|
||||
&.input-more-icon {
|
||||
svg {
|
||||
fill: black;
|
||||
}
|
||||
|
|
@ -250,9 +296,23 @@ body {
|
|||
/* Remove text underline */
|
||||
color: transparent !important;
|
||||
min-height: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.input-area-read-only {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(white, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
|
|
@ -292,6 +352,44 @@ body {
|
|||
.message-wrapper {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
.quick-reaction-container .emoji {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.seen-by-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 16px;
|
||||
.clickable {
|
||||
display: flex;
|
||||
height: 16px;
|
||||
}
|
||||
div {
|
||||
height: 16px;
|
||||
}
|
||||
margin-top: 3px;
|
||||
.more {
|
||||
margin-right: 10px;
|
||||
color: #444444;
|
||||
font-size: 12px;
|
||||
}
|
||||
.seen-by-user {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
margin-left: -5px !important;
|
||||
vertical-align: top;
|
||||
}
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 1s;
|
||||
}
|
||||
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
transform: translateX(24px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageIn {
|
||||
|
|
@ -378,6 +476,13 @@ body {
|
|||
.link {
|
||||
color: inherit;
|
||||
}
|
||||
.quick-reaction-container {
|
||||
margin-left: 42px;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-right: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageOut {
|
||||
|
|
@ -479,6 +584,13 @@ body {
|
|||
.link {
|
||||
color: inherit;
|
||||
}
|
||||
.quick-reaction-container {
|
||||
margin-right: 42px;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-left: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sender,
|
||||
|
|
@ -652,44 +764,6 @@ body {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-reaction-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
transform: translateX(-20px) translateX(-100%);
|
||||
top: 18px;
|
||||
z-index: 2;
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid rgba(white, 0.9);
|
||||
border-radius: 13px;
|
||||
height: 26px;
|
||||
width: max-content;
|
||||
padding: 0px 6px;
|
||||
.messageOut & {
|
||||
transform: translateX(20px) translateX(100%);
|
||||
}
|
||||
.quick-reaction {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
margin: 0px 0px;
|
||||
padding: 1px;
|
||||
font-size: 10px;
|
||||
&:hover {
|
||||
//border: 1px solid #888888;
|
||||
background-color: #e2e2e2;
|
||||
}
|
||||
.quick-reaction-count {
|
||||
color: #888888;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
.sent .quick-reaction-count {
|
||||
color: black;
|
||||
font-weight: 700;
|
||||
// background-color: palegreen;
|
||||
}
|
||||
}
|
||||
|
||||
.download-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
|
@ -709,7 +783,7 @@ body {
|
|||
.room-name-inline {
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 18 * $chat-text-size;
|
||||
font-size: 16 * $chat-text-size;
|
||||
text-transform: uppercase;
|
||||
color: var(--v-foreground-color);
|
||||
text-align: center;
|
||||
|
|
@ -787,6 +861,50 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.room-list {
|
||||
.room-list-room {
|
||||
color: white; // Used as selected item background
|
||||
.v-avatar:not(.round) {
|
||||
// Make avatars rounded squares!
|
||||
border-radius: 8px;
|
||||
}
|
||||
.room-list-name,
|
||||
.room-list-new-room {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 117%;
|
||||
letter-spacing: 0.4px;
|
||||
color: #0e252d;
|
||||
}
|
||||
.room-list-new-room {
|
||||
font-weight: 400;
|
||||
}
|
||||
.room-list-new-messages {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 117%;
|
||||
letter-spacing: 0.4px;
|
||||
color: #1d1d1d;
|
||||
padding-left: 13px;
|
||||
position: relative;
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
left: 0;
|
||||
top: 5px;
|
||||
background: $alert-bg-color;
|
||||
border-radius: 3px;
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-info {
|
||||
background-color: #e8e8e8;
|
||||
height: 100%;
|
||||
|
|
@ -802,7 +920,6 @@ body {
|
|||
height: 64px !important;
|
||||
margin-bottom: 20px;
|
||||
cursor: default;
|
||||
|
||||
.headline {
|
||||
font-size: 70 * $chat-text-size !important;
|
||||
}
|
||||
|
|
@ -1159,6 +1276,26 @@ body {
|
|||
}
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.room-option {
|
||||
.v-input {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.option-warning {
|
||||
background: linear-gradient(0deg, #FFF3F3, #FFF3F3), #FFFBED;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
.v-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-link .v-input__slot::before {
|
||||
|
|
@ -1275,11 +1412,11 @@ body {
|
|||
background-color: var(--v-background-color);
|
||||
color: var(--v-foreground-color);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
.load-earlier {
|
||||
flex: 1 0 auto;
|
||||
padding: 20px;
|
||||
|
|
@ -1297,7 +1434,8 @@ body {
|
|||
height: 32px !important;
|
||||
margin-left: -8px !important;
|
||||
}
|
||||
.list-enter-active, .list-leave-active {
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 1s;
|
||||
}
|
||||
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
|
||||
|
|
@ -1310,7 +1448,7 @@ body {
|
|||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items:center;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -1372,12 +1510,28 @@ body {
|
|||
height: 103px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
#btn-play, #btn-pause {
|
||||
#btn-play,
|
||||
#btn-pause {
|
||||
margin: 26px;
|
||||
}
|
||||
.mic-button {
|
||||
z-index: 0;
|
||||
}
|
||||
.mic-button.dimmed {
|
||||
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 {
|
||||
|
|
@ -1385,4 +1539,4 @@ body {
|
|||
right: 20px;
|
||||
bottom: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
@import './variables';
|
||||
@import './utilities';
|
||||
@import './mixins';
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
|
|
@ -122,6 +123,12 @@ body { position:absolute; top:0; bottom:0; right:0; left:0; }
|
|||
min-height: $chat-standard-padding !important;
|
||||
margin-top: $chat-standard-padding-xs;
|
||||
margin-bottom: $chat-standard-padding-xs;
|
||||
|
||||
.v-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-dialog {
|
||||
|
|
|
|||
5
src/assets/icons/ic_circle.vue
Normal file
5
src/assets/icons/ic_circle.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="7.5" stroke="#242424" stroke-opacity="0.3" />
|
||||
</svg>
|
||||
</template>
|
||||
6
src/assets/icons/ic_circle_filled.vue
Normal file
6
src/assets/icons/ic_circle_filled.vue
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="7.5" stroke="#242424" />
|
||||
<circle cx="8" cy="8" r="4.5" fill="#242424" stroke="#242424" />
|
||||
</svg>
|
||||
</template>
|
||||
15
src/assets/icons/ic_download.vue
Normal file
15
src/assets/icons/ic_download.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M14.1487 15.7001H0.822597C0.376044 15.7001 0 15.3241 0 14.8775C0 14.431 0.376044 14.0549 0.822597 14.0549H14.1487C14.5952 14.0549 14.9713 14.431 14.9713 14.8775C14.9713 15.3241 14.5952 15.7001 14.1487 15.7001Z"
|
||||
fill="#161616" />
|
||||
<path
|
||||
d="M7.4974 12.1509C7.05085 12.1509 6.6748 11.7749 6.6748 11.3283V0.822597C6.6748 0.376044 7.05085 0 7.4974 0C7.94395 0 8.32 0.376044 8.32 0.822597V11.3283C8.32 11.7749 7.94395 12.1509 7.4974 12.1509Z"
|
||||
fill="#161616" />
|
||||
<path
|
||||
d="M7.49734 12.151C7.28581 12.151 7.07429 12.0805 6.90977 11.916L3.05531 8.03806C2.72627 7.70902 2.72627 7.19196 3.05531 6.88643C3.38435 6.55739 3.90141 6.55739 4.20695 6.88643L8.0614 10.7409C8.39044 11.0699 8.39044 11.587 8.0614 11.8925C7.92039 12.0805 7.70886 12.151 7.49734 12.151Z"
|
||||
fill="#161616" />
|
||||
<path
|
||||
d="M7.49739 12.151C7.28586 12.151 7.07434 12.0805 6.90982 11.916C6.58078 11.587 6.58078 11.0699 6.90982 10.7644L10.7878 6.88643C11.1168 6.55739 11.6339 6.55739 11.9394 6.88643C12.2685 7.21547 12.2685 7.73253 11.9394 8.03806L8.08496 11.916C7.92044 12.0805 7.70891 12.151 7.49739 12.151Z"
|
||||
fill="#161616" />
|
||||
</svg></template>
|
||||
12
src/assets/icons/ic_info.vue
Normal file
12
src/assets/icons/ic_info.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.49142 7.29595V7.29595C8.6693 7.29366 8.84063 7.36327 8.96659 7.48895L8.9666 7.48896C9.09283 7.61492 9.16273 7.78654 9.1605 7.96481M8.49142 7.29595L9.0355 7.96399M8.49142 7.29595L8.49009 7.29597M8.49142 7.29595L8.49009 7.29597M9.1605 7.96481C9.1605 7.96512 9.16049 7.96542 9.16049 7.96573L9.0355 7.96399M9.1605 7.96481V7.96399H9.0355M9.1605 7.96481V11.7131M9.0355 7.96399V11.714M9.1605 11.7131C9.1605 11.7128 9.16049 11.7125 9.16049 11.7122L9.0355 11.714M9.1605 11.7131C9.1628 11.8897 9.09425 12.0598 8.97033 12.1855C8.84616 12.3115 8.67664 12.3824 8.49979 12.3824C8.32293 12.3824 8.15349 12.3115 8.02932 12.1855M9.1605 11.7131V11.714H9.0355M9.0355 11.714L8.02932 12.1855M8.02932 12.1855L8.11832 12.0978L8.0293 12.1855C8.02931 12.1855 8.02932 12.1855 8.02932 12.1855ZM8.02932 12.1855C7.90529 12.0598 7.83685 11.8897 7.83907 11.7132M8.02932 12.1855L7.83907 7.96481M8.49009 7.29597C8.48989 7.29597 8.4897 7.29598 8.4895 7.29598C8.31454 7.29866 8.14772 7.37071 8.02581 7.49631L8.49009 7.29597ZM7.83907 7.96481C7.83692 7.79012 7.90404 7.62168 8.02577 7.49636L7.83907 7.96481ZM7.83907 7.96481V11.7132M7.83907 11.7132V11.714H7.96407L7.83908 11.7122C7.83908 11.7126 7.83908 11.7129 7.83907 11.7132Z"
|
||||
fill="#161616" stroke="black" stroke-width="0.25" />
|
||||
<path
|
||||
d="M8.49998 5.94643C8.86489 5.94643 9.16069 5.65062 9.16069 5.28571C9.16069 4.92081 8.86489 4.625 8.49998 4.625C8.13507 4.625 7.83926 4.92081 7.83926 5.28571C7.83926 5.65062 8.13507 5.94643 8.49998 5.94643Z"
|
||||
fill="#161616" stroke="black" stroke-width="0.25" />
|
||||
<path
|
||||
d="M8.5 0.875C4.29525 0.875 0.875 4.29525 0.875 8.5C0.875 12.7048 4.29525 16.125 8.5 16.125C12.7048 16.125 16.125 12.7048 16.125 8.5C16.125 4.29525 12.7048 0.875 8.5 0.875ZM8.5 2.19643C11.9877 2.19643 14.8036 5.01232 14.8036 8.5C14.8036 11.9877 11.9877 14.8036 8.5 14.8036C5.01232 14.8036 2.19643 11.9877 2.19643 8.5C2.19643 5.01232 5.01232 2.19643 8.5 2.19643Z"
|
||||
fill="#161616" stroke="black" stroke-width="0.25" />
|
||||
</svg></template>
|
||||
7
src/assets/icons/ic_link.vue
Normal file
7
src/assets/icons/ic_link.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="17" height="9" viewBox="0 0 17 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.615 4.5C1.615 2.961 2.7965 1.71 4.25 1.71H7.65V0H4.25C1.904 0 0 2.016 0 4.5C0 6.984 1.904 9 4.25 9H7.65V7.29H4.25C2.7965 7.29 1.615 6.039 1.615 4.5ZM5.1 5.4H11.9V3.6H5.1V5.4ZM12.75 0H9.35V1.71H12.75C14.2035 1.71 15.385 2.961 15.385 4.5C15.385 6.039 14.2035 7.29 12.75 7.29H9.35V9H12.75C15.096 9 17 6.984 17 4.5C17 2.016 15.096 0 12.75 0Z"
|
||||
fill="#161616" />
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5714 14.1903C10.7619 14.357 10.9762 14.4284 11.1904 14.4284C11.4523 14.4284 11.7142 14.3093 11.9047 14.095L14.9047 10.6189C15.2142 10.2617 15.2142 9.7379 14.9047 9.38076L11.9047 5.90457C11.5714 5.49981 10.9523 5.45219 10.5714 5.80933C10.1666 6.14267 10.119 6.76171 10.4762 7.14267L12.119 9.04743H6.0952C5.57139 9.04743 5.14282 9.476 5.14282 9.99981C5.14282 10.5236 5.57139 10.9522 6.0952 10.9522H12.119L10.4762 12.857C10.119 13.2617 10.1666 13.857 10.5714 14.1903Z" fill="white"/>
|
||||
<path d="M17.5 6.92857C17.6905 7.40476 18.2619 7.64286 18.7381 7.45238C19.2143 7.2619 19.4524 6.69048 19.2619 6.21429C17.7143 2.42857 14.0714 0 10 0C4.47619 0 0 4.47619 0 10C0 15.5238 4.47619 20 10 20C14.0714 20 17.7143 17.5714 19.2619 13.7857C19.4524 13.3095 19.2381 12.7381 18.7381 12.5476C18.2619 12.3571 17.6905 12.5714 17.5 13.0714C16.2381 16.119 13.3095 18.0952 10 18.0952C5.54762 18.0952 1.90476 14.4524 1.90476 10C1.90476 5.54762 5.54762 1.90476 10 1.90476C13.3095 1.90476 16.2381 3.88095 17.5 6.92857Z" fill="white"/>
|
||||
<path d="M10.5714 14.1903C10.7619 14.357 10.9762 14.4284 11.1904 14.4284C11.4523 14.4284 11.7142 14.3093 11.9047 14.095L14.9047 10.6189C15.2142 10.2617 15.2142 9.7379 14.9047 9.38076L11.9047 5.90457C11.5714 5.49981 10.9523 5.45219 10.5714 5.80933C10.1666 6.14267 10.119 6.76171 10.4762 7.14267L12.119 9.04743H6.0952C5.57139 9.04743 5.14282 9.476 5.14282 9.99981C5.14282 10.5236 5.57139 10.9522 6.0952 10.9522H12.119L10.4762 12.857C10.119 13.2617 10.1666 13.857 10.5714 14.1903Z" fill="currentColor"/>
|
||||
<path d="M17.5 6.92857C17.6905 7.40476 18.2619 7.64286 18.7381 7.45238C19.2143 7.2619 19.4524 6.69048 19.2619 6.21429C17.7143 2.42857 14.0714 0 10 0C4.47619 0 0 4.47619 0 10C0 15.5238 4.47619 20 10 20C14.0714 20 17.7143 17.5714 19.2619 13.7857C19.4524 13.3095 19.2381 12.7381 18.7381 12.5476C18.2619 12.3571 17.6905 12.5714 17.5 13.0714C16.2381 16.119 13.3095 18.0952 10 18.0952C5.54762 18.0952 1.90476 14.4524 1.90476 10C1.90476 5.54762 5.54762 1.90476 10 1.90476C13.3095 1.90476 16.2381 3.88095 17.5 6.92857Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/ic_more.vue
Normal file
7
src/assets/icons/ic_more.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="3" height="15" viewBox="0 0 3 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0.000170402 1.49991C0.00017042 1.10212 0.158154 0.720548 0.439485 0.439314C0.720719 0.157973 1.10229 4.81825e-08 1.50009 6.55708e-08C1.89788 8.29591e-08 2.27945 0.157984 2.56069 0.439314C2.84203 0.720549 3 1.10212 3 1.49991C3 1.89771 2.84202 2.27928 2.56069 2.56052C2.27945 2.84186 1.89788 2.99983 1.50009 2.99983C1.10229 2.99983 0.720718 2.84185 0.439485 2.56052C0.158143 2.27928 0.000170385 1.89771 0.000170402 1.49991ZM0.00017014 7.49957C0.000170157 7.10178 0.158154 6.72021 0.439484 6.43897C0.720718 6.15763 1.10229 5.99966 1.50008 5.99966C1.89788 5.99966 2.27945 6.15764 2.56069 6.43897C2.84203 6.72021 3 7.10178 3 7.49957C3 7.89737 2.84202 8.27894 2.56069 8.56017C2.27945 8.84152 1.89788 8.99949 1.50008 8.99949C1.10052 8.99949 0.717478 8.84014 0.435949 8.55661C0.154394 8.27318 -0.00242794 7.8891 0.00017014 7.48953L0.00017014 7.49957ZM0.000169878 13.4992C0.000169895 13.1014 0.158154 12.7199 0.439484 12.4386C0.720718 12.1573 1.10229 11.9993 1.50008 11.9993C1.89788 11.9993 2.27945 12.1573 2.56068 12.4386C2.84203 12.7199 3 13.1014 3 13.4992C3 13.897 2.84202 14.2786 2.56068 14.5598C2.27945 14.8412 1.89788 14.9991 1.50008 14.9991C1.09832 14.9991 0.713406 14.8381 0.431449 14.5519C0.149492 14.2657 -0.00588337 13.8784 0.000169879 13.4767L0.000169878 13.4992Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/ic_new_room.vue
Normal file
7
src/assets/icons/ic_new_room.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M10.7445 5.16411H6.83749V1.25713C6.83749 0.782712 6.4747 0.419922 6.00028 0.419922C5.52586 0.419922 5.16307 0.782712 5.16307 1.25713V5.16411H1.25609C0.781675 5.16411 0.418884 5.5269 0.418884 6.00132C0.418884 6.47573 0.781675 6.83853 1.25609 6.83853H5.16307V10.7455C5.16307 11.2199 5.52586 11.5827 6.00028 11.5827C6.4747 11.5827 6.83749 11.2199 6.83749 10.7455V6.83853H10.7445C11.2189 6.83853 11.5817 6.47573 11.5817 6.00132C11.5817 5.5269 11.2189 5.16411 10.7445 5.16411Z"
|
||||
fill="#242424" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/ic_public.vue
Normal file
7
src/assets/icons/ic_public.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.59107 0.0357724L5.39388 2.30685C5.36127 2.31538 5.31109 2.32819 5.27848 2.33459H0.667336C0.298545 2.33459 0 2.5886 0 2.90236V6.30471C0 6.61847 0.298545 6.87248 0.667336 6.87248H1.13648C1.25439 6.87248 1.35725 6.93864 1.39237 7.03256L2.36933 9.83565C2.40195 9.93383 2.50732 10 2.62523 10H4.73762C4.91575 10 5.04369 9.85486 4.99352 9.71185L4.10687 7.16703C4.05419 7.02189 4.18214 6.87461 4.36277 6.87461H5.26593C5.3136 6.88742 5.35876 6.89809 5.40642 6.90876L9.59107 9.1713C9.76919 9.26735 10 9.15849 10 8.9792V0.22574C10 0.0485792 9.76668 -0.0602787 9.59107 0.0357724Z"
|
||||
fill="#242424" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/ic_security-shield.vue
Normal file
7
src/assets/icons/ic_security-shield.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M25.619 7.08953L25.5625 7.07888C21.9335 6.38303 18.5747 4.43518 15.2936 1.12288L15.279 1.10825C15.079 0.906348 14.8083 0.790138 14.5241 0.784177C14.2399 0.778215 13.9646 0.88297 13.7563 1.0763L13.7064 1.12288C10.4253 4.43518 7.06646 6.38303 3.4375 7.07889L3.38096 7.08954C3.13088 7.13759 2.90535 7.27127 2.74316 7.46759C2.58096 7.6639 2.49221 7.91059 2.49219 8.16524V8.22179C2.49207 12.2448 3.55097 16.197 5.56245 19.681C7.57393 23.165 10.4671 26.0582 13.9512 28.0696C14.1095 28.1608 14.2881 28.211 14.4707 28.2156C14.6533 28.2203 14.8342 28.1792 14.9969 28.0962L15.0488 28.0696C18.5329 26.0582 21.4261 23.165 23.4375 19.681C25.449 16.197 26.5079 12.2448 26.5078 8.22179V8.16524C26.5078 7.91059 26.419 7.6639 26.2568 7.46758C26.0946 7.27126 25.8691 7.13759 25.619 7.08953ZM14.5 25.6827V3.59895C17.5695 6.48016 20.7467 8.30761 24.1581 9.15314C24.0015 12.4832 23.0412 15.7258 21.3594 18.6042C19.6776 21.4826 17.3241 23.9112 14.5 25.6827Z"
|
||||
fill="#1D1D1D" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/assets/icons/ic_warning.vue
Normal file
7
src/assets/icons/ic_warning.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.35799 14.8085L8.18319 2.98652C8.55719 2.33892 9.25399 1.93652 10.002 1.93652C10.75 1.93652 11.4468 2.33892 11.8208 2.98652L18.7066 14.9137C19.0804 15.5613 19.0804 16.3659 18.7066 17.0137C18.3326 17.6613 17.6358 18.0637 16.8878 18.0637H3.11199C1.95399 18.0637 1.01199 17.1217 1.01199 15.9637C1.01219 15.5505 1.13159 15.1517 1.35799 14.8085ZM3.11219 16.6635H16.8878C17.137 16.6635 17.3694 16.5293 17.494 16.3135C17.6186 16.0975 17.6186 15.8293 17.494 15.6135L10.608 3.68672C10.4834 3.47072 10.251 3.33672 10.0018 3.33672C9.75259 3.33672 9.52019 3.47092 9.39559 3.68672L2.55879 15.5283C2.55039 15.5429 2.54139 15.5573 2.53179 15.5713C2.45319 15.6869 2.41179 15.8227 2.41179 15.9637C2.41219 16.3495 2.72619 16.6635 3.11219 16.6635ZM9.99999 12.7861C9.56799 12.7861 9.21759 12.4359 9.21759 12.0037V7.93752C9.21759 7.50532 9.56799 7.15512 9.99999 7.15512C10.432 7.15512 10.7824 7.50532 10.7824 7.93752V12.0037C10.7824 12.4359 10.432 12.7861 9.99999 12.7861ZM9.99999 13.2451C10.264 13.2451 10.5202 13.3511 10.708 13.5371C10.894 13.7231 11 13.9811 11 14.2451C11 14.5071 10.894 14.7651 10.708 14.9511C10.5202 15.1371 10.2622 15.2451 9.99999 15.2451C9.73799 15.2451 9.47999 15.1371 9.29399 14.9511C9.10619 14.7651 8.99999 14.5071 8.99999 14.2451C8.99999 13.9811 9.10599 13.7231 9.29399 13.5371C9.47999 13.3509 9.73799 13.2451 9.99999 13.2451Z"
|
||||
fill="#1D1D1D" />
|
||||
</svg>
|
||||
</template>
|
||||
BIN
src/assets/sounds/record_stop.mp3
Normal file
BIN
src/assets/sounds/record_stop.mp3
Normal file
Binary file not shown.
|
|
@ -3,11 +3,6 @@
|
|||
"name": "འདུ་འཛོམས།",
|
||||
"tag_line": "འབྲེལ་མཐུད་བྱོས།"
|
||||
},
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"fallbacks": {
|
||||
"download_name": "ཕབ་ལེན།",
|
||||
"original_text": "<མ་ཡིག>",
|
||||
|
|
@ -35,7 +30,16 @@
|
|||
"permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་ཁག",
|
||||
"created_by": "{user} བཟོས།",
|
||||
"copy_link": "གདན་ཞུ་འབྲེལ་ཐག་པར་བཤུས་རྒྱོབས།",
|
||||
"scan_code": "བཤེར་རིས་བཤེར་ཏེ་ཁ་བརྡ་ཁང་དུ་འཛུལ།"
|
||||
"scan_code": "བཤེར་རིས་བཤེར་ཏེ་ཁ་བརྡ་ཁང་དུ་འཛུལ།",
|
||||
"user_admin": "དོ་དམ་པ།",
|
||||
"experimental_features": "ཚོད་ལྟའི་ཁྱད་ཆོས་ཁག",
|
||||
"read_only_room_info": "དོ་དམ་པ་དང་གཙོ་སྐྱོང་བ་ཁོ་ནས་ཁ་བརྡ་ཁང་དུ་གཏོང་ཆོག",
|
||||
"export_room": "ཁ་བརྡའི་ཟིན་ཐོ་ཕྱིར་འདྲེན།",
|
||||
"user_moderator": "གཙོ་སྐྱོང་བ།",
|
||||
"voice_mode": "སྐད་སྒྲའི་རྣམ་པ།",
|
||||
"voice_mode_info": "ཁ་བརྡའི་འབྲེལ་མཐུད་དེ་ཉན་པ་དང་སྒྲ་ཕབ་ཀྱི་རྣམ་པའི་ནང་དུ་བསྒྱུར།",
|
||||
"download_chat": "ཁ་བརྡ་ཕབ་ལེན།",
|
||||
"read_only_room": "ཁ་བརྡ་ཁང་དུ་ཀློག་མ་གཏོགས་མི་ཆོག"
|
||||
},
|
||||
"invite": {
|
||||
"done": "ལེགས་འགྲུབ།",
|
||||
|
|
@ -53,7 +57,15 @@
|
|||
"username_required": "སྤྱོད་མིང་དགོས་ཀྱི་ཡོད།",
|
||||
"create_room": "ཐོ་འགོད་དང་ཁ་བརྡ་ཁང་གསར་སྐྲུན།",
|
||||
"or": "ཡང་ན།",
|
||||
"invalid_message": "རྒྱུན་སྤྱོད་མིང་ངམ་གསང་ཚིག་འགྲིག་མི་འདུག"
|
||||
"invalid_message": "རྒྱུན་སྤྱོད་མིང་ངམ་གསང་ཚིག་འགྲིག་མི་འདུག",
|
||||
"terms": "ཁྱིམ་སྤྱོད་དྲ་བའི་ཞབས་ཞུ་ཆས་ཀྱིས་ཁྱེད་ལ་གཤམ་གྱི་སྲིད་ཇུས་འདི་དག་དང་ལེན་བྱེད་དགོས་པའི་རེ་སྐུལ་བྱེད་ཀྱིན་འདུག",
|
||||
"accept_terms": "དང་ལེན།",
|
||||
"email": "ཁྱེད་ཀྱིས་སོ་སོའི་ཡིག་ཟམ་ཁ་བྱང་ར་སྤྲོད་བྱེད་དགོས།",
|
||||
"resend_verification": "ར་སྤྲོད་ཡིག་ཟམ་བསྐྱར་དུ་ཐོངས།",
|
||||
"send_verification": "ར་སྤྲོད་ཡིག་ཟམ་ཐོངས།",
|
||||
"no_supported_flow": "བཀོལ་ཆས་འདི་སྤྲད་ཡོད་པའི་དྲ་བའི་ཞབས་ཞུ་ཆས་ནང་དུ་འཛུལ་ཐུབ་ཀྱི་མི་འདུག",
|
||||
"sent_verification": "{email}ཡིག་ཟམ་ཁ་བྱང་འདིའི་ཐོག་ཏུ་འཕྲིན་པ་ཞིག་བཏང་ཡོད། ཡིག་ཟམ་ནང་འཛུལ་ཏེ་སོ་སོའི་ཡིག་ཟམ་ཁ་བྱང་ར་སྤྲོད་བྱེད་རོགས།",
|
||||
"email_not_valid": "ཡིག་ཟམ་ཁ་བྱང་བེད་མེད་རེད་འདུག"
|
||||
},
|
||||
"new_room": {
|
||||
"next": "རྗེས་མ།",
|
||||
|
|
@ -72,11 +84,12 @@
|
|||
"set_join_permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་སྒྲིག་འགོད་བྱོས།",
|
||||
"join_permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་ཁག",
|
||||
"new_room": "ཁ་བརྡ་ཁང་གསར་པ།",
|
||||
"name_room": "ཁ་བརྡ་ཁང་ལ་མིང་ཐོགས།",
|
||||
"name_room": "ཁ་བརྡ་ཁང་གི་མིང་།",
|
||||
"room_topic": "གལ་ཏེ་འདོད་པ་ཡོད་ན། ཚོགས་པའི་སྐོར་གྱི་འགྲེལ་བཤད་ཐུང་ངུ་ཞིག་འབྲི་ཆོག",
|
||||
"create": "བཟོས།",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"colon_not_allowed": ": རྟགས་འདི་བཀོལ་མི་ཆོག",
|
||||
"options": "གདམ་ག",
|
||||
"room_name_limit_error_msg": "ཡིག་འབྲུ་གྲངས་༥༠ ལས་བརྒལ་མི་ཆོག"
|
||||
},
|
||||
"menu": {
|
||||
"logout": "ཕྱིར་ཐོན།",
|
||||
|
|
@ -95,7 +108,12 @@
|
|||
"loading": "{appName} སྣོན་འཇུག་བྱེད་བཞིན་པ།",
|
||||
"ignore": "སྣང་མེད་ཐོངས།",
|
||||
"join": "ཞུགས།",
|
||||
"undo": "ཕྱིར་བསྡུ།"
|
||||
"undo": "ཕྱིར་བསྡུ།",
|
||||
"user_kick_and_ban": "སྤྱོད་མཁན་འདི་ཕྱིར་འབུད་དང་བཀག་སྡོམ་བྱོས།",
|
||||
"user_make_admin": "འཛིན་སྐྱོང་པ་བཟོས།",
|
||||
"user_make_moderator": "གཙོ་སྐྱོང་བ་བཟོས།",
|
||||
"user_revoke_moderator": "གཙོ་སྐྱོང་བའི་དབང་ཚད་མེད་པར་བཟོས།",
|
||||
"user_kick": "སྤྱོད་མཁན་འདི་སྒོར་ཕུད།"
|
||||
},
|
||||
"profile": {
|
||||
"change_password": "གསང་ཚིག་རྗེས།",
|
||||
|
|
@ -112,7 +130,7 @@
|
|||
"language_description": "འདུ་འཛོམས་སྐད་ཡིག་མང་པོའི་ནང་དུ་ཡོད།",
|
||||
"dont_see_yours": "ཁྱེད་ཀྱི་མིང་མཐོང་གི་མི་འདུག་གམ།",
|
||||
"tell_us": "ང་ཚོར་ཤོོད།",
|
||||
"display_name_required": ""
|
||||
"display_name_required": "འཆར་མིང་དགོས།"
|
||||
},
|
||||
"device_list": {
|
||||
"not_verified": "ར་སྤྲོད་བྱས་མི་འདུག",
|
||||
|
|
@ -137,11 +155,14 @@
|
|||
"room_list_rooms": "ཁ་བརྡ་ཁང་།",
|
||||
"room_list_invites": "གདན་ཞུ་ཁག",
|
||||
"purge_failed": "ཁ་བརྡ་ཁང་བཤིག་ཐུབ་མ་སོང་།",
|
||||
"purge_removing_members": "ཚོགས་མི་ཁག་ཕྱིར་འདོན།",
|
||||
"purge_redacting_events": "ཁ་བརྡ་གཙང་གསུབ།",
|
||||
"purge_removing_members": "ཚོགས་མི་ཁག་ཕྱིར་འདོན། {total})་ཀྱི་({members}",
|
||||
"purge_redacting_events": "ཁ་བརྡ་གཙང་གསུབ། {total})་ཀྱི་({count}",
|
||||
"purge_set_room_state": "ཁ་བརྡ་ཁང་གི་རྣམ་པ་སྒྲིག་འགོད།",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"room_list_new_messages": "{count} ཆ་འཕྲིན་གསར་པ།",
|
||||
"room_topic_required": "ཁ་བརྡ་ཁང་ལ་འགྲེལ་བཤད་དགོས།",
|
||||
"room_name_required": "ཁ་བརྡ་ཁང་ལ་མིང་ཞིག་དགོས།",
|
||||
"invitations": "ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་གང་ཡང་མི་འདུག | ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་གྲངས་གཅིག་འདུག | ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་{གྲངས}་འདུག",
|
||||
"unseen_messages": "ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་གང་ཡང་མི་འདུག | ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་གཅིག་འདུག | ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་{count}འདུག"
|
||||
},
|
||||
"message": {
|
||||
"users_are_typing": "{count} ཚོགས་མི་ཡིས་གཏགས་བཞིན་འདུག",
|
||||
|
|
@ -180,12 +201,21 @@
|
|||
"room_powerlevel_change": "{user} {changes} ཡི་སྟོབས་ཤུགས་གནས་རིམ་བརྗེས་སོང་།",
|
||||
"user_changed_guest_access_open": "{user} མགྲོན་པོ་ཁ་བརྡ་ཁང་དུ་འཛུལ་བཅུག་སོང་།",
|
||||
"user_changed_guest_access_closed": "{user} མགྲོན་པོ་ཁ་བརྡ་ཁང་དུ་འཛུལ་བཅུག་མ་སོང་།",
|
||||
"reply_image": "",
|
||||
"reply_audio_message": "",
|
||||
"reply_video": "",
|
||||
"reply_poll": "",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"reply_image": "པར་རིས།",
|
||||
"reply_audio_message": "སྐད་བརྡའི་ཆ་འཕྲིན།",
|
||||
"reply_video": "བརྙན།",
|
||||
"user_was_banned_you": "ཁྱེད་རང་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་སོང་།",
|
||||
"incoming_message_deleted_text": "ཆ་འཕྲིན་འདི་བསུབས་ཟིན།",
|
||||
"reply_poll": "བསམ་ཚུལ་བསྡུ་ལེན།",
|
||||
"not_allowed_to_send": "དོ་དམ་པ་དང་གཙོ་སྐྱོང་བ་ཁོ་ནས་མ་གཏོགས་ཁ་བརྡ་ཁང་དུ་གཏོང་མི་ཆོག",
|
||||
"user_was_kicked_by_you": "ཁྱེད་ཀྱིས་{སྤྱོད་མཁན}་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
|
||||
"user_was_kicked": "{སྤྱོད་མཁན} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་ཟིན།",
|
||||
"user_was_kicked_you": "ཁྱེད་རང་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
|
||||
"user_was_banned": "{སྤྱོད་མཁན}་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།",
|
||||
"user_was_banned_by_you": "ཁྱེད་ཀྱིས་{སྤྱོད་མཁན} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།",
|
||||
"time_ago": "དེ་རིང་། | ཁ་སང་། | ཉིན་གྲངས་{count} གོང་།",
|
||||
"outgoing_message_deleted_text": "ཁྱེད་ཀྱིས་ཆ་འཕྲིན་འདི་བསུབས་སོང་།",
|
||||
"reaction_count_more": "{reactionCount} མང་བ།"
|
||||
},
|
||||
"power_level": {
|
||||
"moderator": "མདོ་འཛིན་པ།",
|
||||
|
|
@ -226,27 +256,24 @@
|
|||
"text_public": "གལ་ཏེ་ཁྱེད་ཀྱིས་འབྲེལ་ཐག་དེ་ཧ་གོ་ཚེ། ག་དུས་ཡིན་ཡང་། ཁ་བརྡ་ཁང་དུ་འཛུལ་ཆོག",
|
||||
"title_public": "{user} ག་ལེེར་བཞུགས།"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"join": {
|
||||
"title": "ཁྱེད་རང་ནང་དུ་ཞུགས་པར་དགའ་བསུ་ཞུ།",
|
||||
"title_user": "",
|
||||
"user_name_label": "སྤྱོད་མིང་།",
|
||||
"remember_me": "ང་དྲན་པར་བྱོས།",
|
||||
"joining_as": "ཁྱེད་རང་ཞུགས་བཞིན་པ།:",
|
||||
"join": "ཁ་བརྡ་ཁང་དུ་འཛུལ།",
|
||||
"join_user": "",
|
||||
"enter_room": "ནང་དུ་ཞུགས།",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "ནང་འཛུལ་བྱེད་བཞིན་པ།...",
|
||||
"status_joining": "ཁ་བརྡ་ཁང་དུ་འཛུལ་བཞིན་པ།...",
|
||||
"join_failed": "ཁ་བརྡ་ཁང་དུ་འཛུལ་ཐུབ་མ་སོང་།",
|
||||
"choose_name": ""
|
||||
"join_user": "ཁ་བརྡ་འགོ་རྩོམ།",
|
||||
"enter_room_user": "ཁ་བརྡ་འགོ་རྩོམ།",
|
||||
"choose_name": "མིང་ཞིག་འདེམས་ཏེ་བཀོལ།",
|
||||
"title_user": "དགའ་བསུ་ཞུ། ཁྱེད་རང་ཁ་བརྡ་བྱེད་པར་གདན་ཞུ་གནང་སོང་།"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"powered_by": "ཁ་བརྡ་ཁང་འདི་{product} ནུས་ཤུགས་བསྩལ་ཡོད། {productLink} ནས་དེ་ལས་མང་བ་སྦྱོང་ཆོག་ལ། མདུན་དུ་བསྐྱོད་དེ་ཁ་བརྡ་ཁང་གཞན་ཞིག་བསྐྲུན་ཆོག",
|
||||
"new_room": "+ ཁ་བརྡ་ཁང་གསར་པ།",
|
||||
"new_room": "ཁ་བརྡ་ཁང་གསར་པ།",
|
||||
"want_more": "དེ་ལས་མང་བ་དགོས་སམ།",
|
||||
"logout": "ཕྱིར་ཐོན།",
|
||||
"edit_profile": "སྒེར་གྱི་ཡིག་ཆ་བཅོས་སྒྲིག",
|
||||
|
|
@ -259,5 +286,49 @@
|
|||
"close_tab": "བཤེར་ཆས་ཁ་རྒྱོབས།",
|
||||
"room_deleted": "ཁ་བརྡ་ཁང་མེད་པར་བཟོས་སོང་།"
|
||||
},
|
||||
"language_display_name": "བོད་ཡིག"
|
||||
"language_display_name": "བོད་ཡིག",
|
||||
"global": {
|
||||
"save": "ཉར་ཚགས།",
|
||||
"password_didnot_match": "གསང་ཚིག་མཐུན་གྱི་མི་འདུག",
|
||||
"password_hint": "ཉུང་མཐར་ཡང་ཡིག་འབྲུ་༡༢་དགོས་ལ། དེའི་ནང་དུ་ཨང་གྲངས་གཅིག་དང་། ཡིག་ཆེན་གཅིག ཡིག་ཆུང་གཅིག་ངེས་པར་དུ་ཚང་དགོས།",
|
||||
"add_reaction": "ཡ་ལན་ཁ་སྣོན།",
|
||||
"click_to_remove": "བསྣུན་ཏེ་མེད་པར་བཟོས།",
|
||||
"show_less": "ཉུང་བ་སྟོན།",
|
||||
"show_more": "མང་བ་སྟོན།"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": "ཁྱེད་རང་ཁ་བརྡ་ཁང་ནས་ཕྱི་རུ་ཐོན་རྒྱུ་ཡིན་ནམ།"
|
||||
},
|
||||
"poll_create": {
|
||||
"failed": "བསམ་ཚུལ་བསྡུ་ལེན་གྱི་འདེམས་ཤོག་བཟོ་ཐུབ་མ་སོང་། རྗེས་སུ་བསྐྱར་དུ་ཚོད་ལྟ་བྱོས།",
|
||||
"answer_label_1": "དྲིས་ལན། *",
|
||||
"tip_title": "ཆེད་ལས་གསལ་འདེབས།",
|
||||
"tip_text": "ཚོགས་མི་ཚོས་དྲིས་ལན་བཏབ་ཚར་ན་འདེམས་ཤོག་གི་གྲུབ་འབྲས་མཐོང་ཐུབ། དྲིས་ལན་བཏབ་ཚར་རྗེས་འདེམས་ཤོག་ཁ་བརྒྱབ་སྟེ་ཁ་བརྡ་ཁང་གི་ཚོགས་མི་ཚང་མར་འདེམས་ཤོག་གི་གྲུབ་འབྲས་སྟོན།",
|
||||
"create_poll_menu_option": "འདེམས་ཤོག་བཟོས།",
|
||||
"poll_status_closed": "འདེམས་ཤོག་སྒོ་བརྒྱབ་ཟིན།",
|
||||
"poll_status_open_not_voted": "འདེམས་ཤོག་སྒོ་ཕྱེ་ཡོད། -འདེམས་ཤོག་འཕངས་ཏེ་གྲུབ་འབྲས་ལ་གཟིགས།",
|
||||
"close_poll": "འདེམས་ཤོག་སྒོ་རྒྱོབ།",
|
||||
"poll_submit": "ཡར་སྤྲོད།",
|
||||
"num_answered": "དྲིས་ལན{count}",
|
||||
"creating": "འདེམས་ཤོག་བཟོ་བཞིན་པ།",
|
||||
"poll_disclosed": "འགོ་བཙུགས་ཚར- མིག་སྔའི་མཇུག་འབྲས་ག་དུས་ཡིན་ཡང་སྟོན་གྱི་ཡོད།",
|
||||
"poll_undisclosed": "མཇུག་བསྒྲིལ། - འདེམས་ཤོག་མཇུག་བསྒྲིལ་སྐབས་སྤྱོད་མཁན་གྱིས་གྲུབ་འབྲས་མཐོང་ངེས།",
|
||||
"question_label": "ཁྱེད་ཀྱི་དྲི་བ་དྲིས། *",
|
||||
"question_required": "ཁྱེད་ཀྱིས་དྲི་བ་ཞིག་སྐོང་དགོས།",
|
||||
"poll_status_disclosed": "འདེམས་ཤོག་སྒོ་བརྒྱབ་ན་གྲུབ་འབྲས་སྟོན་ངེས།",
|
||||
"poll_status_open": "འདེམས་ཤོག་སྒོ་ཕྱེ་ཡོད།",
|
||||
"title": "འདེམས་ཤོག་གསར་པ་ཞིག་བཟོས།",
|
||||
"create": "འདོན་སྤེལ།",
|
||||
"add_answer": "དྲིས་ལན་ཁ་སྣོན།",
|
||||
"answer_label_n": "དྲིས་ལན།",
|
||||
"please_complete": "ལེགས་འགྲུབ་བྱེད་རོགས།",
|
||||
"answer_required": "དྲིས་ལན་སྟོང་པ་བཞག་མི་ཆོག ཡིག་འབྲུ་ཁ་ཤས་ཤིག་གཏགས་རོགས། ཡང་མིན་ན། གདམ་ག་འདི་མེད་པར་བཟོས།",
|
||||
"view_results": "གྲུབ་འབྲས་ལ་གཟིགས།",
|
||||
"results_shared": "གྲུབ་འབྲས་ཁ་བརྡ་ཁང་དུ་བརྒྱུད་སྤེལ་བྱེད་ངེས།"
|
||||
},
|
||||
"export": {
|
||||
"exported_date": "{date} ཉིན་ལ་ཕྱིར་འདྲེན་བྱས།",
|
||||
"export_filename": "{date} ཉིན་ཕྱིར་འདྲེན་བྱས་པའི་ཁ་བརྡ།",
|
||||
"processed_n_of_total_events": "བྱུང་བ་{total}ཁོངས་ནས་ལས་སྣོན་བྱས་ཟིན་པའི་སྨྱན་སྦྱོར་གྱི་གྲངས {count}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"language_display_name": "Deutsch",
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"menu": {
|
||||
"start_private_chat": "Private Diskussion mit diesem Benutzer",
|
||||
|
|
@ -59,9 +56,7 @@
|
|||
"user_left": "{user} hat das Gespräch verlassen",
|
||||
"user_joined": "{Benutzer} ist dem Gespräch beigetreten",
|
||||
"download_progress": "{percentage} % heruntergeladen",
|
||||
"user_changed_room_name": "{user} hat den Raumnamen in {name} geändert",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"user_changed_room_name": "{user} hat den Raumnamen in {name} geändert"
|
||||
},
|
||||
"room": {
|
||||
"leave": "Verlassen",
|
||||
|
|
@ -71,9 +66,7 @@
|
|||
"members": "keine Mitglieder | 1 Mitglied | {count} Mitglieder",
|
||||
"purge_removing_members": "Entfernen von Mitgliedern",
|
||||
"purge_failed": "Fehler beim Bereinigen des Raums!",
|
||||
"room_list_rooms": "Räume",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"room_list_rooms": "Räume"
|
||||
},
|
||||
"room_welcome": {
|
||||
"info": "Herzlich willkommen! Hier sind ein paar Dinge, die du über deinen Raum wissen solltest:",
|
||||
|
|
@ -103,9 +96,7 @@
|
|||
"create": "Erstellen",
|
||||
"next": "Nächste",
|
||||
"name_room": "Raum benennen",
|
||||
"room_topic": "Füge eine Beschreibung hinzu, wenn du möchtest",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"room_topic": "Füge eine Beschreibung hinzu, wenn du möchtest"
|
||||
},
|
||||
"device_list": {
|
||||
"title": "GERÄTE",
|
||||
|
|
@ -121,8 +112,7 @@
|
|||
"password_required": "Das Passwort ist erforderlich",
|
||||
"login": "Anmelden",
|
||||
"create_room": "Registrieren und Raum erstellen",
|
||||
"or": "ODER",
|
||||
"invalid_message": ""
|
||||
"or": "ODER"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Mein Profil",
|
||||
|
|
@ -134,8 +124,7 @@
|
|||
"password_old": "Altes Passwort",
|
||||
"password_new": "Neues Kennwort",
|
||||
"password_repeat": "Wiederhole das neue Passwort",
|
||||
"display_name": "Anzeigename",
|
||||
"display_name_required": ""
|
||||
"display_name": "Anzeigename"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"you_are": "Du bist",
|
||||
|
|
@ -156,8 +145,7 @@
|
|||
"status_logging_in": "Wird angemeldet …",
|
||||
"status_joining": "Raum beitreten …",
|
||||
"join_failed": "Beitritt zum Raum fehlgeschlagen.",
|
||||
"title": "Willkommen in {roomName}",
|
||||
"choose_name": ""
|
||||
"title": "Willkommen in {roomName}"
|
||||
},
|
||||
"invite": {
|
||||
"title": "Freunde hinzufügen",
|
||||
|
|
@ -177,7 +165,6 @@
|
|||
"title_invite": "Bist du sicher, dass du gehen willst?"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"purge_room": {
|
||||
"info": "Alle Mitglieder und Nachrichten werden entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@
|
|||
"global": {
|
||||
"save": "Save",
|
||||
"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 atleast one numeric, one uppercase and one lowercase letter",
|
||||
"show_less": "Show less",
|
||||
"show_more": "Show more",
|
||||
"add_reaction": "Add reaction",
|
||||
"click_to_remove": "Click to remove"
|
||||
},
|
||||
"menu": {
|
||||
"start_private_chat": "Private chat with this user",
|
||||
|
|
@ -54,6 +58,7 @@
|
|||
"file_prefix": "File: ",
|
||||
"edited": "(edited)",
|
||||
"download_progress": "{percentage}% downloaded",
|
||||
"upload_file_too_large": "File is too large to upload!",
|
||||
"upload_progress": "Uploaded {count}",
|
||||
"upload_progress_with_total": "Uploaded {count} of {total}",
|
||||
"user_changed_room_history": "{user} made room history {type}",
|
||||
|
|
@ -82,10 +87,14 @@
|
|||
"reply_poll": "Poll",
|
||||
"time_ago": "Today | Yesterday | {count} days ago",
|
||||
"outgoing_message_deleted_text": "You deleted this message.",
|
||||
"incoming_message_deleted_text": "This message was deleted."
|
||||
"incoming_message_deleted_text": "This message was deleted.",
|
||||
"not_allowed_to_send": "Only admins and moderators are allowed to send to the room",
|
||||
"reaction_count_more": "{reactionCount} more",
|
||||
"seen_by": "Seen by no members | Seen by 1 member | Seen by {count} members"
|
||||
},
|
||||
"room": {
|
||||
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",
|
||||
"unseen_messages": "You have no unseen messages | You have 1 unseen message | You have {count} unseen messages",
|
||||
"members": "no members | 1 member | {count} members",
|
||||
"leave": "Leave",
|
||||
"purge_set_room_state": "Setting room state",
|
||||
|
|
@ -93,6 +102,7 @@
|
|||
"purge_removing_members": "Removing members ({count} of {total})",
|
||||
"purge_failed": "Failed to purge room!",
|
||||
"room_list_invites": "Invites",
|
||||
"room_list_new_messages": "{count} new messages",
|
||||
"room_list_rooms": "Rooms",
|
||||
"room_name_required": "Room name is required",
|
||||
"room_topic_required": "Room description is required"
|
||||
|
|
@ -105,7 +115,8 @@
|
|||
"join_public": "Anyone can join by opening this link: {link}.",
|
||||
"join_invite": "Only people you invite can join.",
|
||||
"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."
|
||||
},
|
||||
"new_room": {
|
||||
"new_room": "New Room",
|
||||
|
|
@ -145,7 +156,15 @@
|
|||
"login": "Login",
|
||||
"create_room": "Register & Create Room",
|
||||
"or": "OR",
|
||||
"invalid_message": "Invalid username or password"
|
||||
"invalid_message": "Invalid username or password",
|
||||
"no_supported_flow": "The app can't login to the given server",
|
||||
"terms": "The homeserver requires you to review and accept the following policies:",
|
||||
"accept_terms": "Accept",
|
||||
"email": "You need to verify you email address",
|
||||
"send_verification": "Send verification email",
|
||||
"sent_verification": "An email has been sent to {email}. Please use your regular email client to verify the address.",
|
||||
"resend_verification": "Resend verification email",
|
||||
"email_not_valid": "Email address not valid"
|
||||
},
|
||||
"profile": {
|
||||
"title": "My Profile",
|
||||
|
|
@ -172,7 +191,7 @@
|
|||
"logout": "Logout",
|
||||
"want_more": "Want more?",
|
||||
"powered_by": "This room is powered by {product}. Learn more at {productLink} or go ahead and create another room!",
|
||||
"new_room": "+ New room"
|
||||
"new_room": "New room"
|
||||
},
|
||||
"join": {
|
||||
"title": "Welcome you have been invited to join",
|
||||
|
|
@ -246,7 +265,12 @@
|
|||
"user_moderator": "Moderator",
|
||||
"experimental_features": "Experimental Features",
|
||||
"voice_mode": "Voice mode",
|
||||
"voice_mode_info": "Switches the chat interface to a 'listen and record' mode"
|
||||
"voice_mode_info": "Switches the chat interface to a 'listen and record' mode",
|
||||
"download_chat": "Download chat",
|
||||
"read_only_room": "Read only room",
|
||||
"read_only_room_info": "Only admins and moderators are allowed to send to the room",
|
||||
"make_public": "Make Public",
|
||||
"make_public_warning": "warning: Full message history will be visible to new participants"
|
||||
},
|
||||
"room_info_sheet": {
|
||||
"this_room": "This room",
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@
|
|||
"tag_line": "Simplemente conectar"
|
||||
},
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"room_info": {
|
||||
"identity": "Has iniciado sesión como {displayName}.",
|
||||
|
|
@ -53,7 +50,6 @@
|
|||
"title_public": "Adios, [user}"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"invite": {
|
||||
"status_error": "No se pudo invitar a uno o más amigos!",
|
||||
|
|
@ -64,18 +60,14 @@
|
|||
},
|
||||
"join": {
|
||||
"title": "Bienvenido has sido invitado a unirte",
|
||||
"title_user": "",
|
||||
"user_name_label": "Nombre de usuario",
|
||||
"remember_me": "Recordarme",
|
||||
"joining_as": "Te estas uniendo como:",
|
||||
"join": "Unirse a la sala",
|
||||
"join_user": "",
|
||||
"enter_room": "Entrar habitacion",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "Iniciando sesión...",
|
||||
"status_joining": "Uniendose a la sala...",
|
||||
"join_failed": "No se pudo unir a la sala.",
|
||||
"choose_name": ""
|
||||
"join_failed": "No se pudo unir a la sala."
|
||||
},
|
||||
"profile": {
|
||||
"display_name": "Nombre para mostrar",
|
||||
|
|
@ -91,8 +83,7 @@
|
|||
"set_language": "Establece tu Idioma",
|
||||
"language_description": "Convine esta disponible en varios Idiomas.",
|
||||
"dont_see_yours": "¿No ves el tuyo?",
|
||||
"tell_us": "Dinos.",
|
||||
"display_name_required": ""
|
||||
"tell_us": "Dinos."
|
||||
},
|
||||
"login": {
|
||||
"login": "Iniciar sesión",
|
||||
|
|
@ -130,9 +121,7 @@
|
|||
"done": "Listo",
|
||||
"new_room": "Nueva Sala",
|
||||
"create": "Crear",
|
||||
"room_topic": "Añade una descripción si quieres",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"room_topic": "Añade una descripción si quieres"
|
||||
},
|
||||
"room_welcome": {
|
||||
"join_public": "Cualquiera puede unirse abriendo este vínculo: {link}",
|
||||
|
|
@ -153,9 +142,7 @@
|
|||
"purge_set_room_state": "Estado de la sala",
|
||||
"purge_removing_members": "Eliminar miembros",
|
||||
"purge_failed": "¡Fallo en la purga de la sala!",
|
||||
"room_list_rooms": "Salas",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"room_list_rooms": "Salas"
|
||||
},
|
||||
"message": {
|
||||
"user_powerlevel_change_from_to": "{user} de {powerOld} a {powerNew}",
|
||||
|
|
@ -195,11 +182,8 @@
|
|||
"reply_image": "Imagen",
|
||||
"reply_audio_message": "Mensaje de audio",
|
||||
"reply_video": "Vídeo",
|
||||
"reply_poll": "",
|
||||
"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",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"user_changed_guest_access_open": "{user} has permitido que los invitados se unieran a la sala"
|
||||
},
|
||||
"menu": {
|
||||
"login": "Iniciar sesión",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"menu": {
|
||||
"back": "TAKAISIN",
|
||||
|
|
@ -32,9 +29,7 @@
|
|||
"room_topic": "Lisää kuvaus, jos haluat",
|
||||
"add_people": "Lisää ihmisiä",
|
||||
"link_copied": "Linkki kopioitu!",
|
||||
"public_info": "Kuka tahansa, jolla on linkki",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"public_info": "Kuka tahansa, jolla on linkki"
|
||||
},
|
||||
"purge_room": {
|
||||
"n_seconds": "{seconds} sekuntia",
|
||||
|
|
@ -67,23 +62,16 @@
|
|||
"login": "Kirjaudu sisään",
|
||||
"password": "Anna salasana",
|
||||
"password_required": "Salasana vaaditaan",
|
||||
"create_room": "Rekisteröidy ja luo huone",
|
||||
"invalid_message": ""
|
||||
"create_room": "Rekisteröidy ja luo huone"
|
||||
},
|
||||
"join": {
|
||||
"title": "Tervetuloa huoneen {roomName}",
|
||||
"title_user": "",
|
||||
"user_name_label": "Käyttäjätunnus",
|
||||
"remember_me": "",
|
||||
"joining_as": "Liityt jäsenenä:",
|
||||
"join": "Liity huoneeseen",
|
||||
"join_user": "",
|
||||
"enter_room": "",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "Kirjautuminen sisään…",
|
||||
"status_joining": "Liittyminen huoneeseen…",
|
||||
"join_failed": "Huoneeseen liittyminen epäonnistui.",
|
||||
"choose_name": ""
|
||||
"join_failed": "Huoneeseen liittyminen epäonnistui."
|
||||
},
|
||||
"leave": {
|
||||
"title_public": "Näkemiin, {user}",
|
||||
|
|
@ -94,7 +82,6 @@
|
|||
"text_invite": "Tämä huone on lukittu. Et pääse takaisin ilman erillistä lupaa."
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"message": {
|
||||
"you": "Sinä",
|
||||
|
|
@ -109,15 +96,11 @@
|
|||
"user_joined": "{user} liittyi keskusteluun",
|
||||
"file_prefix": "Tiedosto: ",
|
||||
"edited": "(muokattu)",
|
||||
"users_are_typing": "{count} jäsentä kirjoitavat",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"users_are_typing": "{count} jäsentä kirjoitavat"
|
||||
},
|
||||
"room": {
|
||||
"leave": "Poistu",
|
||||
"room_list_rooms": "Huoneet",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"room_list_rooms": "Huoneet"
|
||||
},
|
||||
"room_welcome": {
|
||||
"room_history_is": "Huoneen historia on {type}.",
|
||||
|
|
@ -136,8 +119,7 @@
|
|||
"set_password": "Aseta salasana",
|
||||
"select_language": "Kieli",
|
||||
"password_old": "Vanha salasana",
|
||||
"display_name": "Näyttönimi",
|
||||
"display_name_required": ""
|
||||
"display_name": "Näyttönimi"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"want_more": "Haluatko lisää?",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"menu": {
|
||||
"edit": "Modifier",
|
||||
|
|
@ -59,9 +56,7 @@
|
|||
"room_joinrule_public": "public",
|
||||
"unread_messages": "Messages non lus",
|
||||
"users_are_typing": "{count} membres écrivent",
|
||||
"room_powerlevel_change": "{user} a changé le statut de {changes}",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"room_powerlevel_change": "{user} a changé le statut de {changes}"
|
||||
},
|
||||
"room": {
|
||||
"members": "aucun membre | 1 membre | {count} membres",
|
||||
|
|
@ -71,9 +66,7 @@
|
|||
"purge_removing_members": "Suppression de membres",
|
||||
"room_list_invites": "Invitations",
|
||||
"room_list_rooms": "Salons",
|
||||
"purge_redacting_events": "Rédaction des évènements",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"purge_redacting_events": "Rédaction des évènements"
|
||||
},
|
||||
"room_welcome": {
|
||||
"info": "Bienvenue ! Voici quelques informations à connaître sur votre salon :",
|
||||
|
|
@ -103,9 +96,7 @@
|
|||
"get_link": "Obtenir le lien",
|
||||
"public_info": "Quiconque avec un lien",
|
||||
"join_permissions_info": "Ces autorisations déterminent comment les personnes peuvent rejoindre le salon et avec quelle facilité d’autres personnes peuvent être invitées. Elles peuvent être modifiées à tout moment.",
|
||||
"status_creating": "Création du salon",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"status_creating": "Création du salon"
|
||||
},
|
||||
"device_list": {
|
||||
"title": "APPAREILS",
|
||||
|
|
@ -121,8 +112,7 @@
|
|||
"password_required": "Le mot de passe est obligatoire",
|
||||
"create_room": "S’inscrire et créer un salon",
|
||||
"or": "OU",
|
||||
"login": "Se connecter",
|
||||
"invalid_message": ""
|
||||
"login": "Se connecter"
|
||||
},
|
||||
"profile": {
|
||||
"temporary_identity": "Cette identité est temporaire. Définissez un mot de passe pour l’utiliser à nouveau",
|
||||
|
|
@ -134,8 +124,7 @@
|
|||
"display_name": "Nom d’affichage",
|
||||
"title": "Mon profil",
|
||||
"set_password": "Définir un mot de passe",
|
||||
"password_new": "Nouveau mot de passe",
|
||||
"display_name_required": ""
|
||||
"password_new": "Nouveau mot de passe"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"you_are": "Vous êtes",
|
||||
|
|
@ -149,18 +138,12 @@
|
|||
},
|
||||
"join": {
|
||||
"title": "Bienvenue dans {roomName}",
|
||||
"title_user": "",
|
||||
"user_name_label": "Nom d’utilisateur",
|
||||
"remember_me": "",
|
||||
"joining_as": "Vous rejoignez en tant que :",
|
||||
"join": "Rejoindre le salon",
|
||||
"join_user": "",
|
||||
"enter_room": "",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "Connexion en cours…",
|
||||
"status_joining": "Adhésion au salon…",
|
||||
"join_failed": "Impossible de rejoindre le salon.",
|
||||
"choose_name": ""
|
||||
"join_failed": "Impossible de rejoindre le salon."
|
||||
},
|
||||
"invite": {
|
||||
"title": "Ajouter des amis",
|
||||
|
|
@ -180,7 +163,6 @@
|
|||
"go_back": "Retour"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"purge_room": {
|
||||
"title": "Supprimer le salon ?",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"message": {
|
||||
"file_prefix": "File: ",
|
||||
|
|
@ -40,9 +37,7 @@
|
|||
"room_history_world_readable": "leggibile da chiunque",
|
||||
"room_history_shared": "leggibile da tutti i membri nella stanza",
|
||||
"user_is_typing": "{user} sta scrivendo",
|
||||
"users_are_typing": "{count} membri stanno scrivendo",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"users_are_typing": "{count} membri stanno scrivendo"
|
||||
},
|
||||
"room": {
|
||||
"purge_removing_members": "Rimozione di membri",
|
||||
|
|
@ -52,9 +47,7 @@
|
|||
"purge_redacting_events": "Redazione di eventi",
|
||||
"purge_failed": "Impossibile pulire la stanza!",
|
||||
"room_list_invites": "Inviti",
|
||||
"room_list_rooms": "Stanze",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"room_list_rooms": "Stanze"
|
||||
},
|
||||
"menu": {
|
||||
"reply": "Risposta",
|
||||
|
|
@ -102,9 +95,7 @@
|
|||
"new_room": "Nuova stanza",
|
||||
"invite_info": "Solo le persone aggiunte",
|
||||
"join_permissions_info": "Questi permessi determinano come le persone possono entrare nella stanza e quanto facilmente gli altri possono essere invitati. Possono essere cambiati in qualsiasi momento.",
|
||||
"public_info": "Chiunque abbia un collegamento",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"public_info": "Chiunque abbia un collegamento"
|
||||
},
|
||||
"device_list": {
|
||||
"title": "DISPOSITIVI",
|
||||
|
|
@ -120,8 +111,7 @@
|
|||
"login": "Accedi",
|
||||
"create_room": "Registrati e crea una stanza",
|
||||
"or": "O",
|
||||
"username_required": "Il nome utente è richiesto",
|
||||
"invalid_message": ""
|
||||
"username_required": "Il nome utente è richiesto"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Il mio profilo",
|
||||
|
|
@ -133,8 +123,7 @@
|
|||
"display_name": "Nome visualizzato",
|
||||
"change_name": "Cambia il nome",
|
||||
"change_password": "Cambia la password",
|
||||
"password_new": "Nuova password",
|
||||
"display_name_required": ""
|
||||
"password_new": "Nuova password"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"you_are": "Sei",
|
||||
|
|
@ -148,18 +137,12 @@
|
|||
},
|
||||
"join": {
|
||||
"title": "Benvenuto/a in {roomName}",
|
||||
"title_user": "",
|
||||
"user_name_label": "Nome utente",
|
||||
"remember_me": "",
|
||||
"joining_as": "Ti stai unendo come:",
|
||||
"join": "Unisciti alla stanza",
|
||||
"join_user": "",
|
||||
"enter_room": "",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "Accesso in corso…",
|
||||
"status_joining": "Unendosi alla stanza…",
|
||||
"join_failed": "Impossibile unirsi alla stanza.",
|
||||
"choose_name": ""
|
||||
"join_failed": "Impossibile unirsi alla stanza."
|
||||
},
|
||||
"invite": {
|
||||
"title": "Aggiungi amici",
|
||||
|
|
@ -179,7 +162,6 @@
|
|||
"text_public_lastroom": "Se vuoi unirti di nuovo a questa stanza, puoi farlo con una nuova identità. Per mantenere {user}, {action}."
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"purge_room": {
|
||||
"info": "Tutti i membri e i messaggi saranno rimossi. Questa azione non può essere annullata.",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
{
|
||||
"project": {
|
||||
"name": "Convene",
|
||||
"tag_line": ""
|
||||
"name": "Convene"
|
||||
},
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"message": {
|
||||
"user_changed_guest_access_open": "{user} tillot gjester å ta del i rommet",
|
||||
|
|
@ -33,13 +29,7 @@
|
|||
"file_prefix": "Fil: ",
|
||||
"user_said": "{user} sa:",
|
||||
"user_created_room": "{user} opprettet rommet",
|
||||
"you": "Deg",
|
||||
"reply_image": "",
|
||||
"reply_audio_message": "",
|
||||
"reply_video": "",
|
||||
"reply_poll": "",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"you": "Deg"
|
||||
},
|
||||
"device_list": {
|
||||
"title": "Enheter",
|
||||
|
|
@ -80,18 +70,9 @@
|
|||
},
|
||||
"join": {
|
||||
"title": "Velkommen til {roomName}",
|
||||
"title_user": "",
|
||||
"user_name_label": "",
|
||||
"remember_me": "",
|
||||
"joining_as": "",
|
||||
"join": "Ta del i rom",
|
||||
"join_user": "",
|
||||
"enter_room": "",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "Logger inn …",
|
||||
"status_joining": "Tar del i rom…",
|
||||
"join_failed": "",
|
||||
"choose_name": ""
|
||||
"status_joining": "Tar del i rom…"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"identity_temporary": "{displayName}",
|
||||
|
|
@ -104,35 +85,25 @@
|
|||
"password_new": "Nytt passord",
|
||||
"password_old": "Gammelt passord",
|
||||
"select_language": "Språk",
|
||||
"set_language": "",
|
||||
"language_description": "",
|
||||
"dont_see_yours": "",
|
||||
"tell_us": "",
|
||||
"change_password": "Endre passord",
|
||||
"change_name": "Endre navn",
|
||||
"set_password": "Sett passord",
|
||||
"title": "Min profil",
|
||||
"display_name": "Visningsnavn",
|
||||
"password_repeat": "Gjenta nytt passord",
|
||||
"display_name_required": ""
|
||||
"password_repeat": "Gjenta nytt passord"
|
||||
},
|
||||
"login": {
|
||||
"password_required": "Passord kreves",
|
||||
"username_required": "Brukernavn kreves",
|
||||
"password": "Passord",
|
||||
"username": "Brukernavn",
|
||||
"create_room": "",
|
||||
"or": "",
|
||||
"invalid_message": ""
|
||||
"username": "Brukernavn"
|
||||
},
|
||||
"new_room": {
|
||||
"add_people": "Legg til folk",
|
||||
"link_copied": "Lenke kopiert.",
|
||||
"next": "Neste",
|
||||
"create": "Opprett",
|
||||
"new_room": "Nytt rom",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"new_room": "Nytt rom"
|
||||
},
|
||||
"room_welcome": {
|
||||
"room_history_is": "Romhistorikken er {type}.",
|
||||
|
|
@ -144,9 +115,7 @@
|
|||
"room_list_rooms": "Rom",
|
||||
"room_list_invites": "Invitasjoner",
|
||||
"purge_set_room_state": "Setter romtilstand",
|
||||
"leave": "Forlat",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"leave": "Forlat"
|
||||
},
|
||||
"purge_room": {
|
||||
"n_seconds": "{seconds} sekunder",
|
||||
|
|
@ -159,7 +128,6 @@
|
|||
"title_public": "Adjø, {user}"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"invite": {
|
||||
"status_inviting": "Inviterer venn {index} av {count}",
|
||||
|
|
@ -176,7 +144,6 @@
|
|||
"login": "Logg inn",
|
||||
"send": "Send",
|
||||
"ok": "OK",
|
||||
"done": "",
|
||||
"cancel": "Avbryt",
|
||||
"download": "last ned",
|
||||
"delete": "Slett",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
"tag_line": "Basta conectar"
|
||||
},
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
"save": "Salvar",
|
||||
"password_didnot_match": "A senha não coincidiu",
|
||||
"password_hint": "Mínimo de 12 caracteres contendo pelo menos um número, uma maiúscula e uma minúscula",
|
||||
"show_less": "Mostrar menos",
|
||||
"show_more": "Mostrar mais",
|
||||
"add_reaction": "Adicionar reação",
|
||||
"click_to_remove": "Clique para remover"
|
||||
},
|
||||
"invite": {
|
||||
"title": "Adiciona amigos",
|
||||
|
|
@ -39,7 +43,12 @@
|
|||
"undo": "Desfazer",
|
||||
"join": "Entrar",
|
||||
"ignore": "Ignore",
|
||||
"loading": "Carregando {appName}"
|
||||
"loading": "Carregando {appName}",
|
||||
"user_kick": "Expulsar este usuário",
|
||||
"user_make_moderator": "Tornar moderador",
|
||||
"user_kick_and_ban": "Expulsar e banir este usuário",
|
||||
"user_make_admin": "Tornar administrador",
|
||||
"user_revoke_moderator": "Revogar moderador"
|
||||
},
|
||||
"message": {
|
||||
"you": "Você",
|
||||
|
|
@ -81,23 +90,33 @@
|
|||
"reply_image": "Imagem",
|
||||
"reply_audio_message": "Mensagem de áudio",
|
||||
"reply_video": "Vídeo",
|
||||
"reply_poll": "",
|
||||
"reply_poll": "Enquete",
|
||||
"time_ago": "Hoje | Ontem | {count} dias atrás",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"outgoing_message_deleted_text": "Você excluiu esta mensagem.",
|
||||
"incoming_message_deleted_text": "Esta mensagem foi excluída.",
|
||||
"user_was_banned_by_you": "Você expulsou e baniu {user} do chat.",
|
||||
"not_allowed_to_send": "Apenas administradores e moderadores podem postar na sala",
|
||||
"user_was_kicked_by_you": "Você expulsou {user} do chat.",
|
||||
"user_was_kicked_you": "Você foi expulso do chat.",
|
||||
"user_was_banned": "{user} foi expulso e banido do chat.",
|
||||
"user_was_kicked": "{user} foi expulso do chat.",
|
||||
"user_was_banned_you": "Você foi expulso e banido do chat.",
|
||||
"reaction_count_more": "{reactionCount} mais"
|
||||
},
|
||||
"room": {
|
||||
"members": "sem membros | 1 membro | {count} membros",
|
||||
"leave": "Sair",
|
||||
"purge_set_room_state": "Configurando o estado da sala",
|
||||
"purge_redacting_events": "Redigindo os eventos",
|
||||
"purge_removing_members": "Removendo os membros",
|
||||
"purge_redacting_events": "Redigindo eventos ({count} de {total})",
|
||||
"purge_removing_members": "Removendo membros ({count} de {total})",
|
||||
"purge_failed": "Houve uma falha ao eliminar a sala!",
|
||||
"room_list_invites": "Convites",
|
||||
"room_list_rooms": "Salas",
|
||||
"invitations": "Você não tem convites | Você tem 1 convite | Você tem {count} convites",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"room_name_required": "O nome da sala é obrigatório",
|
||||
"room_topic_required": "A descrição da sala é obrigatória",
|
||||
"unseen_messages": "Você não possui mensagens não lidas | Você tem 1 mensagem não lida | Você tem {count} mensagens não lidas",
|
||||
"room_list_new_messages": "{count} novas mensagens"
|
||||
},
|
||||
"room_welcome": {
|
||||
"info": "Bem-vindo! Aqui estão algumas coisas que você deve saber sobre a sua sala:",
|
||||
|
|
@ -129,7 +148,8 @@
|
|||
"status_avatar_total": "Enviando o avatar: {count} de {total}",
|
||||
"status_avatar": "Enviando avatar: {count}",
|
||||
"room_name_limit_error_msg": "O máximo de 50 caracteres são permitidos",
|
||||
"colon_not_allowed": ""
|
||||
"colon_not_allowed": "Dois pontos não são permitidos",
|
||||
"options": "Opções"
|
||||
},
|
||||
"device_list": {
|
||||
"title": "DISPOSITIVOS",
|
||||
|
|
@ -146,7 +166,15 @@
|
|||
"login": "Entrar",
|
||||
"create_room": "Cadastre-se e crie uma sala",
|
||||
"or": "OU",
|
||||
"invalid_message": "O nome de usuário ou a senha estão inválidos"
|
||||
"invalid_message": "O nome de usuário ou a senha estão inválidos",
|
||||
"terms": "O servidor doméstico exige que você analise e aceite as seguintes políticas:",
|
||||
"accept_terms": "Aceitar",
|
||||
"email": "Você precisa verificar o seu endereço de e-mail",
|
||||
"send_verification": "Enviar 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",
|
||||
"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"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Meu perfil",
|
||||
|
|
@ -163,7 +191,7 @@
|
|||
"password_new": "Nova senha",
|
||||
"password_repeat": "Repita a nova senha",
|
||||
"display_name": "Nome de exibição",
|
||||
"display_name_required": ""
|
||||
"display_name_required": "O nome de exibição é obrigatório"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"you_are": "Você é",
|
||||
|
|
@ -173,18 +201,18 @@
|
|||
"logout": "Sair",
|
||||
"want_more": "Quer mais?",
|
||||
"powered_by": "Esta sala é oferecida por {product}. Saiba mais em {productLink} ou vá em frente e crie outra sala!",
|
||||
"new_room": "+ Nova sala"
|
||||
"new_room": "Nova sala"
|
||||
},
|
||||
"join": {
|
||||
"title": "Bem-vindo, você foi convidado a participar",
|
||||
"title_user": "",
|
||||
"title_user": "Bem-vindo, você foi convidado para conversar com",
|
||||
"user_name_label": "Nome do usuário",
|
||||
"remember_me": "Me lembre",
|
||||
"joining_as": "Você está entrando como:",
|
||||
"join": "Entrar na sala",
|
||||
"join_user": "",
|
||||
"join_user": "Comece a conversar",
|
||||
"enter_room": "Entre na sala",
|
||||
"enter_room_user": "",
|
||||
"enter_room_user": "Comece a conversar",
|
||||
"status_logging_in": "Fazendo login...",
|
||||
"status_joining": "Entrando na sala...",
|
||||
"join_failed": "Houve uma falha ao entrar na sala.",
|
||||
|
|
@ -201,7 +229,7 @@
|
|||
"leave": "Sair"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
"confirm_text": "Tem certeza de que deseja sair?"
|
||||
},
|
||||
"purge_room": {
|
||||
"title": "Exclui a sala?",
|
||||
|
|
@ -235,7 +263,15 @@
|
|||
"leave_room": "Sair",
|
||||
"version_info": "Desenvolvido por Guardian Project. Versão: {version}",
|
||||
"scan_code": "Faça a varredura para entrar na sala",
|
||||
"export_room": "Exportar a conversa"
|
||||
"export_room": "Exportar a conversa",
|
||||
"voice_mode": "Modo de voz",
|
||||
"user_admin": "Administrador",
|
||||
"voice_mode_info": "Alterna a interface de chat para um modo de 'ouvir e gravar'",
|
||||
"read_only_room": "Sala somente leitura",
|
||||
"user_moderator": "Moderador",
|
||||
"experimental_features": "Recursos experimentais",
|
||||
"download_chat": "Baixar o chat",
|
||||
"read_only_room_info": "Apenas administradores e moderadores podem postar na sala"
|
||||
},
|
||||
"room_info_sheet": {
|
||||
"this_room": "Esta sala",
|
||||
|
|
@ -256,14 +292,14 @@
|
|||
"restricted": "restrito"
|
||||
},
|
||||
"poll_create": {
|
||||
"title": "Criar uma enquete",
|
||||
"title": "Criar uma nova enquete",
|
||||
"intro": "Preencha os detalhes abaixo.",
|
||||
"create": "Criar",
|
||||
"create": "Publicar",
|
||||
"creating": "Criando a enquete",
|
||||
"poll_disclosed": "Aberto - os resultados atuais são mostrados em todos os momentos.",
|
||||
"add_answer": "Adicionar uma resposta",
|
||||
"failed": "Houve uma falha ao criar a enquete. Tente novamente mais tarde.",
|
||||
"question_label": "Digite a sua pergunta aqui",
|
||||
"question_label": "Faça a sua 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.",
|
||||
|
|
@ -274,8 +310,15 @@
|
|||
"poll_status_open_not_voted": "A enquete está aberta - vote para ver os resultados",
|
||||
"close_poll": "Fechar a enquete",
|
||||
"poll_submit": "Enviar",
|
||||
"num_answered": "{count} responderam",
|
||||
"poll_undisclosed": "Fechado - os usuários verão os resultados quando a pesquisa for fechada."
|
||||
"num_answered": "{count} respostas",
|
||||
"poll_undisclosed": "Fechado - os usuários verão os resultados quando a pesquisa for fechada.",
|
||||
"answer_label_1": "Resposta*",
|
||||
"answer_label_n": "Resposta",
|
||||
"please_complete": "Favor completar",
|
||||
"tip_text": "Os membros verão os resultados da enquete depois que responderem. Feche a enquete ao concluir para exibir os resultados a todos na sala.",
|
||||
"tip_title": "DICA PRO",
|
||||
"view_results": "Ver os resultados",
|
||||
"results_shared": "Resultados compartilhados com a sala."
|
||||
},
|
||||
"export": {
|
||||
"exported_date": "Foi exportado em {date}",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@
|
|||
"tag_line": "Conectați pur și simplu"
|
||||
},
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"menu": {
|
||||
"ok": "OK",
|
||||
|
|
@ -89,7 +86,6 @@
|
|||
"title_public": "La revedere, {user}"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"invite": {
|
||||
"status_error": "Nu ați reușit să invitați unul sau mai mulți prieteni!",
|
||||
|
|
@ -100,18 +96,14 @@
|
|||
},
|
||||
"join": {
|
||||
"title": "Bine ați venit, ați fost invitat să vă alăturați",
|
||||
"title_user": "",
|
||||
"user_name_label": "Numele utilizatorului",
|
||||
"remember_me": "Amintește-ți de mine",
|
||||
"joining_as": "Vă înscrieți ca:",
|
||||
"join": "Alăturați-vă camerei",
|
||||
"join_user": "",
|
||||
"enter_room": "Intră în cameră",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "Autentificare...",
|
||||
"status_joining": "Intrarea în cameră...",
|
||||
"join_failed": "Nu a reușit să intre în cameră.",
|
||||
"choose_name": ""
|
||||
"join_failed": "Nu a reușit să intre în cameră."
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"new_room": "+ Cameră nouă",
|
||||
|
|
@ -137,8 +129,7 @@
|
|||
"change_name": "Schimbă numele",
|
||||
"set_password": "Setați parola",
|
||||
"temporary_identity": "Această identitate este temporară. Setați o parolă pentru a o utiliza din nou",
|
||||
"title": "Profilul meu",
|
||||
"display_name_required": ""
|
||||
"title": "Profilul meu"
|
||||
},
|
||||
"login": {
|
||||
"login": "Autentificare",
|
||||
|
|
@ -175,9 +166,7 @@
|
|||
"name_room": "Nume cameră",
|
||||
"next": "Următorul",
|
||||
"create": "Creați",
|
||||
"new_room": "Cameră nouă",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"new_room": "Cameră nouă"
|
||||
},
|
||||
"room_welcome": {
|
||||
"got_it": "L-am prins",
|
||||
|
|
@ -197,9 +186,7 @@
|
|||
"purge_redacting_events": "Redactarea evenimentelor",
|
||||
"purge_set_room_state": "Setarea stării camerei",
|
||||
"leave": "Lăsați",
|
||||
"members": "fără membri | 1 membru | {count} membri",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"members": "fără membri | 1 membru | {count} membri"
|
||||
},
|
||||
"message": {
|
||||
"user_changed_guest_access_open": "{user} a permis oaspeților să intre în cameră",
|
||||
|
|
@ -240,10 +227,7 @@
|
|||
"you": "Tu",
|
||||
"reply_image": "Imagine",
|
||||
"reply_audio_message": "Mesaj audio",
|
||||
"reply_video": "Videoclip",
|
||||
"reply_poll": "",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"reply_video": "Videoclip"
|
||||
},
|
||||
"language_display_name": "Engleză",
|
||||
"fallbacks": {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
{
|
||||
"project": {
|
||||
"name": "Convene",
|
||||
"tag_line": ""
|
||||
"name": "Convene"
|
||||
},
|
||||
"menu": {
|
||||
"ok": "හරි",
|
||||
"done": "",
|
||||
"download": "බාගන්න",
|
||||
"edit": "සංස්කරණය",
|
||||
"reply": "පිලිතුර",
|
||||
|
|
@ -22,33 +20,12 @@
|
|||
"message": {
|
||||
"download_progress": "{percentage}% බාගත වී ඇත",
|
||||
"file_prefix": "ගොනුව: ",
|
||||
"reply_image": "",
|
||||
"reply_audio_message": "",
|
||||
"reply_video": "",
|
||||
"reply_poll": "",
|
||||
"you": "ඔබ",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"you": "ඔබ"
|
||||
},
|
||||
"login": {
|
||||
"invalid_message": ""
|
||||
},
|
||||
"join": {
|
||||
"title": "",
|
||||
"title_user": "",
|
||||
"user_name_label": "",
|
||||
"remember_me": "",
|
||||
"joining_as": "",
|
||||
"join": "",
|
||||
"join_user": "",
|
||||
"enter_room": "",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "",
|
||||
"status_joining": "",
|
||||
"join_failed": "",
|
||||
"choose_name": ""
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
{
|
||||
"project": {
|
||||
"name": "Convene",
|
||||
"tag_line": ""
|
||||
"name": "Convene"
|
||||
},
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"language_is_rtl": true,
|
||||
"menu": {
|
||||
"ok": "تامام",
|
||||
"done": "",
|
||||
"download": "چۈشۈرۈش",
|
||||
"delete": "ئۆچۈرۈش",
|
||||
"edit": "تەھرىر",
|
||||
|
|
@ -29,12 +24,12 @@
|
|||
"start_private_chat": "قوللانغۇچى بىلەن شەخسى ئۇچۇرلاشماق"
|
||||
},
|
||||
"message": {
|
||||
"upload_progress_with_total": "{ئومۇمىي} نىڭ {سان} يۈكلەندى",
|
||||
"upload_progress": "يۈكلەندى {سان}",
|
||||
"download_progress": "{پىرسەنت}% چۈشۈرۈلدى",
|
||||
"upload_progress_with_total": "{number} نىڭ {total} يۈكلەندى",
|
||||
"upload_progress": "يۈكلەندى {count}",
|
||||
"download_progress": "{percentage}% چۈشۈرۈلدى",
|
||||
"edited": "تەھرىرلەندى",
|
||||
"file_prefix": "ھۆججەت ",
|
||||
"user_said": "قوللانغۇچى سۆزلىدى:",
|
||||
"user_said": "{user} سۆزلىدى:",
|
||||
"user_left": "قوللانغۇچى مۇنازىرىدىن چېكىندى",
|
||||
"user_joined": "قوللانغۇچى مۇنازىرىغا قاتناشتى",
|
||||
"user_was_invited": "قوللانغۇچى مۇنازىرە ئۆيىگە تەكلىپ قىلىندى...",
|
||||
|
|
@ -45,12 +40,12 @@
|
|||
"user_aliased_room": "مۇنازىرە ئۆيىنىڭ ئىسمى ئۆزگەرتىلدى",
|
||||
"user_created_room": "يېڭى مۇنازىرە ئۆيى قۇرۇلدى",
|
||||
"you": "سىز",
|
||||
"room_powerlevel_change": "{قوللانغۇچى} دەرىجىسىنى ئۆزگەرتىش {ئۆزگەرتىش}",
|
||||
"room_powerlevel_change": "{user} دەرىجىسىنى ئۆزگەرتىش {changes}",
|
||||
"user_changed_guest_access_open": "قوللانغۇچى ئەزالارنىڭ مۇنازىرەخانىغا قوشۇلىشىغا رۇخسەت قىلدى",
|
||||
"user_changed_guest_access_closed": "قوللانغۇچى ئەزالارنىڭ مۇنازىرەخانىغا قوشۇلۇشتىن رەت قىلىندى",
|
||||
"user_powerlevel_change_from_to": "قوللانغۇچى بۇرۇنقى دەرىجىسىدىن يېڭى دەرىجىسىگە كۆتۈرىلدى",
|
||||
"scale_image": "كىچىكلەنمە رەسىم",
|
||||
"users_are_typing": "ئەزالىرى يېزىۋاتىدۇ {نەپەر}",
|
||||
"users_are_typing": "ئەزالىرى يېزىۋاتىدۇ {count}",
|
||||
"user_is_typing": "قوللانغۇچى يېزىۋاتىدۇ",
|
||||
"your_message": "ئۇچۇرىڭىز ...",
|
||||
"replying_to": "{user}",
|
||||
|
|
@ -64,19 +59,13 @@
|
|||
"room_history_invited": "ئەزالار تەكلىپ قىلىنغان ۋاقىتتىن باشلاپ ئوقۇغىلى بولىدۇ",
|
||||
"room_history_shared": "مۇنازىرەخانىدىكى ھەركىم ئوقۇيالايدۇ",
|
||||
"room_history_world_readable": "ھەركىم ئوقۇيالايدۇ",
|
||||
"user_changed_room_history": "قوللانغۇچى» مۇنازىرەخانىنىڭ تارىخىنى قۇردى»",
|
||||
"reply_image": "",
|
||||
"reply_audio_message": "",
|
||||
"reply_video": "",
|
||||
"reply_poll": "",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"user_changed_room_history": "قوللانغۇچى» مۇنازىرەخانىنىڭ تارىخىنى قۇردى»"
|
||||
},
|
||||
"language_display_name": "ئۇيغۇرچە",
|
||||
"new_room": {
|
||||
"link_copied": "ئۇلىنىش كۆچۈرۈلدى!",
|
||||
"status_avatar": "باش سۈرىتى يۈكلىنىۋاتىدۇ: {ئومۇمىي}",
|
||||
"status_avatar_total": "باش سۈرىتىنى يۈكلەش: {ئومۇمىي} نىڭ {سان}",
|
||||
"status_avatar": "باش سۈرىتى يۈكلىنىۋاتىدۇ: {count}",
|
||||
"status_avatar_total": "باش سۈرىتىنى يۈكلەش: {count} نىڭ {total}",
|
||||
"status_creating": "مۇنازىرە ئۆيى قۇرۇڭ",
|
||||
"invite_description": "تىزىملىكتىن تاللاڭ ياكى ھېسابات كىملىكى ئارقىلىق ئىزدەڭ",
|
||||
"invite_info": "پەقەت ئەزالارنى قوشۇش",
|
||||
|
|
@ -91,9 +80,7 @@
|
|||
"name_room": "مۇنازىرەخانىغا ئىسىم قويۇڭ",
|
||||
"next": "كېيىنكى",
|
||||
"create": "قۇرۇش",
|
||||
"new_room": "يېڭى مۇنازىرەخانا",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"new_room": "يېڭى مۇنازىرەخانا"
|
||||
},
|
||||
"room": {
|
||||
"purge_failed": "مۇنازىرەخانىنى يۇيۇش مەغلۇب بولدى!",
|
||||
|
|
@ -103,37 +90,30 @@
|
|||
"purge_redacting_events": "پائالىيەتلەرنى تەھرىرلەش",
|
||||
"purge_set_room_state": "مۇنازىرەخانىنىڭ شەرتىنى قۇرۇش",
|
||||
"leave": "كېتىش",
|
||||
"members": "ئەزالار يوق | بىر ئەزا | [نەپەر] ئەزا",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"members": "ئەزالار يوق | بىر ئەزا | [نەپەر] ئەزا"
|
||||
},
|
||||
"leave": {
|
||||
"text_public_lastroom": "ئەگەر بۇ ئۆيگە يەنە قوشۇلماقچى بولسىڭىز ، يېڭى سالاھىيەت ئاستىدا قاتناشسىڭىز بولىدۇ. {ئىشلەتكۈچى} ، {ھەرىكەت} نى ساقلاش.",
|
||||
"text_public_lastroom": "ئەگەر بۇ ئۆيگە يەنە قوشۇلماقچى بولسىڭىز ، يېڭى سالاھىيەت ئاستىدا قاتناشسىڭىز بولىدۇ. {user} ، {action} نى ساقلاش.",
|
||||
"leave": "كېتىڭ",
|
||||
"go_back": "قايتىڭ",
|
||||
"create_account": "ھېسابات قۇرۇڭ",
|
||||
"text_invite": "بۇ ئۆي قۇلۇپلانغان. ئالاھىدە ئىجازەت ئالماي تۇرۇپ قايتا جەم بولالمايسىز.",
|
||||
"title_invite": "كەتمەكچىمۇ؟",
|
||||
"text_public": "ئۇلىنىشنى بىلسىڭىز ھەمىشە بۇ ئۆيگە قايتا كىرەلەيسىز.",
|
||||
"title_public": "خەير خوش ، {ئىشلەتكۈچى}"
|
||||
"title_public": "خەير خوش ، {user}"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"join": {
|
||||
"title": "{ياتاق ئىسمى} غا خۇش كەپسىز",
|
||||
"title_user": "",
|
||||
"user_name_label": "قوللانغۇچى ئىسمى",
|
||||
"remember_me": "",
|
||||
"joining_as": "سىز تۆۋەندىكىدەك قاتنىشىۋاتىسىز:",
|
||||
"join": "مۇنازىرىگە قوشۇلۇڭ",
|
||||
"join_user": "",
|
||||
"enter_room": "مۇنازىرىگە قوشۇلۇڭ",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "كىرىش ...",
|
||||
"status_joining": "مۇنازىرىگە كىرىش...",
|
||||
"join_failed": "مۇنازىرە ئۆيىگە قوشۇلۇش مەغلۇب بولدى.",
|
||||
"choose_name": ""
|
||||
"status_joining": "مۇنازىرىگە كىرىش...",
|
||||
"status_logging_in": "كىرىش ...",
|
||||
"enter_room": "مۇنازىرىگە قوشۇلۇڭ",
|
||||
"join": "مۇنازىرىگە قوشۇلۇڭ",
|
||||
"joining_as": "سىز تۆۋەندىكىدەك قاتنىشىۋاتىسىز:",
|
||||
"user_name_label": "قوللانغۇچى ئىسمى",
|
||||
"title": "{ياتاق ئىسمى} غا خۇش كەپسىز",
|
||||
"user_name_label": "قوللانغۇچى ئىسمى"
|
||||
},
|
||||
"room_welcome": {
|
||||
"info_permissions": "ياتاق تەڭشىكىدە خالىغان ۋاقىتتا «قوشۇلۇش ئىجازەتنامىسى» نى ئۆزگەرتەلەيسىز.",
|
||||
|
|
@ -141,7 +121,7 @@
|
|||
"join_invite": "سىز تەكلىپ قىلغان كىشىلەرلا قاتناشسا بولىدۇ.",
|
||||
"join_public": "ھەركىم بۇ ئۇلىنىشنى ئېچىش ئارقىلىق قوشۇلالايدۇ: {ئۇلىنىش}.",
|
||||
"room_history_joined": "ئەزالارقوشۇلغاندىن كېيىنلا ئەۋەتىلگەن ئۇچۇرلارنى كۆرەلەيدۇ.",
|
||||
"room_history_is": "مۇنازىرەخانا تارىخى {تىپى}.",
|
||||
"room_history_is": "مۇنازىرەخانا تارىخى {type}.",
|
||||
"encrypted": "ئۇچۇرلار ئاخىرىغىچە مەخپىيلەشتۈرۈلگەن.",
|
||||
"info": "خۇش كەپسىز! مۇنازىرەخانا ھەققىدە بىلىشكە تېگىشلىك بىر قانچە ئىش:"
|
||||
},
|
||||
|
|
@ -171,7 +151,7 @@
|
|||
},
|
||||
"room_info": {
|
||||
"scan_code": "سىكانېرلاپ ئۆيگە قوشۇلۇڭ",
|
||||
"version_info": "قوغدىغۇچى تۈرى تەرىپىدىن ئىشلەنگەن. نەشرى: {نەشرى}",
|
||||
"version_info": "قوغدىغۇچى تۈرى تەرىپىدىن ئىشلەنگەن. نەشرى: {version}",
|
||||
"leave_room": "ئايرىلماق",
|
||||
"show_all": "<ھەممىنى كۆرسەتمەك",
|
||||
"hide_all": "يوشۇرۇن",
|
||||
|
|
@ -193,30 +173,30 @@
|
|||
"room_deleted": "ئۆي ئۆچۈرۈلدى."
|
||||
},
|
||||
"purge_room": {
|
||||
"room_deletion_notice": "خوشلىشىش ۋاقتى! بۇ ئۆي {ئىشلەتكۈچى} تەرىپىدىن ئۆچۈرۈلدى. ئۇ سېكۇنتتا ئۆزىنى ھالاك قىلىدۇ.",
|
||||
"room_deletion_notice": "خوشلىشىش ۋاقتى! بۇ ئۆي {user} تەرىپىدىن ئۆچۈرۈلدى. ئۇ سېكۇنتتا ئۆزىنى ھالاك قىلىدۇ.",
|
||||
"notified": "ئەزالارغا ئۇقتۇرۇش قىلدۇق.",
|
||||
"deleting": "ئۆچۈرۈش ئۆيى:",
|
||||
"self_destruct": "ئۆي سېكۇنتتا ئۆزىنى ھالاك قىلىدۇ.",
|
||||
"n_seconds": "{سېكۇنت} سېكۇنت",
|
||||
"n_seconds": "{seconds} سېكۇنت",
|
||||
"button": "ئۆچۈرۈش",
|
||||
"info": "بارلىق ئەزالار ۋە ئۇچۇرلار ئۆچۈرۈلىدۇ. بۇ ھەرىكەتنى ئەمەلدىن قالدۇرغىلى بولمايدۇ.",
|
||||
"title": "ئۆينى ئۆچۈرەمسىز؟"
|
||||
},
|
||||
"invite": {
|
||||
"status_error": "بىر ياكى بىر نەچچە دوستنى تەكلىپ قىلىش مەغلۇب بولدى!",
|
||||
"status_inviting": "{سان} نىڭ دوستى {كۆرسەتكۈچ} نى تەكلىپ قىلىش",
|
||||
"status_inviting": "{count} نىڭ دوستى {index} نى تەكلىپ قىلىش",
|
||||
"send_invites_to": "تەكلىپ قىلىش",
|
||||
"done": "تاماملاندى",
|
||||
"title": "دوست قوشۇڭ"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"new_room": "+ يېڭى ئۆي",
|
||||
"powered_by": "بۇ مۇنازىرە ئۆيى {مەھسۇلات} ئىشلىتىلگەن.xx دىن تېخىمۇ كۆپ بىلىمگە ئېرىشىڭ ياكى ئىلگىرىلەپ باشقا ئۆي قۇرۇڭ!",
|
||||
"powered_by": "بۇ مۇنازىرە ئۆيى {product} ئىشلىتىلگەن.xx دىن تېخىمۇ كۆپ بىلىمگە ئېرىشىڭ ياكى ئىلگىرىلەپ باشقا ئۆي قۇرۇڭ!",
|
||||
"want_more": "تېخىمۇ كۆپ خالامسىز؟",
|
||||
"logout": "چېكىنىش",
|
||||
"edit_profile": "ئارخىپنى تەھرىرلەش",
|
||||
"identity_temporary": "{كۆرسىتىش ئىسمى}",
|
||||
"identity": "{كۆرسىتىش ئىسمى}",
|
||||
"identity_temporary": "{displayName}",
|
||||
"identity": "{displayName}",
|
||||
"you_are": "سىز"
|
||||
},
|
||||
"profile": {
|
||||
|
|
@ -225,16 +205,11 @@
|
|||
"password_new": "يېڭى پارول",
|
||||
"password_old": "كونا پارول",
|
||||
"select_language": "تىل",
|
||||
"set_language": "",
|
||||
"language_description": "",
|
||||
"dont_see_yours": "",
|
||||
"tell_us": "",
|
||||
"change_password": "پارولنى ئۆزگەرتىڭ",
|
||||
"change_name": "ئىسىم ئۆزگەرتىش",
|
||||
"set_password": "پارول بەلگىلەڭ",
|
||||
"temporary_identity": "بۇ كىملىك ۋاقىتلىق. قايتا ئىشلىتىش ئۈچۈن پارول بەلگىلەڭ",
|
||||
"title": "مېنىڭ ئارخىپىم",
|
||||
"display_name_required": ""
|
||||
"title": "مېنىڭ ئارخىپىم"
|
||||
},
|
||||
"login": {
|
||||
"login": "كىرىش",
|
||||
|
|
@ -242,10 +217,7 @@
|
|||
"username_required": "قوللانغۇچى ئىسمى تەلەپ قىلىنىدۇ",
|
||||
"password": "پارول كىرگۈزۈڭ",
|
||||
"username": "قوللانغۇچى ئىسمى (مەسىلەن: marta)",
|
||||
"title": "كىرىش",
|
||||
"create_room": "",
|
||||
"or": "",
|
||||
"invalid_message": ""
|
||||
"title": "كىرىش"
|
||||
},
|
||||
"device_list": {
|
||||
"not_verified": "دەلىللەنمىدى",
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@
|
|||
"name": "Convene",
|
||||
"tag_line": "只需连接"
|
||||
},
|
||||
"global": {
|
||||
"save": "",
|
||||
"password_didnot_match": "",
|
||||
"password_hint": ""
|
||||
},
|
||||
"fallbacks": {
|
||||
"download_name": "下载",
|
||||
"original_text": "<原文>",
|
||||
|
|
@ -35,7 +30,16 @@
|
|||
"permissions": "加入权限",
|
||||
"created_by": "由 {user} 创建",
|
||||
"copy_link": "复制邀请链接",
|
||||
"scan_code": "扫一扫加入聊天室"
|
||||
"scan_code": "扫一扫加入聊天室",
|
||||
"user_admin": "管理员",
|
||||
"voice_mode": "语音模块",
|
||||
"voice_mode_info": "将聊天界面切换到“收听和录音”模式",
|
||||
"download_chat": "下载聊天",
|
||||
"read_only_room": "只读聊天室",
|
||||
"read_only_room_info": "只允许管理员和版主发送到聊天室",
|
||||
"export_room": "导出聊天",
|
||||
"user_moderator": "版主",
|
||||
"experimental_features": "实验功能"
|
||||
},
|
||||
"leave": {
|
||||
"leave": "离开",
|
||||
|
|
@ -47,9 +51,6 @@
|
|||
"text_public": "如果您知道链接,您可以随时再次加入此聊天室。",
|
||||
"title_public": "再见,{user}"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": ""
|
||||
},
|
||||
"login": {
|
||||
"login": "登录",
|
||||
"password": "输入密码",
|
||||
|
|
@ -59,7 +60,15 @@
|
|||
"username": "用户名 (如: marta)",
|
||||
"create_room": "注册并创建聊天室",
|
||||
"or": "或者",
|
||||
"invalid_message": "用户名或密码无效"
|
||||
"invalid_message": "用户名或密码无效",
|
||||
"resend_verification": "重新发送验证邮件",
|
||||
"sent_verification": "一封电子邮件已发送至 {email}。 请使用您的常用电子邮件客户端来验证地址。",
|
||||
"no_supported_flow": "该应用程序无法登录到给出的服务器",
|
||||
"send_verification": "发送验证邮件",
|
||||
"email_not_valid": "电子邮件地址无效",
|
||||
"terms": "主服务器要求您查看并接受以下政策:",
|
||||
"accept_terms": "接受",
|
||||
"email": "您需要验证您的电子邮件地址"
|
||||
},
|
||||
"device_list": {
|
||||
"title": "设备",
|
||||
|
|
@ -73,11 +82,14 @@
|
|||
"room_list_rooms": "聊天室",
|
||||
"room_list_invites": "邀请",
|
||||
"purge_failed": "删除聊天室失败了!",
|
||||
"purge_removing_members": "移除成员",
|
||||
"purge_redacting_events": "编辑事件",
|
||||
"purge_removing_members": "移除成员({count} 个,共 {total} 个)",
|
||||
"purge_redacting_events": "编辑事件({count} 个,共 {total} 个)",
|
||||
"purge_set_room_state": "设置聊天室状态",
|
||||
"room_name_required": "",
|
||||
"room_topic_required": ""
|
||||
"invitations": "您没有邀请 | 您有 1 个邀请 | 您有 {count} 个邀请",
|
||||
"unseen_messages": "你没有任何未读信息 | 您有 1 条未读信息 | 您有 {count} 条未读信息",
|
||||
"room_list_new_messages": "{count} 条新消息",
|
||||
"room_name_required": "聊天室名称必填",
|
||||
"room_topic_required": "需要聊天室描述"
|
||||
},
|
||||
"message": {
|
||||
"you": "您",
|
||||
|
|
@ -116,12 +128,21 @@
|
|||
"replying_to": "{user}",
|
||||
"user_changed_guest_access_open": "{user} 允许客人加入聊天室",
|
||||
"user_changed_guest_access_closed": "{user} 不允许客人加入聊天室",
|
||||
"reply_image": "",
|
||||
"reply_audio_message": "",
|
||||
"reply_video": "",
|
||||
"reply_poll": "",
|
||||
"outgoing_message_deleted_text": "",
|
||||
"incoming_message_deleted_text": ""
|
||||
"reply_audio_message": "语音留言",
|
||||
"reply_video": "视频",
|
||||
"reply_image": "图片",
|
||||
"user_was_kicked": "{user} 被移除了聊天。",
|
||||
"user_was_kicked_by_you": "您把{user} 移除了聊天室。",
|
||||
"user_was_kicked_you": "你被移除了聊天室。",
|
||||
"user_was_banned": "{user}被移除并禁止聊天。",
|
||||
"user_was_banned_by_you": "您把{user} 移除并禁止聊天。",
|
||||
"user_was_banned_you": "你被移除并禁止聊天。",
|
||||
"reply_poll": "投票",
|
||||
"time_ago": "今天| 昨天 | {count} 天前",
|
||||
"outgoing_message_deleted_text": "你删除了这条信息。",
|
||||
"incoming_message_deleted_text": "这条信息已删除。",
|
||||
"not_allowed_to_send": "只允许管理员和版主发送到聊天室",
|
||||
"reaction_count_more": "{reactionCount} 更多"
|
||||
},
|
||||
"menu": {
|
||||
"login": "登录",
|
||||
|
|
@ -140,7 +161,12 @@
|
|||
"loading": "正在加载 {appName}",
|
||||
"ignore": "忽略",
|
||||
"join": "加入",
|
||||
"undo": "撤销"
|
||||
"undo": "撤销",
|
||||
"user_kick_and_ban": "移出并禁止该用户",
|
||||
"user_kick": "移出该用户",
|
||||
"user_make_admin": "设为管理员",
|
||||
"user_make_moderator": "设为版主",
|
||||
"user_revoke_moderator": "撤销版主身份"
|
||||
},
|
||||
"power_level": {
|
||||
"restricted": "被限制",
|
||||
|
|
@ -180,18 +206,18 @@
|
|||
},
|
||||
"join": {
|
||||
"title": "欢迎您被邀请加入",
|
||||
"title_user": "",
|
||||
"user_name_label": "用户名",
|
||||
"remember_me": "记得我",
|
||||
"joining_as": "您以以下身份加入:",
|
||||
"join": "加入聊天室",
|
||||
"join_user": "",
|
||||
"enter_room": "加入聊天室",
|
||||
"enter_room_user": "",
|
||||
"status_logging_in": "正在登录中...",
|
||||
"status_joining": "正在加入聊天室...",
|
||||
"join_failed": "加入聊天室失败。",
|
||||
"choose_name": ""
|
||||
"title_user": "欢迎您被邀请聊天",
|
||||
"join_user": "开始聊天",
|
||||
"enter_room_user": "开始聊天",
|
||||
"choose_name": "选择要使用的名称"
|
||||
},
|
||||
"profile": {
|
||||
"display_name": "显示名称",
|
||||
|
|
@ -208,7 +234,7 @@
|
|||
"language_description": "Convene 提供多种语言.",
|
||||
"dont_see_yours": "看不到你的?",
|
||||
"tell_us": "告诉我们。",
|
||||
"display_name_required": ""
|
||||
"display_name_required": "显示名称是必需的"
|
||||
},
|
||||
"new_room": {
|
||||
"status_avatar": "正在上传头像:{count}",
|
||||
|
|
@ -224,14 +250,15 @@
|
|||
"join_permissions_info": "这些权限决定了人们如何加入聊天室以及邀请其他人的难易程度。 你可以随时更改它们。",
|
||||
"set_join_permissions": "设置加入权限",
|
||||
"join_permissions": "加入权限",
|
||||
"name_room": "命名聊天室",
|
||||
"name_room": "聊天室名称",
|
||||
"next": "下一步",
|
||||
"done": "完毕",
|
||||
"new_room": "新的聊天室",
|
||||
"room_topic": "如果您愿意,请添加说明",
|
||||
"create": "创建",
|
||||
"room_name_limit_error_msg": "",
|
||||
"colon_not_allowed": ""
|
||||
"colon_not_allowed": "冒号是不允许的",
|
||||
"options": "选项",
|
||||
"room_name_limit_error_msg": "最多允许 50 个字符"
|
||||
},
|
||||
"room_welcome": {
|
||||
"got_it": "知道了",
|
||||
|
|
@ -245,7 +272,7 @@
|
|||
"encrypted": "信息是端到端加密的。"
|
||||
},
|
||||
"profile_info_popup": {
|
||||
"new_room": "+ 新的聊天室",
|
||||
"new_room": "新的聊天室",
|
||||
"powered_by": "这个聊天室由 {product} 提供支持。 在 {productLink} 了解更多信息或继续创建另一个聊天室!",
|
||||
"want_more": "想要更多?",
|
||||
"logout": "退出登录",
|
||||
|
|
@ -259,5 +286,51 @@
|
|||
"close_tab": "关闭浏览器标签",
|
||||
"room_deleted": "聊天室已删除。"
|
||||
},
|
||||
"language_display_name": "简体中文"
|
||||
"language_display_name": "简体中文",
|
||||
"global": {
|
||||
"save": "保存",
|
||||
"password_didnot_match": "密码不匹配",
|
||||
"password_hint": "至少 12 个字符,包含至少一个数字、一个大写字母和一个小写字母",
|
||||
"show_less": "显示较少",
|
||||
"show_more": "展示更多",
|
||||
"add_reaction": "添加反应",
|
||||
"click_to_remove": "点击删除"
|
||||
},
|
||||
"logout": {
|
||||
"confirm_text": "您确定要注销吗?"
|
||||
},
|
||||
"poll_create": {
|
||||
"title": "创建新投票",
|
||||
"create": "发布",
|
||||
"creating": "创建投票",
|
||||
"poll_disclosed": "打开 - 当前结果始终显示。",
|
||||
"answer_label_1": "答案*",
|
||||
"answer_label_n": "答案",
|
||||
"please_complete": "请完成",
|
||||
"tip_title": "专业提示",
|
||||
"tip_text": "成员回答后将看到投票结果。 完成后关闭投票,向聊天室里的每个人展示结果。",
|
||||
"poll_status_open_not_voted": "投票已开始 - 投票以查看结果",
|
||||
"poll_status_open": "投票已开始",
|
||||
"view_results": "查看结果",
|
||||
"poll_submit": "提交",
|
||||
"close_poll": "关闭投票",
|
||||
"results_shared": "结果共享到聊天室。",
|
||||
"question_required": "您需要输入一个问题!",
|
||||
"add_answer": "添加答案",
|
||||
"failed": "创建投票失败,请稍后重试。",
|
||||
"question_label": "问你的问题*",
|
||||
"create_poll_menu_option": "创建投票",
|
||||
"poll_status_closed": "投票结束",
|
||||
"poll_status_disclosed": "结果将在投票结束时显示。",
|
||||
"poll_undisclosed": "关闭 - 用户将在投票关闭时看到结果。",
|
||||
"answer_required": "答案不能为空。 请输入一些文本或删除此选项。",
|
||||
"num_answered": "{count} 答案"
|
||||
},
|
||||
"export": {
|
||||
"fetched_n_events": "获取了 {count} 个事件",
|
||||
"exported_date": "于 {date} 导出",
|
||||
"fetched_n_of_total_events": "已获取 {count} 个事件,共 {total} 个事件",
|
||||
"processed_n_of_total_events": "已处理 {count} 个事件的媒体,共 {total} 个事件",
|
||||
"export_filename": "导出的聊天 {date}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
v-on="$listeners"
|
||||
>
|
||||
<v-col cols="auto" class="me-2">
|
||||
<v-icon size="22">{{ icon }}</v-icon>
|
||||
<v-icon :size="iconSize">{{ icon }}</v-icon>
|
||||
</v-col>
|
||||
<v-col>{{ text }}</v-col>
|
||||
</v-row>
|
||||
|
|
@ -22,6 +22,12 @@ export default {
|
|||
return null;
|
||||
},
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: function() {
|
||||
return 22;
|
||||
}
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: function () {
|
||||
|
|
|
|||
|
|
@ -56,12 +56,14 @@
|
|||
</div>
|
||||
|
||||
<div class="load-later">
|
||||
<v-btn v-if="canRecordAudio" class="mic-button" ref="mic_button" fab small elevation="0" v-blur
|
||||
@click.stop="$emit('start-recording')">
|
||||
<v-btn :class="{'mic-button': true, 'dimmed': !canRecordAudio}" ref="mic_button" fab small elevation="0" v-blur
|
||||
@click.stop="micButtonClicked()">
|
||||
<v-icon color="white">mic</v-icon>
|
||||
</v-btn>
|
||||
<v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon>
|
||||
</div>
|
||||
|
||||
<div v-if="showReadOnlyToast" class="toast-read-only">{{ $t("message.not_allowed_to_send") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -111,6 +113,7 @@ export default {
|
|||
playing: false,
|
||||
analyzer: null,
|
||||
analyzerDataArray: null,
|
||||
showReadOnlyToast: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -162,12 +165,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
canRecordAudio() {
|
||||
if (this.room) {
|
||||
const myUserId = this.$matrix.currentUserId;
|
||||
const me = this.room.getMember(myUserId);
|
||||
return me && me.powerLevelNorm > 0 && util.browserCanRecordAudio();
|
||||
}
|
||||
return false;
|
||||
return !this.$matrix.currentRoomIsReadOnlyForUser && util.browserCanRecordAudio();
|
||||
},
|
||||
currentTime() {
|
||||
return util.formatDuration(this.playTime);
|
||||
|
|
@ -448,6 +446,17 @@ export default {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
micButtonClicked() {
|
||||
if (this.$matrix.currentRoomIsReadOnlyForUser) {
|
||||
this.showReadOnlyToast = true;
|
||||
setTimeout(() => {
|
||||
this.showReadOnlyToast = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
this.$emit('start-recording');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
color="black"
|
||||
@click.stop="onBackgroundClick"
|
||||
class="bottom-sheet-close"
|
||||
v-if="showCloseButton"
|
||||
>
|
||||
<v-icon color="white" >cancel</v-icon>
|
||||
</v-btn>
|
||||
|
|
@ -40,6 +41,10 @@ import Hammer from "hammerjs";
|
|||
|
||||
export default {
|
||||
props: {
|
||||
showCloseButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
openY: {
|
||||
type: Number,
|
||||
default: 0.1,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
<template>
|
||||
<div class="chat-root fill-height d-flex flex-column">
|
||||
<div class="chat-room-invitations clickable" v-if="invitationCount > 0" @click.stop="onInvitationsClick">
|
||||
{{ $tc("room.invitations", invitationCount) }}
|
||||
</div>
|
||||
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" />
|
||||
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" v-on:view-room-details="viewRoomDetails" />
|
||||
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room"
|
||||
:events="events" :autoplay="!showRecorder"
|
||||
:events="events" :autoplay="!showRecorder"
|
||||
:timelineSet="timelineSet"
|
||||
:readMarker="readMarker"
|
||||
:recordingMembers="typingMembers"
|
||||
v-on:start-recording="showRecorder = true"
|
||||
v-on:start-recording="setShowRecorder()"
|
||||
v-on:loadnext="handleScrolledToBottom(false)"
|
||||
v-on:loadprevious="handleScrolledToTop()"
|
||||
v-on:mark-read="sendRR"
|
||||
|
|
@ -45,9 +42,9 @@
|
|||
|
||||
<CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" />
|
||||
|
||||
<div v-for="(event, index) in events" :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 -->
|
||||
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker" :title="dayForEvent(event)" />
|
||||
<div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker" :title="dayForEvent(event)" />
|
||||
|
||||
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
|
||||
<div class="message-wrapper" v-on:touchstart="
|
||||
|
|
@ -55,18 +52,25 @@
|
|||
touchStart(e, event);
|
||||
}
|
||||
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
|
||||
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="events[index + 1]"
|
||||
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]"
|
||||
:timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
|
||||
v-on:context-menu="showContextMenuForEvent($event)" v-on:own-avatar-clicked="viewProfile"
|
||||
v-on:other-avatar-clicked="showAvatarMenuForEvent($event)" v-on:download="download(event)"
|
||||
v-on:poll-closed="pollWasClosed(event)" />
|
||||
v-on:poll-closed="pollWasClosed(event)"
|
||||
v-on:more="
|
||||
isEmojiQuickReaction = true
|
||||
showMoreMessageOperations($event)
|
||||
"
|
||||
/>
|
||||
<!-- <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="event.getId() == readMarker && index < events.length - 1" class="read-marker"
|
||||
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"
|
||||
:title="$t('message.unread_messages')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />
|
||||
</div>
|
||||
|
||||
<!-- Input area -->
|
||||
|
|
@ -109,7 +113,7 @@
|
|||
{{ typingMembersString }}
|
||||
</div>
|
||||
</v-row>
|
||||
<v-row class="input-area-inner align-center">
|
||||
<v-row class="input-area-inner align-center" v-if="!showRecorder && !$matrix.currentRoomIsReadOnlyForUser">
|
||||
<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"
|
||||
no-resize class="input-area-text" :placeholder="$t('message.your_message')" hide-details
|
||||
|
|
@ -126,7 +130,7 @@
|
|||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="(!currentInput || currentInput.length == 0) && !showRecorder && canCreatePoll"
|
||||
<v-col v-if="(!currentInput || currentInput.length == 0) && canCreatePoll && !replyToEvent"
|
||||
class="input-area-button text-center flex-grow-0 flex-shrink-1">
|
||||
<v-btn icon large color="black" @click="showCreatePollDialog = true">
|
||||
<v-icon dark>$vuetify.icons.poll</v-icon>
|
||||
|
|
@ -162,7 +166,7 @@
|
|||
</v-col>
|
||||
|
||||
<v-col v-if="$config.shortCodeStickers" class="input-area-button text-center flex-grow-0 flex-shrink-1">
|
||||
<v-btn v-if="!showRecorder" id="btn-attach" icon large color="black" @click="showStickerPicker"
|
||||
<v-btn id="btn-attach" icon large color="black" @click="showStickerPicker"
|
||||
:disabled="attachButtonDisabled">
|
||||
<v-icon large>face</v-icon>
|
||||
</v-btn>
|
||||
|
|
@ -170,7 +174,7 @@
|
|||
|
||||
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1">
|
||||
<label icon flat ref="attachmentLabel">
|
||||
<v-btn v-if="!showRecorder" icon large color="black" @click="showAttachmentPicker"
|
||||
<v-btn icon large color="black" @click="showAttachmentPicker"
|
||||
:disabled="attachButtonDisabled">
|
||||
<v-icon x-large>add_circle_outline</v-icon>
|
||||
</v-btn>
|
||||
|
|
@ -180,10 +184,11 @@
|
|||
<VoiceRecorder :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" />
|
||||
</div>
|
||||
<div v-if="!useVoiceMode && room && $matrix.currentRoomIsReadOnlyForUser" class="input-area-read-only">{{ $t("message.not_allowed_to_send") }}</div>
|
||||
</v-container>
|
||||
|
||||
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
|
||||
accept="image/*|audio/*|video/*|application/pdf" class="d-none" />
|
||||
accept="image/*, audio/*, video/*, .pdf" class="d-none" />
|
||||
|
||||
<div v-if="currentImageInputPath">
|
||||
<v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'">
|
||||
|
|
@ -257,7 +262,7 @@
|
|||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import { TimelineWindow, EventTimeline, AbortError } from "matrix-js-sdk";
|
||||
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
|
||||
import util from "../plugins/utils";
|
||||
import MessageOperations from "./messages/MessageOperations.vue";
|
||||
import AvatarOperations from "./messages/AvatarOperations.vue";
|
||||
|
|
@ -265,6 +270,7 @@ import ChatHeader from "./ChatHeader";
|
|||
import VoiceRecorder from "./VoiceRecorder";
|
||||
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
|
||||
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
|
||||
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
|
||||
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
|
||||
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
|
||||
import BottomSheet from "./BottomSheet.vue";
|
||||
|
|
@ -314,6 +320,7 @@ export default {
|
|||
VoiceRecorder,
|
||||
RoomInfoBottomSheet,
|
||||
CreatedRoomWelcomeHeader,
|
||||
NoHistoryRoomWelcomeHeader,
|
||||
MessageOperationsBottomSheet,
|
||||
StickerPickerBottomSheet,
|
||||
BottomSheet,
|
||||
|
|
@ -324,6 +331,7 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
waitingForRoomObject: false,
|
||||
events: [],
|
||||
currentInput: "",
|
||||
typingMembers: [],
|
||||
|
|
@ -421,7 +429,6 @@ export default {
|
|||
computed: {
|
||||
chatContainer() {
|
||||
const container = this.$refs.chatContainer;
|
||||
console.log("GOT CONTAINER", container);
|
||||
if (this.useVoiceMode) {
|
||||
return container.$el;
|
||||
}
|
||||
|
|
@ -461,7 +468,7 @@ export default {
|
|||
return this.fullyReadMarker || this.room.getEventReadUpTo(this.$matrix.currentUserId, false);
|
||||
},
|
||||
fullyReadMarker() {
|
||||
const readEvent = this.room.getAccountData("m.fully_read");
|
||||
const readEvent = this.room && this.room.getAccountData("m.fully_read");
|
||||
if (readEvent) {
|
||||
return readEvent.getContent().event_id;
|
||||
}
|
||||
|
|
@ -513,9 +520,6 @@ export default {
|
|||
debugging() {
|
||||
return false; //(window.location.host || "").startsWith("localhost");
|
||||
},
|
||||
invitationCount() {
|
||||
return this.$matrix.invites.length;
|
||||
},
|
||||
canCreatePoll() {
|
||||
// We say that if you can redact events, you are allowed to create polls.
|
||||
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
|
||||
|
|
@ -529,6 +533,34 @@ export default {
|
|||
return util.useVoiceMode(this.room);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* If we have no events and the room is encrypted, show info about this
|
||||
* to the user.
|
||||
*/
|
||||
showNoHistoryRoomWelcomeHeader() {
|
||||
return this.filteredEvents.length == 0 && this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId);
|
||||
},
|
||||
|
||||
filteredEvents() {
|
||||
if (this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId)) {
|
||||
if (this.room.getHistoryVisibility() == "joined") {
|
||||
// For encrypted rooms where history is set to "joined" we can't read old events.
|
||||
// We might, however, have old status events from room creation etc.
|
||||
// We filter out anything that happened before our own join event.
|
||||
for (let idx = this.events.length - 1; idx >= 0; idx--) {
|
||||
const e = this.events[idx];
|
||||
if (e.getType() == "m.room.member" &&
|
||||
e.getContent().membership == "join" &&
|
||||
(!e.getPrevContent() || e.getPrevContent().membership != "join") &&
|
||||
e.getStateKey() == this.$matrix.currentUserId) {
|
||||
// Our own join event.
|
||||
return this.events.slice(idx + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.events;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
@ -552,6 +584,7 @@ export default {
|
|||
this.$matrix.off("Room.timeline", this.onEvent);
|
||||
this.$matrix.off("RoomMember.typing", this.onUserTyping);
|
||||
|
||||
this.waitingForRoomObject = false;
|
||||
this.events = [];
|
||||
this.timelineWindow = null;
|
||||
this.typingMembers = [];
|
||||
|
|
@ -562,27 +595,32 @@ export default {
|
|||
this.stopRRTimer();
|
||||
this.lastRR = null;
|
||||
|
||||
if (!this.room) {
|
||||
// Public room?
|
||||
if (this.roomId && this.roomId.startsWith("#")) {
|
||||
this.onRoomNotJoined();
|
||||
} else if (this.roomId) {
|
||||
this.onRoomNotJoined(); // Private room we are not joined to. What to do? We redirect to join
|
||||
// screen, maybe the user has an invite already?
|
||||
}
|
||||
if (this.roomId) {
|
||||
this.$matrix.isJoinedToRoom(this.roomId).then(joined => {
|
||||
if (!joined) {
|
||||
this.onRoomNotJoined();
|
||||
} else {
|
||||
if (this.room) {
|
||||
this.onRoomJoined(this.readMarker);
|
||||
} else {
|
||||
this.waitingForRoomObject = true;
|
||||
return; // no room, wait for it (we know we are joined so need to wait for sync to complete)
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.initialLoadDone = true;
|
||||
return; // no room
|
||||
}
|
||||
|
||||
// Joined?
|
||||
if (this.room.hasMembershipState(this.currentUser.user_id, "join")) {
|
||||
// Yes, load everything
|
||||
this.onRoomJoined(this.readMarker);
|
||||
} else {
|
||||
this.onRoomNotJoined();
|
||||
}
|
||||
},
|
||||
},
|
||||
room() {
|
||||
// Were we waiting?
|
||||
if (this.room && this.room.roomId == this.roomId && this.waitingForRoomObject) {
|
||||
this.waitingForRoomObject = false;
|
||||
this.onRoomJoined(this.readMarker);
|
||||
}
|
||||
},
|
||||
showMessageOperations() {
|
||||
if (this.showMessageOperations) {
|
||||
this.$nextTick(() => {
|
||||
|
|
@ -597,7 +635,7 @@ export default {
|
|||
var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
|
||||
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
|
||||
top = rectAnchor.top - rectChat.top - 50;
|
||||
left = rectAnchor.left - rectChat.left - 50;
|
||||
left = rectAnchor.left - rectChat.left - 75;
|
||||
if (left + rectOps.width >= rectChat.right) {
|
||||
left = rectChat.right - rectOps.width - 10; // No overflow
|
||||
}
|
||||
|
|
@ -898,7 +936,6 @@ export default {
|
|||
var reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const file = event.target.files[0];
|
||||
this.currentSendShowSendButton = true;
|
||||
if (file.type.startsWith("image/")) {
|
||||
this.currentImageInput = {
|
||||
image: e.target.result,
|
||||
|
|
@ -947,7 +984,15 @@ export default {
|
|||
}
|
||||
}
|
||||
console.log(this.currentImageInput);
|
||||
this.currentImageInputPath = file;
|
||||
this.$matrix.matrixClient.getMediaConfig().then((config) => {
|
||||
this.currentImageInputPath = 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(event.target.files[0]);
|
||||
}
|
||||
|
|
@ -996,7 +1041,7 @@ export default {
|
|||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err instanceof AbortError || err === "Abort") {
|
||||
if (err.name === "AbortError" || err === "Abort") {
|
||||
this.currentSendError = null;
|
||||
} else {
|
||||
this.currentSendError = err.LocaleString();
|
||||
|
|
@ -1436,16 +1481,17 @@ export default {
|
|||
},
|
||||
|
||||
onHeaderClick() {
|
||||
const invitations = this.$matrix.invites.length;
|
||||
const joinedRooms = this.$matrix.joinedRooms;
|
||||
if (joinedRooms && joinedRooms.length == 1 && joinedRooms[0].roomId == this.room.roomId) {
|
||||
if (invitations == 0 && joinedRooms && joinedRooms.length == 1 && joinedRooms[0].roomId == this.room.roomId) {
|
||||
// Only joined to this room, go directly to room details!
|
||||
this.$navigation.push({ name: "RoomInfo" });
|
||||
return;
|
||||
}
|
||||
this.$refs.roomInfoSheet.open();
|
||||
},
|
||||
onInvitationsClick() {
|
||||
this.$navigation.push({ name: "Home" }, -1);
|
||||
viewRoomDetails() {
|
||||
this.$navigation.push({ name: "RoomInfo" });
|
||||
},
|
||||
pollWasClosed(ignoredE) {
|
||||
let div = document.createElement("div");
|
||||
|
|
@ -1455,7 +1501,15 @@ export default {
|
|||
setTimeout(() => {
|
||||
this.chatContainer.parentElement.removeChild(div);
|
||||
}, 3000);
|
||||
},
|
||||
setShowRecorder() {
|
||||
if (this.canRecordAudio) {
|
||||
this.showRecorder = true;
|
||||
} else {
|
||||
this.showNoRecordingAvailableDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,89 +4,81 @@
|
|||
<v-col
|
||||
cols="auto"
|
||||
class="chat-header-members text-start ma-0 pa-0"
|
||||
@click.stop="onHeaderClicked"
|
||||
>
|
||||
<v-avatar size="40" class="me-2">
|
||||
<v-img v-if="room.avatar || memberAvatar" :src="room.avatar || memberAvatar" />
|
||||
<v-avatar size="40" class="clickable me-2 chat-header-avatar" color="grey" @click.stop="onAvatarClicked">
|
||||
<v-img v-if="roomAvatar" :src="roomAvatar" />
|
||||
<span v-else class="white--text headline">{{
|
||||
room.name.substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="chat-header-name ma-0 pa-0 flex-shrink-1 flex-nowrap"
|
||||
@click.stop="onHeaderClicked"
|
||||
>
|
||||
<div class="room-name-inline text-truncate" :title="room.name">
|
||||
{{ room.name }}<v-icon class="icon-dropdown" size="11">$vuetify.icons.ic_dropdown</v-icon><div class="notification-alert" v-if="notifications"></div>
|
||||
|
||||
<!--<v-icon>expand_more</v-icon>-->
|
||||
<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="room.name">
|
||||
{{ room.name }}
|
||||
</div>
|
||||
<v-icon class="icon-dropdown" size="11">$vuetify.icons.ic_dropdown</v-icon>
|
||||
<div :class="{ 'notification-alert': true, 'popup-open': showMissedItemsInfo }" v-if="notifications">
|
||||
<!-- 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">{{ $tc("room.members", memberCount) }}</div>
|
||||
</v-col>
|
||||
<v-col cols="auto" class="text-end ma-0 pa-0">
|
||||
<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>
|
||||
<v-btn
|
||||
id="btn-leave-room"
|
||||
class="mx-2 box-shadow-none"
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="red"
|
||||
@click.stop="leaveRoom"
|
||||
v-else
|
||||
>
|
||||
<v-icon>$vuetify.icons.ic_member-leave</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="auto" class="text-end ma-0 pa-0 ms-2">
|
||||
<v-avatar
|
||||
class="avatar-32 clickable"
|
||||
size="40"
|
||||
color="#e0e0e0"
|
||||
@click.stop="showProfileInfo = true"
|
||||
>
|
||||
<v-col 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>
|
||||
<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>
|
||||
</v-col>
|
||||
<v-col 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="showMoreMenu = true">
|
||||
<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"
|
||||
/>
|
||||
<LeaveRoomDialog :show="showLeaveConfirmation" :room="room" @close="showLeaveConfirmation = false" />
|
||||
|
||||
<!-- PROFILE INFO POPUP -->
|
||||
<ProfileInfoPopup
|
||||
:show="showProfileInfo"
|
||||
@close="showProfileInfo = false"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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 profileInfoMixin from "../components/profileInfoMixin";
|
||||
import PurgeRoomDialog from "../components/PurgeRoomDialog";
|
||||
import RoomExport from "../components/RoomExport";
|
||||
|
||||
import roomInfoMixin from "./roomInfoMixin";
|
||||
|
||||
|
|
@ -96,14 +88,22 @@ export default {
|
|||
components: {
|
||||
LeaveRoomDialog,
|
||||
ProfileInfoPopup,
|
||||
PurgeRoomDialog
|
||||
MoreMenuPopup,
|
||||
PurgeRoomDialog,
|
||||
RoomExport
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
memberCount: null,
|
||||
showLeaveConfirmation: false,
|
||||
showProfileInfo: false,
|
||||
showPurgeConfirmation: false
|
||||
showPurgeConfirmation: false,
|
||||
showMoreMenu: false,
|
||||
downloadingChat: false,
|
||||
showMissedItemsInfo: false,
|
||||
|
||||
/** Timer for showing the "missed items" hint */
|
||||
timerMissedItems: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -123,8 +123,8 @@ export default {
|
|||
let roomMember;
|
||||
if (this.room) {
|
||||
this.room.getMembers().forEach(member => {
|
||||
if(this.room.name === member.name) {
|
||||
roomMember = member;
|
||||
if (this.room.name === member.name) {
|
||||
roomMember = member;
|
||||
}
|
||||
});
|
||||
if (roomMember) {
|
||||
|
|
@ -140,8 +140,68 @@ export default {
|
|||
return null;
|
||||
},
|
||||
notifications() {
|
||||
return this.$matrix.joinedRooms.some(room => room.roomId !== this.$matrix.currentRoomId && room.getUnreadNotificationCount("total") > 0);
|
||||
}
|
||||
return this.$matrix.joinedRooms.some(r => (r.roomId !== this.$matrix.currentRoomId && r.getCanonicalAlias() !== this.$matrix.currentRoomId) && r.getUnreadNotificationCount("total") > 0) ||
|
||||
this.$matrix.invites.length > 0;
|
||||
},
|
||||
notificationsText() {
|
||||
const invitationCount = this.$matrix.invites.length
|
||||
if (invitationCount > 0) {
|
||||
return this.$tc('room.invitations', invitationCount);
|
||||
}
|
||||
const missedMessagesCount = this.$matrix.joinedRooms.reduce((value, r) => ((r.roomId !== this.$matrix.currentRoomId && r.getCanonicalAlias() !== this.$matrix.currentRoomId) ? (value + r.getUnreadNotificationCount("total")) : value), 0);
|
||||
if (missedMessagesCount > 0) {
|
||||
return this.$tc('room.unseen_messages', missedMessagesCount);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
moreMenuItems() {
|
||||
let items = [];
|
||||
const roomLink = this.publicRoomLink;
|
||||
if (roomLink) {
|
||||
items.push({
|
||||
icon: '$vuetify.icons.ic_link', text: this.$t('room_info.copy_link'), handler: () => {
|
||||
this.$copyText(this.publicRoomLink);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.userCanExportChat) {
|
||||
items.push({
|
||||
icon: '$vuetify.icons.ic_download', text: this.$t('room_info.download_chat'), handler: () => {
|
||||
this.downloadingChat = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
icon: '$vuetify.icons.ic_info', text: this.$t('room_info.title'), handler: () => {
|
||||
this.$emit("view-room-details", { event: this.event });
|
||||
}
|
||||
});
|
||||
items.push({
|
||||
icon: '$vuetify.icons.ic_member-leave', text: this.$t('leave.leave'), handler: () => {
|
||||
this.leaveRoom();
|
||||
}
|
||||
});
|
||||
return items;
|
||||
},
|
||||
roomAvatar() {
|
||||
const room = this.room;
|
||||
if (this.$matrix.isDirectRoom(room)) {
|
||||
if (room.avatar) {
|
||||
return room.avatar;
|
||||
}
|
||||
const membersNotMe = room.getMembers().filter(m => m.userId != this.$matrix.currentUserId);
|
||||
if (membersNotMe && membersNotMe.length == 1) {
|
||||
return membersNotMe[0].getAvatarUrl(
|
||||
this.$matrix.matrixClient.getHomeserverUrl(),
|
||||
40,
|
||||
40,
|
||||
"scale",
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
return room.avatar;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
room: {
|
||||
|
|
@ -153,9 +213,23 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (this.$store.state.hasShownMissedItemsHint !== "1" && val > 0 && !this.showMissedItemsInfo && this.timerMissedItems == null) {
|
||||
this.timerMissedItems = setTimeout(() => {
|
||||
this.showMissedItemsInfo = true;
|
||||
}, 3500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setHasShownMissedItemsHint() {
|
||||
this.$store.commit('setHasShownMissedItemsHint', "1");
|
||||
this.showMissedItemsInfo = false;
|
||||
},
|
||||
onEvent(event) {
|
||||
if (!this.room || event.getRoomId() !== this.room.roomId) {
|
||||
return; // Not for this room
|
||||
|
|
@ -169,6 +243,10 @@ export default {
|
|||
this.$emit("header-click", { event: this.event });
|
||||
},
|
||||
|
||||
onAvatarClicked() {
|
||||
this.$emit("view-room-details", { event: this.event });
|
||||
},
|
||||
|
||||
updateMemberCount() {
|
||||
if (!this.room) {
|
||||
this.memberCount = 0;
|
||||
|
|
@ -186,4 +264,45 @@ export default {
|
|||
|
||||
<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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -39,17 +39,27 @@
|
|||
background-color="white" v-on:keyup.enter="$refs.topic.focus()" :disabled="step > steps.INITIAL" autofocus
|
||||
solo @update:error="updateErrorState"></v-text-field>
|
||||
<div class="text-left font-weight-light" v-show="roomName.length > 0">{{ $t("new_room.room_topic") }}</div>
|
||||
<v-text-field v-model="roomTopic" v-show="roomName.length > 0" color="black" background-color="white"
|
||||
v-on:keyup.enter="$refs.create.focus()" :disabled="step > steps.INITIAL" solo></v-text-field>
|
||||
<v-text-field v-model="roomTopic" v-show="roomName.length > 0" ref="topic" color="black" background-color="white"
|
||||
v-on:keyup.enter="$refs.create.$el.focus()" :disabled="step > steps.INITIAL" solo></v-text-field>
|
||||
|
||||
<!-- Our only option right now is voice mode, so if not enabled, hide the 'options' drop down as well -->
|
||||
<template v-if="$config.experimental_voice_mode">
|
||||
<template v-if="$config.experimental_voice_mode || $config.experimental_read_only_room || $config.experimental_public_room">
|
||||
<div @click.stop="showOptions = !showOptions" v-show="roomName.length > 0" class="options clickable">
|
||||
<div>{{ $t("new_room.options") }}</div>
|
||||
<v-icon v-if="!showOptions">expand_more</v-icon>
|
||||
<v-icon v-else>expand_less</v-icon>
|
||||
</div>
|
||||
<v-card v-show="showOptions" class="account ma-3" flat>
|
||||
<v-card v-if="$config.experimental_public_room" v-show="showOptions" class="room-option account ma-0" flat>
|
||||
<v-card-text class="with-right-label">
|
||||
<div>
|
||||
<div class="option-title">{{ $t('room_info.make_public') }}</div>
|
||||
<!-- <div class="option-text">{{ $t('room_info.read_only_room_info') }}</div> -->
|
||||
</div>
|
||||
<v-switch v-model="unencryptedRoom"></v-switch>
|
||||
</v-card-text>
|
||||
<div class="option-warning" v-if="unencryptedRoom"><v-icon size="18">$vuetify.icons.ic_warning</v-icon>{{ $t("room_info.make_public_warning")}}</div>
|
||||
</v-card>
|
||||
<v-card v-if="$config.experimental_voice_mode" v-show="showOptions" class="room-option account ma-0" flat>
|
||||
<v-card-text class="with-right-label">
|
||||
<div>
|
||||
<div class="option-title">{{ $t('room_info.voice_mode') }}</div>
|
||||
|
|
@ -58,11 +68,20 @@
|
|||
<v-switch v-model="useVoiceMode"></v-switch>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card v-if="$config.experimental_read_only_room" v-show="showOptions" class="room-option account ma-0" flat>
|
||||
<v-card-text class="with-right-label">
|
||||
<div>
|
||||
<div class="option-title">{{ $t('room_info.read_only_room') }}</div>
|
||||
<div class="option-text">{{ $t('room_info.read_only_room_info') }}</div>
|
||||
</div>
|
||||
<v-switch v-model="readOnlyRoom"></v-switch>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<div class="error--text" v-if="roomCreationErrorMsg"> {{ roomCreationErrorMsg }}</div>
|
||||
<v-btn id="btn-room-create" color="black" depressed class="filled-button" @click.stop="onCreate"
|
||||
:disabled="isDisabled">
|
||||
:disabled="isDisabled" ref="create">
|
||||
<div v-if="status && !enterRoomDialog" class="text-center">
|
||||
{{ status }}
|
||||
<v-progress-circular v-if="step == steps.CREATING" indeterminate color="primary"
|
||||
|
|
@ -74,91 +93,8 @@
|
|||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-fade-transition>
|
||||
<!-- <div class="section ma-3" flat v-if="step > steps.INITIAL"> -->
|
||||
<!-- <div class="h4 text-left">{{ $t("new_room.join_permissions") }}</div>
|
||||
<div class="h2 text-left">
|
||||
{{ $t("new_room.set_join_permissions") }}
|
||||
</div>
|
||||
<div>{{ $t("new_room.join_permissions_info") }}</div>
|
||||
<v-select
|
||||
:disabled="step >= steps.CREATING"
|
||||
:items="joinRules"
|
||||
class="mt-4"
|
||||
v-model="joinRule"
|
||||
item-value="id"
|
||||
>
|
||||
<template v-slot:selection="{ item }">
|
||||
{{ item.text }}
|
||||
</template>
|
||||
<template v-slot:item="{ item, attrs, on }">
|
||||
<v-list-item v-on="on" v-bind="attrs" #default="{ active }">
|
||||
<v-list-item-avatar>
|
||||
<v-icon class="grey lighten-1" dark>{{ item.icon }}</v-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.text"></v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
v-text="item.descr"
|
||||
></v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn icon v-if="active">
|
||||
<v-icon color="grey lighten-1">check</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<interactive-auth ref="interactiveAuth" />
|
||||
|
||||
<v-divider style="margin-bottom: 20px" />
|
||||
|
||||
<v-text-field
|
||||
v-if="publicRoomLink"
|
||||
:value="publicRoomLink"
|
||||
class="room-link"
|
||||
readonly
|
||||
filled
|
||||
background-color="transparent"
|
||||
append-icon="content_copy"
|
||||
type="text"
|
||||
@click:append.stop="copyRoomLink"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
v-else-if="joinRule == 'public'"
|
||||
:loading="step == steps.CREATING"
|
||||
block
|
||||
depressed
|
||||
class="outlined-button"
|
||||
@click.stop="getPublicLink"
|
||||
><v-icon class="me-2">link</v-icon
|
||||
>{{ $t("new_room.get_link") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-else-if="joinRule == 'invite'"
|
||||
block
|
||||
depressed
|
||||
class="outlined-button"
|
||||
@click.stop="addPeople"
|
||||
><v-icon class="me-2">person_add</v-icon
|
||||
>{{ $t("new_room.add_people") }}</v-btn
|
||||
>
|
||||
|
||||
<div v-if="publicRoomLinkCopied" class="link-copied">
|
||||
{{ $t("new_room.link_copied") }}
|
||||
</div>
|
||||
-->
|
||||
<!-- <div v-if="status && !enterRoomDialog" class="text-center">
|
||||
<v-progress-circular
|
||||
v-if="step == steps.CREATING"
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="20"
|
||||
></v-progress-circular>
|
||||
{{ status }}
|
||||
</div> -->
|
||||
<!-- </div> -->
|
||||
</v-fade-transition>
|
||||
<input id="room-avatar-picker" ref="avatar" type="file" name="avatar" @change="handlePickedAvatar($event)"
|
||||
accept="image/*" class="d-none" />
|
||||
<v-dialog v-model="enterRoomDialog" :width="$vuetify.breakpoint.smAndUp ? '50%' : '90%'">
|
||||
|
|
@ -205,6 +141,7 @@
|
|||
|
||||
<script>
|
||||
import util, { ROOM_TYPE_VOICE_MODE } from "../plugins/utils";
|
||||
import InteractiveAuth from './InteractiveAuth.vue';
|
||||
import rememberMeMixin from "./rememberMeMixin";
|
||||
|
||||
const steps = Object.freeze({
|
||||
|
|
@ -216,6 +153,7 @@ const steps = Object.freeze({
|
|||
|
||||
export default {
|
||||
name: "CreateRoom",
|
||||
components: { InteractiveAuth },
|
||||
mixins: [rememberMeMixin],
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -258,7 +196,9 @@ export default {
|
|||
roomNameHasError: false,
|
||||
roomCreationErrorMsg: "",
|
||||
showOptions: false,
|
||||
unencryptedRoom: false,
|
||||
useVoiceMode: false,
|
||||
readOnlyRoom: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -296,7 +236,7 @@ export default {
|
|||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -387,7 +327,17 @@ export default {
|
|||
visibility: "private", // Not listed!
|
||||
name: this.roomName,
|
||||
preset: "public_chat",
|
||||
initial_state: [
|
||||
initial_state:
|
||||
this.unencryptedRoom ? [
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: "shared"
|
||||
}
|
||||
}
|
||||
] :
|
||||
[
|
||||
{
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
|
|
@ -401,7 +351,7 @@ export default {
|
|||
content: {
|
||||
history_visibility: "joined"
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
|
|
@ -446,7 +396,7 @@ export default {
|
|||
}
|
||||
|
||||
return this.$matrix
|
||||
.getLoginPromise()
|
||||
.getLoginPromise(this.$refs.interactiveAuth.registrationFlowHandler)
|
||||
.then(
|
||||
function (user) {
|
||||
if (user.is_guest && !hasUser) {
|
||||
|
|
@ -516,6 +466,20 @@ export default {
|
|||
}
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
// Set power level event. Need to do that here, because we might not have the userId when the options object is created.
|
||||
const powerLevels = {};
|
||||
powerLevels[this.$matrix.currentUserId] = 100;
|
||||
createRoomOptions.initial_state.push(
|
||||
{
|
||||
type: "m.room.power_levels",
|
||||
state_key: "",
|
||||
content: {
|
||||
users: powerLevels,
|
||||
events_default: this.readOnlyRoom ? 50 : 0
|
||||
}
|
||||
});
|
||||
|
||||
return this.$matrix.matrixClient
|
||||
.createRoom(createRoomOptions)
|
||||
.then(({ room_id, room_alias }) => {
|
||||
|
|
@ -604,7 +568,7 @@ export default {
|
|||
showAvatarPickerList() {
|
||||
this.$refs.avatar.$refs.input.click();
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
<div class="mt-2" v-if="roomHistoryDescription">
|
||||
{{ roomHistoryDescription }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-end">
|
||||
<v-btn id="btn-got-it" text @click.stop="$emit('close')" class="text-transform-0">
|
||||
{{ $t("room_welcome.got_it") }}
|
||||
</v-btn>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<v-container fluid class="text-center mt-8">
|
||||
<v-row align="center" justify="center">
|
||||
<v-col class="text-center" cols="auto">
|
||||
<v-img contain src="@/assets/logo.svg" width="64" height="64" />
|
||||
<v-img contain :src="logotype" width="64" height="64" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
|
@ -15,8 +15,6 @@
|
|||
<RoomList
|
||||
showInvites
|
||||
showCreate
|
||||
title=""
|
||||
:invitesTitle="$t('room.room_list_invites')"
|
||||
v-on:newroom="createRoom"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
|
@ -44,12 +42,13 @@
|
|||
<script>
|
||||
import RoomList from "../components/RoomList";
|
||||
import YouAre from "../components/YouAre.vue";
|
||||
|
||||
import logoMixin from "../components/logoMixin";
|
||||
export default {
|
||||
components: {
|
||||
RoomList,
|
||||
YouAre,
|
||||
},
|
||||
mixins: [logoMixin],
|
||||
computed: {
|
||||
loading() {
|
||||
return !this.$matrix.ready;
|
||||
|
|
|
|||
|
|
@ -160,6 +160,11 @@ export default {
|
|||
position: absolute;
|
||||
right: 16px;
|
||||
align-self: center;
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: 16px;
|
||||
right: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
293
src/components/InteractiveAuth.vue
Normal file
293
src/components/InteractiveAuth.vue
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
<template>
|
||||
<v-fade-transition>
|
||||
<v-container fluid class="mt-40" v-if="step == steps.CAPTCHA">
|
||||
<div class="ma-3" flat ref="captcha" id="captcha">
|
||||
</div>
|
||||
</v-container>
|
||||
|
||||
<v-container fluid class="mt-40" v-if="step == steps.ENTER_EMAIL">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-left font-weight-light">{{ $t("login.email") }}</div>
|
||||
<v-text-field v-model="email" color="black" :rules="emailRules" type="email" maxlength="200"
|
||||
background-color="white" v-on:keyup.enter="onEmailEntered(email)" autofocus solo></v-text-field>
|
||||
<v-btn :disabled="!emailIsValid" color="black" depressed class="filled-button"
|
||||
@click.stop="onEmailEntered(email)">
|
||||
{{ $t("login.send_verification") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-container fluid class="mt-40" v-if="step == steps.TERMS">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-left font-weight-light">{{ $t("login.terms") }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div v-for="(policy) in this.policies" :key="policy.id">
|
||||
<v-checkbox class="mt-0" v-model="policy.accepted">
|
||||
<template v-slot:label>
|
||||
<a target="_blank" :href="policy.url" @click.stop>{{ policy.name }}
|
||||
</a>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<v-btn color="black" :disabled="!this.allPoliciesAccepted" depressed class="filled-button"
|
||||
@click.stop="onPoliciesAccepted()">
|
||||
{{ $t("login.accept_terms") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-container fluid class="mt-40" v-if="step == steps.AWAITING_EMAIL_VERIFICATION">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div>
|
||||
<div class="text-left font-weight-light">{{ $t("login.sent_verification", { email: this.email }) }}</div>
|
||||
<v-progress-circular style="display: inline-flex" indeterminate color="primary"
|
||||
size="20"></v-progress-circular>
|
||||
</div>
|
||||
<v-btn color="black" depressed class="filled-button" @click.stop="onEmailResend()">
|
||||
{{ $t("login.resend_verification") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-fade-transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import util from "../plugins/utils";
|
||||
|
||||
const steps = Object.freeze({
|
||||
INITIAL: 0,
|
||||
CAPTCHA: 1,
|
||||
TERMS: 2,
|
||||
EXTERNAL_AUTH: 3,
|
||||
ENTER_EMAIL: 4,
|
||||
AWAITING_EMAIL_VERIFICATION: 5,
|
||||
});
|
||||
|
||||
export default {
|
||||
name: "InteractiveAuth",
|
||||
data() {
|
||||
return {
|
||||
steps,
|
||||
step: steps.INITIAL,
|
||||
emailRules: [
|
||||
v => this.validEmail(v) || this.$t("login.email_not_valid")
|
||||
],
|
||||
policies: null,
|
||||
onPoliciesAccepted: () => { },
|
||||
onEmailResend: () => { },
|
||||
onEmailVerify: () => { },
|
||||
email: "",
|
||||
emailVerification: "",
|
||||
emailVerificationSecret: util.randomUser("tokn"),
|
||||
emailVerificationAttempt: 1,
|
||||
emailVerificationSid: null,
|
||||
emailVerificationPollTimer: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
allPoliciesAccepted() {
|
||||
return Object.keys(this.policies).every(id => this.policies[id].accepted);
|
||||
},
|
||||
emailIsValid() {
|
||||
return this.validEmail(this.email);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
validEmail(email) {
|
||||
if (/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(.\w{2,3})+$/.test(email)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
registrationFlowHandler(client, authData) {
|
||||
const flows = authData.flows;
|
||||
if (!flows || flows.length == 0) {
|
||||
return Promise.reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
const currentFlow = flows[0];
|
||||
if (!currentFlow || !currentFlow.stages || currentFlow.stages.length == 0) {
|
||||
return Promise.reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
|
||||
// TODO - For now hardcoded to flows[0]
|
||||
const completedStages = authData.completed || [];
|
||||
const remainingStages = currentFlow.stages.filter((stage) => {
|
||||
return !completedStages.includes(stage);
|
||||
});
|
||||
|
||||
const nextStage = remainingStages[0];
|
||||
|
||||
const submitStageData = (resolve, reject, data) => {
|
||||
this.step = steps.CREATING;
|
||||
resolve(client.registerRequest({ auth: data }).catch(err => {
|
||||
if (err.httpStatus == 401 && err.data) {
|
||||
// Start next stage!
|
||||
return this.registrationFlowHandler(client, err.data);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
));
|
||||
};
|
||||
|
||||
switch (nextStage) {
|
||||
case "m.login.recaptcha":
|
||||
this.step = steps.CAPTCHA;
|
||||
return new Promise((resolve, reject) => {
|
||||
let script = document.createElement("script");
|
||||
script.src = "https://www.google.com/recaptcha/api.js?onload=onReCaptchaLoaded&render=explicit";
|
||||
window.onReCaptchaLoaded = (ignoredev) => {
|
||||
const params = authData.params[nextStage];
|
||||
if (!params || !params.public_key) {
|
||||
return reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
const site_key = params.public_key;
|
||||
window.grecaptcha.render('captcha', {
|
||||
'sitekey': site_key,
|
||||
'theme': 'light',
|
||||
callback: (captcha_response) => {
|
||||
const data = {
|
||||
session: authData.session,
|
||||
type: nextStage,
|
||||
response: captcha_response
|
||||
};
|
||||
submitStageData(resolve, reject, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
document.body.append(script);
|
||||
});
|
||||
case "m.login.terms":
|
||||
this.step = steps.TERMS;
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = authData.params[nextStage];
|
||||
if (!params || !params.policies) {
|
||||
return reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
|
||||
let policies = [];
|
||||
Object.keys(params.policies).forEach((policyId) => {
|
||||
const policy = params.policies[policyId];
|
||||
let localized = policy[this.$i18n.locale] || policy["en"] || policy[Object.keys(policy).filter(k => k != "version")[0]];
|
||||
if (!localized) {
|
||||
// Did not find info for this policy!
|
||||
return reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
policies.push({
|
||||
id: policyId,
|
||||
name: localized.name,
|
||||
url: localized.url,
|
||||
accepted: false
|
||||
});
|
||||
});
|
||||
this.onPoliciesAccepted = () => {
|
||||
// Button should not be enabled if not all are accepted, but double check here...
|
||||
if (this.allPoliciesAccepted) {
|
||||
const data = {
|
||||
session: authData.session,
|
||||
type: nextStage,
|
||||
};
|
||||
submitStageData(resolve, reject, data);
|
||||
}
|
||||
}
|
||||
this.policies = policies;
|
||||
});
|
||||
|
||||
case "m.login.email.identity":
|
||||
this.step = steps.ENTER_EMAIL;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.onEmailEntered = (email) => {
|
||||
this.step = steps.CREATING;
|
||||
this.emailVerificationAttempt = 1;
|
||||
client.requestRegisterEmailToken(email, this.emailVerificationSecret, this.emailVerificationAttempt, null)
|
||||
.then(response => {
|
||||
this.emailVerificationSid = response.sid;
|
||||
this.step = steps.AWAITING_EMAIL_VERIFICATION;
|
||||
this.onEmailResend = () => {
|
||||
this.emailVerificationAttempt += 1;
|
||||
client.requestRegisterEmailToken(email, this.emailVerificationSecret, this.emailVerificationAttempt, null)
|
||||
.then(response => {
|
||||
this.emailVerificationSid = response.sid; // Update the SID (TODO: will it really change?)
|
||||
})
|
||||
.catch(ignorederr => { });
|
||||
},
|
||||
this.onEmailVerify = async () => {
|
||||
const data = {
|
||||
type: nextStage,
|
||||
threepid_creds: {
|
||||
sid: this.emailVerificationSid,
|
||||
client_secret: this.emailVerificationSecret
|
||||
},
|
||||
session: authData.session,
|
||||
};
|
||||
console.log("Polling...");
|
||||
await client.registerRequest({ auth: data })
|
||||
.then((result) => {
|
||||
// Yes, email is verified!
|
||||
clearTimeout(this.emailVerificationPollTimer);
|
||||
this.emailVerificationPollTimer = null;
|
||||
resolve(result);
|
||||
})
|
||||
.catch(ignorederr => {
|
||||
// Continue polling
|
||||
});
|
||||
};
|
||||
this.emailVerificationPollTimer = setInterval(this.onEmailVerify, 5000);
|
||||
})
|
||||
.catch((ignorederr) => {
|
||||
console.error("ERROR", ignorederr);
|
||||
return reject(this.$t('login.no_supported_flow'))
|
||||
});
|
||||
}
|
||||
});
|
||||
default:
|
||||
this.step = steps.EXTERNAL_AUTH;
|
||||
return new Promise((resolve, reject) => {
|
||||
let fallbackUrl = client.getFallbackAuthUrl(nextStage, authData.session);
|
||||
var popupWindow;
|
||||
|
||||
var eventListener = function (ev) {
|
||||
// check it's the right message from the right place.
|
||||
if (ev.data !== "authDone" || !fallbackUrl.startsWith(ev.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// close the popup
|
||||
popupWindow.close();
|
||||
window.removeEventListener("message", eventListener);
|
||||
|
||||
const data = {
|
||||
session: authData.session,
|
||||
};
|
||||
submitStageData(resolve, reject, data);
|
||||
};
|
||||
|
||||
window.addEventListener("message", eventListener);
|
||||
popupWindow = window.open(fallbackUrl, "_blank", "popup=true");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
|
|
@ -80,6 +80,8 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<interactive-auth ref="interactiveAuth" />
|
||||
|
||||
<v-btn id="btn-join" class="btn-dark" large @click.stop="handleJoin" :loading="loading" v-if="!currentUser">{{
|
||||
roomId && roomId.startsWith("@") ? $t("join.enter_room_user") : $t("join.enter_room")
|
||||
}}</v-btn>
|
||||
|
|
@ -121,7 +123,7 @@
|
|||
|
||||
<div class="d-flex justify-center align-center mt-9">
|
||||
<div class="mr-2">
|
||||
<img src="@/assets/logo.svg" width="32" height="32" contain class="d-inline" />
|
||||
<img :src="logotype" width="32" height="32" contain class="d-inline" />
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ $t("project.name") }}</strong>
|
||||
|
|
@ -133,16 +135,18 @@
|
|||
|
||||
<script>
|
||||
import util from "../plugins/utils";
|
||||
import InteractiveAuth from './InteractiveAuth.vue';
|
||||
import LanguageMixin from "./languageMixin";
|
||||
import rememberMeMixin from "./rememberMeMixin";
|
||||
|
||||
import logoMixin from "./logoMixin";
|
||||
import SelectLanguageDialog from "./SelectLanguageDialog.vue";
|
||||
|
||||
export default {
|
||||
name: "Join",
|
||||
mixins: [LanguageMixin, rememberMeMixin],
|
||||
mixins: [LanguageMixin, rememberMeMixin, logoMixin],
|
||||
components: {
|
||||
SelectLanguageDialog,
|
||||
InteractiveAuth
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -269,7 +273,10 @@ export default {
|
|||
if (roomName && roomName.startsWith("@")) {
|
||||
return roomName.substring(roomName.indexOf("@") + 1, lastIndex);
|
||||
}
|
||||
return roomName ? roomName.substring(roomName.indexOf("#") + 1, lastIndex) : "";
|
||||
if (roomName && roomName.startsWith("#")) {
|
||||
return roomName.substring(roomName.indexOf("#") + 1, lastIndex);
|
||||
}
|
||||
return roomName ? roomName : "";
|
||||
},
|
||||
getRoomInfo() {
|
||||
if (this.roomId.startsWith("#")) {
|
||||
|
|
@ -306,6 +313,8 @@ export default {
|
|||
const room = this.$matrix.getRoom(this.roomId);
|
||||
if (room) {
|
||||
this.roomName = this.removeHomeServer(room.name || this.roomName);
|
||||
} else {
|
||||
this.roomName = this.removeHomeServer(this.roomAliasOrId);
|
||||
}
|
||||
this.waitingForInfo = false;
|
||||
}
|
||||
|
|
@ -335,7 +344,7 @@ export default {
|
|||
const hasUser = this.currentUser ? true : false;
|
||||
var setProfileData = false;
|
||||
return this.$matrix
|
||||
.getLoginPromise()
|
||||
.getLoginPromise(this.$refs.interactiveAuth.registrationFlowHandler)
|
||||
.then(
|
||||
function (user) {
|
||||
if (user.is_guest && !hasUser) {
|
||||
|
|
@ -385,14 +394,14 @@ export default {
|
|||
return this.$matrix.matrixClient.joinRoom(this.roomId);
|
||||
}
|
||||
})
|
||||
.then((ignoredRoom) => {
|
||||
.then((room) => {
|
||||
this.loading = false;
|
||||
this.loadingMessage = null;
|
||||
this.$nextTick(() => {
|
||||
this.$navigation.push(
|
||||
{
|
||||
name: "Chat",
|
||||
params: { roomId: util.sanitizeRoomId(this.roomAliasOrId) },
|
||||
params: { roomId: util.sanitizeRoomId(room.roomId) },
|
||||
},
|
||||
-1
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-img
|
||||
src="@/assets/logo.svg"
|
||||
:src="logotype"
|
||||
width="32"
|
||||
height="32"
|
||||
contain
|
||||
|
|
@ -37,10 +37,15 @@
|
|||
:error="userErrorMessage != null"
|
||||
:error-messages="userErrorMessage"
|
||||
required
|
||||
v-on:keyup.enter="$refs.password.focus()"
|
||||
v-on:keyup.enter="onUsernameEnter"
|
||||
v-on:keydown="hasError=false"
|
||||
v-on:blur="onUsernameBlur"
|
||||
></v-text-field>
|
||||
|
||||
<div class="error--text" v-if="loadingLoginFlows">Loading login flows...</div>
|
||||
|
||||
<v-text-field
|
||||
v-show="showPasswordField"
|
||||
prepend-inner-icon="$vuetify.icons.password"
|
||||
ref="password"
|
||||
v-model="user.password"
|
||||
|
|
@ -72,7 +77,7 @@
|
|||
/>
|
||||
<v-btn
|
||||
id="btn-login"
|
||||
:disabled="!isValid || loading"
|
||||
:disabled="!isValid || loading || loadingLoginFlows"
|
||||
color="black"
|
||||
depressed
|
||||
block
|
||||
|
|
@ -100,10 +105,12 @@
|
|||
import User from "../models/user";
|
||||
import util from "../plugins/utils";
|
||||
import rememberMeMixin from "./rememberMeMixin";
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
import logoMixin from "./logoMixin";
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
mixins:[rememberMeMixin],
|
||||
mixins:[rememberMeMixin, logoMixin],
|
||||
data() {
|
||||
return {
|
||||
user: new User(this.$config.defaultServer, "", ""),
|
||||
|
|
@ -112,7 +119,11 @@ export default {
|
|||
message: "",
|
||||
userErrorMessage: null,
|
||||
passErrorMessage: null,
|
||||
hasError: false
|
||||
hasError: false,
|
||||
currentLoginServer: "",
|
||||
loadingLoginFlows: false,
|
||||
loginFlows: null,
|
||||
showPasswordField: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -124,7 +135,7 @@ export default {
|
|||
},
|
||||
showCloseButton() {
|
||||
return this.$navigation && this.$navigation.canPop();
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.loggedIn) {
|
||||
|
|
@ -158,7 +169,7 @@ export default {
|
|||
user.normalize();
|
||||
|
||||
this.loading = true;
|
||||
this.$store.dispatch("login", user).then(
|
||||
this.$store.dispatch("login", { user }).then(
|
||||
() => {
|
||||
if (this.$matrix.currentRoomId) {
|
||||
this.$navigation.push(
|
||||
|
|
@ -175,12 +186,13 @@ export default {
|
|||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
this.loading = false;
|
||||
this.message =
|
||||
(error.data && error.data.error) ||
|
||||
error.message ||
|
||||
error.toString();
|
||||
if(error.data.errcode ==='M_FORBIDDEN') {
|
||||
if(error.data && error.data.errcode ==='M_FORBIDDEN') {
|
||||
this.message = this.$i18n.messages[this.$i18n.locale].login.invalid_message;
|
||||
this.hasError = true;
|
||||
}
|
||||
|
|
@ -192,6 +204,45 @@ export default {
|
|||
handleCreateRoom() {
|
||||
this.$navigation.push({ name: "CreateRoom" });
|
||||
},
|
||||
onUsernameEnter() {
|
||||
this.$refs.password.focus();
|
||||
this.onUsernameBlur();
|
||||
},
|
||||
onUsernameBlur() {
|
||||
var user = Object.assign({}, this.user);
|
||||
user.normalize();
|
||||
const server = user.home_server || this.$config.defaultServer;
|
||||
if (server !== this.currentLoginServer) {
|
||||
|
||||
this.showPasswordField = false;
|
||||
|
||||
this.currentLoginServer = server;
|
||||
this.loadingLoginFlows = true;
|
||||
|
||||
const matrixClient = sdk.createClient({baseUrl: server});
|
||||
matrixClient.loginFlows().then((response) => {
|
||||
console.log("FLOWS", response.flows);
|
||||
this.loginFlows = response.flows.filter(this.supportedLoginFlow);
|
||||
this.loadingLoginFlows = false;
|
||||
if (this.loginFlows.length == 0) {
|
||||
this.message = this.$t('login.no_supported_flow')
|
||||
this.hasError = true;
|
||||
} else {
|
||||
this.message = "";
|
||||
this.hasError = false;
|
||||
this.showPasswordField = this.loginFlows.some(f => f.type == "m.login.password");
|
||||
if (this.showPasswordField) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.password.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
supportedLoginFlow(flow) {
|
||||
return ["m.login.password"].includes(flow.type);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
169
src/components/MoreMenuPopup.vue
Normal file
169
src/components/MoreMenuPopup.vue
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<v-dialog v-model="showDialog" content-class="more-menu-popup" class="ma-0 pa-0">
|
||||
<div class="popup-wrapper">
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
|
||||
<v-container class="mt-0 pa-0 pt-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()" />
|
||||
|
||||
<v-row class="profile-row clickable" @click="viewProfile" no-gutters align-content="center">
|
||||
<v-col cols="auto" class="me-2">
|
||||
<v-avatar class="avatar-32" size="32" color="#e0e0e0" @click.stop="viewProfile">
|
||||
<img v-if="userAvatar" :src="userAvatar" />
|
||||
<span v-else class="white--text">{{ userAvatarLetter }}</span>
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<div class="profile-label">{{ $t('profile.title') }}</div>
|
||||
<div class="display-name">{{ displayName }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
import profileInfoMixin from "./profileInfoMixin";
|
||||
import ActionRow from "./ActionRow.vue";
|
||||
|
||||
export default {
|
||||
name: "MoreMenuPopup",
|
||||
mixins: [profileInfoMixin],
|
||||
components: { ActionRow },
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
menuItems: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDialog: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
immediate: true,
|
||||
handler(newVal, ignoredOldVal) {
|
||||
this.showDialog = newVal;
|
||||
},
|
||||
},
|
||||
showDialog() {
|
||||
if (!this.showDialog) {
|
||||
this.$emit("close");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
viewProfile() {
|
||||
this.showDialog = false;
|
||||
this.$navigation.push({ name: "Profile" }, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
@import '~vuetify/src/styles/settings/_variables.scss';
|
||||
|
||||
.popup-wrapper {
|
||||
width: fit-content;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 18px;
|
||||
pointer-events: initial;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.more-menu-popup {
|
||||
font-family: "Inter", sans-serif !important;
|
||||
font-size: 16px;
|
||||
line-height: 117%;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.4px;
|
||||
position: fixed;
|
||||
margin: 0px;
|
||||
top: 70px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
.v-card__text {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
height: 40px;
|
||||
padding: 4px 20px !important;
|
||||
font-size: 16px;
|
||||
color: #000B16;
|
||||
}
|
||||
|
||||
.profile-row {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 20px 20px !important;
|
||||
}
|
||||
|
||||
.action-row:after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.profile-label {
|
||||
letter-spacing: 0.4px;
|
||||
color: #000B16;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.4px;
|
||||
color: #000B16;
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
right: inherit;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
//border-radius: 40px;
|
||||
width: 95%;
|
||||
|
||||
@media #{map-get($display-breakpoints, 'sm-and-up')} {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
@media #{map-get($display-breakpoints, 'lg-and-up')} {
|
||||
overflow: unset;
|
||||
width: $main-desktop-width;
|
||||
;
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: unset;
|
||||
width: $dialog-desktop-width;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
right: 40px;
|
||||
}
|
||||
|
||||
// .v-card {
|
||||
// border-radius: 20px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
src/components/NoHistoryRoomWelcomeHeader.vue
Normal file
19
src/components/NoHistoryRoomWelcomeHeader.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-icon size="27" class="shield">$vuetify.icons.ic_security-shield</v-icon>
|
||||
<div>{{ $t("room_welcome.no_past_messages") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "NoHistoryRoomWelcomeHeader",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
.shield {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,26 +1,20 @@
|
|||
<template>
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
content-class="profile-info-popup"
|
||||
class="ma-0 pa-0"
|
||||
>
|
||||
<v-dialog v-model="showDialog" content-class="profile-info-popup" class="ma-0 pa-0">
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<div class="you-are">{{ $t("profile_info_popup.you_are") }}</div>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col :class="['username',{'editable': editDisplayName }]" cols="pa-2" ref="username">
|
||||
<v-col :class="['username', { 'editable': editDisplayName }]" cols="pa-2" ref="username">
|
||||
<div v-if="$matrix.currentUser.is_guest">
|
||||
<i18n path="profile_info_popup.identity_temporary" tag="span">
|
||||
<template v-slot:displayName>
|
||||
<input
|
||||
v-model="displayName"
|
||||
@blur="
|
||||
updateDisplayName($event.target.value);
|
||||
editDisplayName = !editDisplayName;
|
||||
"
|
||||
@focus="editDisplayName = !editDisplayName"
|
||||
/>
|
||||
<input v-model="displayName"
|
||||
@keyup.enter="$event => $event.target.blur()"
|
||||
@blur="
|
||||
updateDisplayName($event.target.value);
|
||||
editDisplayName = !editDisplayName;
|
||||
" @focus="editDisplayName = !editDisplayName" />
|
||||
</template>
|
||||
</i18n>
|
||||
</div>
|
||||
|
|
@ -29,23 +23,16 @@
|
|||
<template v-slot:displayName>
|
||||
<input
|
||||
v-model="displayName"
|
||||
@blur="
|
||||
updateDisplayName($event.target.value);
|
||||
editDisplayName = !editDisplayName;
|
||||
"
|
||||
@keyup.enter="$event => $event.target.blur()"
|
||||
@blur="updateDisplayName($event.target.value);editDisplayName = !editDisplayName;"
|
||||
@focus="editDisplayName = !editDisplayName"
|
||||
/>
|
||||
/>
|
||||
</template>
|
||||
</i18n>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="auto" class="pa-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" />
|
||||
<span v-else class="white--text">{{ userAvatarLetter }}</span>
|
||||
</v-avatar>
|
||||
|
|
@ -53,24 +40,6 @@
|
|||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-container class="mt-4 pa-0">
|
||||
<ActionRow
|
||||
@click="viewProfile"
|
||||
:icon="'account_circle'"
|
||||
:text="$t('profile_info_popup.edit_profile')"
|
||||
/>
|
||||
<ActionRow
|
||||
@click.stop="showLogoutPopup=true"
|
||||
:icon="'logout'"
|
||||
:text="$t('profile_info_popup.logout')"
|
||||
/>
|
||||
<LogoutRoomDialog
|
||||
:showLogoutPopup="showLogoutPopup"
|
||||
@onOutsideLogoutPopupClicked="showLogoutPopup=false"
|
||||
@onCancelLogoutClicked="showLogoutPopup=false"
|
||||
/>
|
||||
</v-container>
|
||||
|
||||
<div class="more-container">
|
||||
<div class="want_more">
|
||||
🙌 {{ $t("profile_info_popup.want_more") }}
|
||||
|
|
@ -78,7 +47,7 @@
|
|||
<i18n path="profile_info_popup.powered_by" tag="div">
|
||||
<template v-slot:product>{{ product }}</template>
|
||||
<template v-slot:productLink>
|
||||
<a :href="'//'+productLink">{{ productLink }}</a>
|
||||
<a :href="'//' + productLink">{{ productLink }}</a>
|
||||
</template>
|
||||
</i18n>
|
||||
<div class="text-end">
|
||||
|
|
@ -93,16 +62,10 @@
|
|||
</template>
|
||||
<script>
|
||||
import profileInfoMixin from "./profileInfoMixin";
|
||||
import ActionRow from "./ActionRow.vue";
|
||||
import LogoutRoomDialog from './LogoutRoomDialog.vue';
|
||||
|
||||
export default {
|
||||
name: "ProfileInfoPopup",
|
||||
mixins: [profileInfoMixin],
|
||||
components: {
|
||||
ActionRow,
|
||||
LogoutRoomDialog
|
||||
},
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
|
|
@ -115,7 +78,6 @@ export default {
|
|||
return {
|
||||
showDialog: false,
|
||||
editDisplayName: false,
|
||||
showLogoutPopup: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -163,28 +125,20 @@ export default {
|
|||
margin: 0px;
|
||||
top: 70px;
|
||||
right: 10px;
|
||||
|
||||
[dir="rtl"] & {
|
||||
right: inherit;
|
||||
left: 10px;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
border-radius: 40px;
|
||||
width: 95%;
|
||||
|
||||
&::before {
|
||||
content: "▲";
|
||||
position: fixed;
|
||||
top: 57px;
|
||||
right: 22px;
|
||||
[dir="rtl"] & {
|
||||
left: 22px;
|
||||
right: inherit;
|
||||
}
|
||||
color: white;
|
||||
}
|
||||
.you-are {
|
||||
padding-top: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.username {
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
|
|
@ -202,19 +156,45 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.more-container {
|
||||
border-radius: 10px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14 * $chat-text-size;
|
||||
line-height: 117%;
|
||||
letter-spacing: 0.4px;
|
||||
color: #000B16;
|
||||
margin-top: 18px;
|
||||
a {
|
||||
color: #000B16 !important;
|
||||
}
|
||||
.want_more {
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 13 * $chat-text-size;
|
||||
color: #0e252d;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.new_room {
|
||||
margin-top: 16px;
|
||||
height: 27px;
|
||||
border-radius: 13.5px;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
.new_room .v-btn__content {
|
||||
font-family: "Poppins", sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 13 * $chat-text-size !important;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 10 * $chat-text-size !important;
|
||||
line-height: 133%;
|
||||
letter-spacing: 0.34px;
|
||||
text-transform: uppercase;
|
||||
color: #181719;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +204,7 @@ export default {
|
|||
|
||||
@media #{map-get($display-breakpoints, 'lg-and-up')} {
|
||||
overflow: unset;
|
||||
width: $main-desktop-width;;
|
||||
width: $main-desktop-width;
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: unset;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,6 @@
|
|||
<!-- Header-->
|
||||
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
|
||||
<v-row class="chat-header-row flex-nowrap">
|
||||
<v-col cols="auto" class="chat-header-members text-start ma-0 pa-0" @click.stop="onHeaderClicked">
|
||||
<v-avatar size="40" class="me-2">
|
||||
<v-img v-if="room.avatar" :src="room.avatar" />
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
|
||||
<v-col class="chat-header-name ma-0 pa-0 flex-shrink-1 flex-nowrap">
|
||||
<div class="room-name-inline text-truncate" :title="room.name">
|
||||
{{ room.name }}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
<v-btn
|
||||
id="btn-back"
|
||||
text
|
||||
:class="$navigation && $navigation.canPop() ? 'v-visible' : 'v-hidden'"
|
||||
@click.stop="$navigation.pop"
|
||||
:class="(($navigation && $navigation.canPop()) || $matrix.currentRoomId) ? 'v-visible' : 'v-hidden'"
|
||||
@click.stop="goBack()"
|
||||
>
|
||||
<v-icon>arrow_back</v-icon>
|
||||
<span class="d-none d-sm-block">{{ $t("menu.back") }}</span>
|
||||
|
|
@ -165,9 +165,9 @@
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="account ma-3" flat v-if="$config.experimental_voice_mode">
|
||||
<v-card class="account ma-3" flat v-if="$config.experimental_voice_mode || canChangeReadOnly()">
|
||||
<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-card-text class="with-right-label" v-if="$config.experimental_voice_mode">
|
||||
<div>
|
||||
<div class="option-title">{{ $t('room_info.voice_mode') }}</div>
|
||||
<div class="option-text">{{ $t('room_info.voice_mode_info') }}</div>
|
||||
|
|
@ -176,6 +176,15 @@
|
|||
v-model="useVoiceMode"
|
||||
></v-switch>
|
||||
</v-card-text>
|
||||
<v-card-text class="with-right-label" v-if="canChangeReadOnly()">
|
||||
<div>
|
||||
<div class="option-title">{{ $t('room_info.read_only_room') }}</div>
|
||||
<div class="option-text">{{ $t('room_info.read_only_room_info') }}</div>
|
||||
</div>
|
||||
<v-switch
|
||||
v-model="readOnlyRoom"
|
||||
></v-switch>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="members ma-3" flat>
|
||||
|
|
@ -254,11 +263,12 @@
|
|||
id="btn-purge-room"
|
||||
v-if="userCanPurgeRoom"
|
||||
color="red"
|
||||
depressed
|
||||
fab
|
||||
class="filled-button"
|
||||
@click.stop="showPurgeConfirmation = true"
|
||||
>{{ $t("room_info.purge") }}</v-btn
|
||||
>
|
||||
>
|
||||
<v-icon light>$vuetify.icons.ic_moderator-delete</v-icon> {{ $t("room_info.purge") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="build-version">
|
||||
|
|
@ -388,6 +398,14 @@ export default {
|
|||
this.$matrix.matrixClient.setRoomTag(this.room.roomId, "ui_options", options);
|
||||
}
|
||||
},
|
||||
},
|
||||
readOnlyRoom: {
|
||||
get: function () {
|
||||
return this.$matrix.isReadOnlyRoom(this.room.roomId);
|
||||
},
|
||||
set: function (readOnly) {
|
||||
this.$matrix.setReadOnlyRoom(this.room.roomId, readOnly);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -396,7 +414,10 @@ export default {
|
|||
handler(ignoredNewVal, ignoredOldVal) {
|
||||
console.log("RoomInfo: Current room changed");
|
||||
this.updateMembers();
|
||||
this.updateQRCode();
|
||||
this.$nextTick(() => {
|
||||
// Wait a tick, we want to be sure that the QR canvas ref is already created!
|
||||
this.updateQRCode();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -590,6 +611,16 @@ export default {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Return true if we can change power levels in the room, i.e. make read only room
|
||||
*/
|
||||
canChangeReadOnly() {
|
||||
if (!this.$config.experimental_read_only_room) { return false; }
|
||||
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!
|
||||
isAdmin(member) {
|
||||
return member.powerLevelNorm > 50;
|
||||
|
|
@ -599,7 +630,7 @@ export default {
|
|||
},
|
||||
/**
|
||||
* Return true if WE can make the member an admin
|
||||
* @param member
|
||||
* @param member
|
||||
*/
|
||||
canMakeAdmin(ignoredmember) {
|
||||
if (this.room) {
|
||||
|
|
@ -612,7 +643,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Return true if WE can make the member a moderator
|
||||
* @param member
|
||||
* @param member
|
||||
*/
|
||||
canMakeModerator(ignoredmember) {
|
||||
if (this.room) {
|
||||
|
|
@ -620,11 +651,11 @@ export default {
|
|||
const me = this.room.getMember(myUserId);
|
||||
return me && this.isAdmin(me);
|
||||
}
|
||||
return false;
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Return true if WE can "unmake" the member a moderator
|
||||
* @param member
|
||||
* @param member
|
||||
*/
|
||||
canRevokeModerator(member) {
|
||||
if (this.room) {
|
||||
|
|
@ -632,7 +663,7 @@ export default {
|
|||
const me = this.room.getMember(myUserId);
|
||||
return me && this.isAdmin(me) && me.powerLevel > member.powerLevel;
|
||||
}
|
||||
return false;
|
||||
return false;
|
||||
},
|
||||
makeAdmin(member) {
|
||||
if (this.room) {
|
||||
|
|
@ -658,6 +689,23 @@ export default {
|
|||
if (this.room) {
|
||||
this.$matrix.banUser(this.room.roomId, member.userId)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Go back to previous page, or if none on the stack, go back to current room view.
|
||||
*/
|
||||
goBack() {
|
||||
if (this.$navigation.canPop()) {
|
||||
this.$navigation.pop();
|
||||
} else if (this.$matrix.currentRoomId) {
|
||||
this.$navigation.push(
|
||||
{
|
||||
name: "Chat",
|
||||
params: { roomId: util.sanitizeRoomId(this.$matrix.currentRoomId) },
|
||||
},
|
||||
-1
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,52 +3,16 @@
|
|||
class="room-info-bottom-sheet"
|
||||
:halfY="0.12"
|
||||
ref="sheet"
|
||||
:showCloseButton="false"
|
||||
>
|
||||
<div class="room-info-sheet" ref="roomInfoSheetContent">
|
||||
<div class="text-center current-room">
|
||||
<room-avatar-picker />
|
||||
<div class="h4">{{$t('room_info_sheet.this_room')}}</div>
|
||||
<div
|
||||
class="h2"
|
||||
v-if="!isRoomNameEditMode"
|
||||
@click="onRoomNameClicked()"
|
||||
>
|
||||
{{ roomName }}
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="editedRoomName"
|
||||
ref="editedRoomName"
|
||||
:rules="[(v) => !!v || $t('room.room_name_required')]"
|
||||
:error="roomNameErrorMessage != null"
|
||||
:error-messages="roomNameErrorMessage"
|
||||
required
|
||||
color="black"
|
||||
counter="50"
|
||||
background-color="white"
|
||||
autofocus
|
||||
v-if="isRoomNameEditMode"
|
||||
maxlength="50"
|
||||
@blur="updateRoomName()"
|
||||
@keyup.enter="updateRoomName()"
|
||||
solo
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
id="btn-room-details"
|
||||
height="20px"
|
||||
color="black"
|
||||
class="filled-button"
|
||||
@click.stop="showDetails"
|
||||
>{{$t('room_info_sheet.view_details')}}</v-btn
|
||||
>
|
||||
</div>
|
||||
<room-list :title="'Other rooms'" v-on:close="close" v-on:newroom="createRoom" :showCreate="true" />
|
||||
<room-list v-on:close="close" v-on:newroom="createRoom" :showCreate="true" />
|
||||
</div>
|
||||
</BottomSheet>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BottomSheet from "./BottomSheet";
|
||||
import RoomAvatarPicker from "./RoomAvatarPicker";
|
||||
import RoomList from "./RoomList";
|
||||
import roomInfoMixin from "./roomInfoMixin";
|
||||
|
||||
|
|
@ -58,7 +22,6 @@ export default {
|
|||
components: {
|
||||
BottomSheet,
|
||||
RoomList,
|
||||
RoomAvatarPicker,
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
|
|
|
|||
|
|
@ -1,70 +1,58 @@
|
|||
<template>
|
||||
<v-list dense class="room-list">
|
||||
<div class="h4">{{ title }}</div>
|
||||
<v-list-item-group v-model="currentRoomId" color="primary">
|
||||
<v-list-item v-if="showCreate" @click.stop="$emit('newroom')">
|
||||
<v-list-item-group @change="roomChange" color="primary">
|
||||
|
||||
<v-list-item v-if="showCreate" @click.stop="$emit('newroom')" class="room-list-room" :value="null">
|
||||
<v-list-item-avatar class="round" size="42" color="#d9d9d9">
|
||||
<v-icon size="11">$vuetify.icons.ic_new_room</v-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="new-room">{{
|
||||
<v-list-item-title class="room-list-new-room">{{
|
||||
$t("menu.new_room")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<!-- invites -->
|
||||
<v-list-item
|
||||
:disabled="roomsProcessing[room.roomId]"
|
||||
v-for="room in invitedRooms"
|
||||
:key="room.roomId"
|
||||
:value="room.roomId"
|
||||
>
|
||||
<v-list-item-avatar size="40" color="#e0e0e0">
|
||||
<v-img v-if="room.avatar" :src="room.avatar" />
|
||||
<v-list-item :disabled="roomsProcessing[room.roomId]" v-for="room in invitedRooms" :key="room.roomId"
|
||||
:value="room.roomId" class="room-list-room">
|
||||
<v-list-item-avatar size="42" color="#d9d9d9">
|
||||
<v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" />
|
||||
<span v-else class="white--text headline">{{
|
||||
room.name.substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ room.name }}</v-list-item-title>
|
||||
<v-list-item-title class="room-list-name">{{ room.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
id="btn-accept"
|
||||
class="filled-button"
|
||||
depressed
|
||||
color="black"
|
||||
@click.stop="acceptInvitation(room)"
|
||||
>{{ $t("menu.join") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
id="btn-reject"
|
||||
class="filled-button"
|
||||
color="black"
|
||||
@click.stop="rejectInvitation(room)"
|
||||
text
|
||||
>{{ $t("menu.ignore") }}</v-btn
|
||||
>
|
||||
<v-btn id="btn-accept" class="filled-button" depressed color="black" @click.stop="acceptInvitation(room)">{{
|
||||
$t("menu.join") }}</v-btn>
|
||||
<v-btn id="btn-reject" class="filled-button" color="black" @click.stop="rejectInvitation(room)" text>{{
|
||||
$t("menu.ignore") }}</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-for="room in joinedRooms"
|
||||
:key="room.roomId"
|
||||
:value="room.roomId"
|
||||
>
|
||||
<v-list-item-avatar size="40" color="#e0e0e0">
|
||||
<v-img v-if="room.avatar" :src="room.avatar" />
|
||||
<v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room.roomId" class="room-list-room">
|
||||
<v-list-item-avatar size="42" color="#d9d9d9">
|
||||
<v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" />
|
||||
<span v-else class="white--text headline">{{
|
||||
room.name.substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
</v-list-item-avatar>
|
||||
<div class="room-list-notification-count" v-if="notificationCount(room) > 0">
|
||||
{{ notificationCount(room) }}
|
||||
</div>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ room.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle>
|
||||
<v-list-item-title class="room-list-name">{{ room.name }}
|
||||
<!-- <v-icon class="ml-2 mb-1" size="10" v-if="isPublic(room)">$vuetify.icons.ic_public</v-icon> -->
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="room-list-new-messages" v-if="notificationCount(room) > 0">
|
||||
{{ $t("room.room_list_new_messages", { count: notificationCount(room) }) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-icon size="16" v-if="room.roomId == $matrix.currentRoomId">$vuetify.icons.ic_circle_filled</v-icon>
|
||||
<v-icon size="16" v-else>$vuetify.icons.ic_circle</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
|
|
@ -93,7 +81,6 @@ export default {
|
|||
},
|
||||
|
||||
data: () => ({
|
||||
currentRoomId: null,
|
||||
/** A list of rooms currently processing some operation, like "join" or "reject" */
|
||||
roomsProcessing: {},
|
||||
}),
|
||||
|
|
@ -106,8 +93,27 @@ export default {
|
|||
return this.sortItemsOnName(this.$matrix.joinedRooms);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
roomAvatar(room) {
|
||||
if (this.isDirect(room)) {
|
||||
if (room.avatar) {
|
||||
return room.avatar;
|
||||
}
|
||||
const membersNotMe = room.getMembers().filter(m => m.userId != this.$matrix.currentUserId);
|
||||
if (membersNotMe && membersNotMe.length == 1) {
|
||||
return membersNotMe[0].getAvatarUrl(
|
||||
this.$matrix.matrixClient.getHomeserverUrl(),
|
||||
42,
|
||||
42,
|
||||
"scale",
|
||||
true
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return room.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
sortItemsOnName(items) {
|
||||
if (items == null) {
|
||||
return [];
|
||||
|
|
@ -166,11 +172,17 @@ export default {
|
|||
Vue.delete(this.roomsProcessing, room.roomId);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
currentRoomId() {
|
||||
if (this.currentRoomId == null) {
|
||||
isPublic(room) {
|
||||
return this.$matrix.getRoomJoinRule(room) === "public"
|
||||
},
|
||||
|
||||
isDirect(room) {
|
||||
return this.$matrix.isDirectRoom(room);
|
||||
},
|
||||
|
||||
roomChange(roomId) {
|
||||
if (roomId == null || roomId == undefined) {
|
||||
// Ignore, this is caused by "new room" etc.
|
||||
return;
|
||||
}
|
||||
|
|
@ -178,11 +190,11 @@ export default {
|
|||
this.$navigation.push(
|
||||
{
|
||||
name: "Chat",
|
||||
params: { roomId: util.sanitizeRoomId(this.currentRoomId) },
|
||||
params: { roomId: util.sanitizeRoomId(roomId) },
|
||||
},
|
||||
-1
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
<transition name="grow" mode="out-in">
|
||||
<div
|
||||
v-show="show"
|
||||
:class="{ 'voice-recorder': true, ptt: ptt, row: !ptt }"
|
||||
:class="{ 'voice-recorder': true, ptt: usePTT, row: !usePTT }"
|
||||
ref="vrroot"
|
||||
>
|
||||
<v-container v-if="!ptt" fluid fill-height>
|
||||
<v-container v-if="!usePTT" fluid fill-height>
|
||||
<v-row align="center" class="mt-3">
|
||||
<v-col cols="4" align="center">
|
||||
<v-btn v-show="state == states.RECORDED" icon @click.stop="redo">
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
{{ recordingTime }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" v-if="ptt">
|
||||
<v-col cols="6" v-if="usePTT">
|
||||
<div class="swipe-info">
|
||||
<< {{ $t("voice_recorder.swipe_to_cancel") }}
|
||||
</div>
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
</div>
|
||||
|
||||
<VoiceRecorderLock
|
||||
v-show="state == states.RECORDING && ptt"
|
||||
v-show="state == states.RECORDING && usePTT"
|
||||
:style="lockButtonStyle"
|
||||
:isLocked="recordingLocked"
|
||||
/>
|
||||
|
|
@ -209,6 +209,9 @@ export default {
|
|||
errorMessage: null,
|
||||
recorder: null,
|
||||
previewPlayer: null,
|
||||
wakeLock: null,
|
||||
maxRecordingLength: 300, // In seconds
|
||||
forceNonPTTMode: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -244,13 +247,14 @@ export default {
|
|||
}
|
||||
},
|
||||
show(val) {
|
||||
this.forceNonPTTMode = false;
|
||||
if (val) {
|
||||
// Add listeners
|
||||
this.state = State.INITIAL;
|
||||
this.errorMessage = null;
|
||||
this.recordedFile = null;
|
||||
this.recordingTime = String.fromCharCode(160);
|
||||
if (this.ptt) {
|
||||
if (this.usePTT) {
|
||||
document.addEventListener("mouseup", this.mouseUp, false);
|
||||
document.addEventListener("mousemove", this.mouseMove, false);
|
||||
document.addEventListener("touchend", this.mouseUp, false);
|
||||
|
|
@ -288,6 +292,9 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
usePTT() {
|
||||
return this.ptt && !this.forceNonPTTMode;
|
||||
},
|
||||
lockButtonStyle() {
|
||||
/**
|
||||
Calculate where to show the lock button (it should be at the same X-coord as the)
|
||||
|
|
@ -366,6 +373,9 @@ export default {
|
|||
this.recordStartedAt = Date.now();
|
||||
this.startRecordTimer();
|
||||
})
|
||||
.then(async () => {
|
||||
this.aquireWakeLock();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
if (e && e.name == "NotAllowedError") {
|
||||
|
|
@ -374,25 +384,65 @@ export default {
|
|||
this.state = State.ERROR;
|
||||
});
|
||||
},
|
||||
screenLocked() {
|
||||
if (document.visibilityState === "hidden" && this.state == State.RECORDING) {
|
||||
this.pauseRecording();
|
||||
}
|
||||
},
|
||||
playRecordedSound() {
|
||||
const audio = new Audio(require("@/assets/sounds/record_stop.mp3"));
|
||||
audio.play();
|
||||
},
|
||||
aquireWakeLock() {
|
||||
document.addEventListener("visibilitychange", this.screenLocked);
|
||||
try {
|
||||
if (navigator.wakeLock && !this.wakeLock) {
|
||||
navigator.wakeLock.request('screen').then((lock) => this.wakeLock = lock);
|
||||
}
|
||||
}
|
||||
catch(err) { console.error(err)}
|
||||
},
|
||||
releaseWakeLock() {
|
||||
document.removeEventListener("visibilitychange", this.screenLocked);
|
||||
if (this.wakeLock) {
|
||||
this.wakeLock.release().then(() => {
|
||||
this.wakeLock = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
cancelRecording() {
|
||||
if(this.recorder) {
|
||||
this.recorder.stop();
|
||||
this.recorder = null;
|
||||
}
|
||||
this.releaseWakeLock();
|
||||
this.state = State.INITIAL;
|
||||
this.close();
|
||||
},
|
||||
pauseRecording() {
|
||||
// Remove PTT mode. We can get here in PTT if screen is locked or if max time is reached.
|
||||
if (this.ptt) {
|
||||
this.forceNonPTTMode = true;
|
||||
this.recordingLocked = false;
|
||||
document.removeEventListener("mouseup", this.mouseUp, false);
|
||||
document.removeEventListener("mousemove", this.mouseMove, false);
|
||||
document.removeEventListener("touchend", this.mouseUp, false);
|
||||
document.removeEventListener("touchmove", this.mouseMove, false);
|
||||
}
|
||||
this.state = State.RECORDED;
|
||||
this.stopRecordTimer();
|
||||
this.releaseWakeLock();
|
||||
this.getFile(false);
|
||||
this.playRecordedSound();
|
||||
},
|
||||
stopRecording() {
|
||||
this.state = State.RECORDED;
|
||||
this.stopRecordTimer();
|
||||
this.releaseWakeLock();
|
||||
this.recordingTime = String.fromCharCode(160); // nbsp;
|
||||
this.close();
|
||||
this.getFile(true);
|
||||
this.playRecordedSound();
|
||||
},
|
||||
redo() {
|
||||
this.state = State.INITIAL;
|
||||
|
|
@ -431,6 +481,10 @@ export default {
|
|||
this.recordingTime = util.formatRecordDuration(
|
||||
now - this.recordStartedAt
|
||||
);
|
||||
// Auto-stop?
|
||||
if ((now - this.recordStartedAt) >= (1000 * this.maxRecordingLength) && this.state == State.RECORDING) {
|
||||
this.pauseRecording();
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
stopRecordTimer() {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,15 @@ export default {
|
|||
CreatePollDialog,
|
||||
},
|
||||
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) {
|
||||
const idx = this.events.indexOf(event);
|
||||
if (idx <= 0) {
|
||||
|
|
@ -132,10 +141,12 @@ export default {
|
|||
return ContactKicked;
|
||||
}
|
||||
return ContactLeave;
|
||||
} else if (event.getContent().membership == "invite") {
|
||||
return ContactInvited;
|
||||
} else if (event.getContent().membership == "ban") {
|
||||
return ContactBanned;
|
||||
} else if (!this.showOnlyUserStatusMessages()) {
|
||||
if (event.getContent().membership == "invite") {
|
||||
return ContactInvited;
|
||||
} else if (event.getContent().membership == "ban") {
|
||||
return ContactBanned;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -203,34 +214,64 @@ export default {
|
|||
}
|
||||
|
||||
case "m.room.create":
|
||||
return RoomCreated;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomCreated;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.canonical_alias":
|
||||
return RoomAliased;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomAliased;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.name":
|
||||
return RoomNameChanged;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomNameChanged;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.topic":
|
||||
return RoomTopicChanged;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomTopicChanged;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.avatar":
|
||||
return RoomAvatarChanged;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomAvatarChanged;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.history_visibility":
|
||||
return RoomHistoryVisibility;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomHistoryVisibility;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.join_rules":
|
||||
return RoomJoinRules;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomJoinRules;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.power_levels":
|
||||
return RoomPowerLevelsChanged;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomPowerLevelsChanged;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.guest_access":
|
||||
return RoomGuestAccessChanged;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomGuestAccessChanged;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.encryption":
|
||||
return RoomEncrypted;
|
||||
if (!this.showOnlyUserStatusMessages()) {
|
||||
return RoomEncrypted;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.poll.start":
|
||||
case "org.matrix.msc3381.poll.start":
|
||||
|
|
|
|||
10
src/components/logoMixin.js
Normal file
10
src/components/logoMixin.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
computed: {
|
||||
logotype() {
|
||||
if (this.$config.logo) {
|
||||
return this.$config.logo;
|
||||
}
|
||||
return require("@/assets/logo.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,20 +15,23 @@
|
|||
</v-avatar>
|
||||
<!-- SLOT FOR CONTENT -->
|
||||
<slot></slot>
|
||||
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
|
||||
<div class="op-button" ref="opbutton" v-if="!event.isRedacted() && !$matrix.currentRoomIsReadOnlyForUser">
|
||||
<v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
|
||||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<QuickReactions :event="event" :timelineSet="timelineSet" />
|
||||
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||
<SeenBy :room="room" :event="event"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SeenBy from "./SeenBy.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
mixins: [messageMixin],
|
||||
components: { SeenBy }
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
<div class="status">{{ event.status }}</div>
|
||||
</div>
|
||||
|
||||
<QuickReactions :event="event" :timelineSet="timelineSet" />
|
||||
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
|
||||
<div class="op-button" ref="opbutton" v-if="!event.isRedacted() && !$matrix.currentRoomIsReadOnlyForUser">
|
||||
<v-btn id="btn-show-menu" icon @click.stop="showContextMenu($refs.opbutton)">
|
||||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
|
|
@ -25,14 +24,18 @@
|
|||
<img v-if="userAvatar" :src="userAvatar" />
|
||||
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
|
||||
</v-avatar>
|
||||
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
|
||||
<SeenBy :room="room" :event="event"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SeenBy from "./SeenBy.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: { SeenBy }
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -1,13 +1,67 @@
|
|||
<template>
|
||||
<div class="quick-reaction-container" v-show="reactions">
|
||||
<span :class="{'quick-reaction':true,'sent':value.includes($matrix.currentUserId)}" v-for="(value, name) in reactionMap" :key="name" @click.stop="onClickEmoji(name)">
|
||||
{{ name }} <span class="quick-reaction-count">{{ value.length }}</span>
|
||||
</span>
|
||||
<div
|
||||
class="emoji"
|
||||
v-for="(value, name, index) in reactionMap"
|
||||
:key="name"
|
||||
v-show="showAllReaction || index < REACTION_LIMIT"
|
||||
>
|
||||
<v-tooltip top v-if="value.includes($matrix.currentUserId)">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-chip
|
||||
class="pa-2 ma-1 ml-0"
|
||||
outlined
|
||||
small
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
@click="onClickEmoji(name)"
|
||||
>
|
||||
{{ name }} {{ value.length }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<span v-if="value.includes($matrix.currentUserId)">{{ $t("global.click_to_remove") }}</span>
|
||||
</v-tooltip>
|
||||
<v-chip
|
||||
v-else
|
||||
class="pa-2 ma-1 ml-0"
|
||||
outlined
|
||||
small
|
||||
>
|
||||
{{ name }} {{ value.length }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-chip
|
||||
v-if="totalReaction > REACTION_LIMIT"
|
||||
@click="showAllReaction = !showAllReaction"
|
||||
class="pa-2 ma-1 ml-0"
|
||||
outlined
|
||||
small
|
||||
>
|
||||
{{ otherReactionText }}
|
||||
</v-chip>
|
||||
<v-tooltip top v-if="!!totalReaction">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-chip
|
||||
outlined
|
||||
small
|
||||
class="pa-2 ma-1 ml-0"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
@click="more"
|
||||
>
|
||||
<v-icon small> $vuetify.icons.addReaction </v-icon>
|
||||
</v-chip>
|
||||
</template>
|
||||
<span>{{ $t("global.add_reaction") }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageOperationsMixin from "./messageOperationsMixin";
|
||||
|
||||
export default {
|
||||
mixins: [messageOperationsMixin],
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
|
|
@ -25,7 +79,9 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
reactionMap: {},
|
||||
reactions: null
|
||||
reactions: null,
|
||||
REACTION_LIMIT: 5,
|
||||
showAllReaction: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -38,6 +94,14 @@ export default {
|
|||
this.reactions.off('Relations.add', this.onAddRelation);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalReaction() {
|
||||
return Object.keys(this.reactionMap).length
|
||||
},
|
||||
otherReactionText() {
|
||||
return this.showAllReaction ? this.$t("global.show_less") : this.$t("message.reaction_count_more", { reactionCount: this.totalReaction - this.REACTION_LIMIT })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onRelationsCreated() {
|
||||
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');
|
||||
|
|
|
|||
96
src/components/messages/SeenBy.vue
Normal file
96
src/components/messages/SeenBy.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div class="seen-by-container">
|
||||
<v-tooltip top open-delay="500" v-if="seenBy.length > 0">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<div v-bind="attrs" v-on="on" class="clickable">
|
||||
<div class="more" v-if="seenBy.length > 0">{{ moreItems }}</div>
|
||||
<transition-group name="list" tag="div" v-if="seenBy.length > 0">
|
||||
<v-avatar v-for="(member, index) in seenBy" :key="member.userId" class="seen-by-user" size="16" color="grey"
|
||||
v-show="index < SHOW_LIMIT">
|
||||
<img v-if="memberAvatar(member)" :src="memberAvatar(member)" />
|
||||
<span v-else class="white--text headline">{{
|
||||
member.name.substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ $tc("message.seen_by", seenBy.length) }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
room: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
event: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
seenBy: [],
|
||||
SHOW_LIMIT: 5,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.update();
|
||||
if (this.room) {
|
||||
this.room.on("Room.receipt", this.onReceipt);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.room) {
|
||||
this.room.off("Room.receipt", this.onReceipt);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
moreItems() {
|
||||
if (this.seenBy.length > this.SHOW_LIMIT) {
|
||||
return `+${this.seenBy.length - this.SHOW_LIMIT}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onReceipt(ignoredevent) {
|
||||
this.update();
|
||||
},
|
||||
memberAvatar(member) {
|
||||
if (member) {
|
||||
return member.getAvatarUrl(
|
||||
this.$matrix.matrixClient.getHomeserverUrl(),
|
||||
16,
|
||||
16,
|
||||
"scale",
|
||||
true
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
update() {
|
||||
this.seenBy = ((this.room && this.event) ? this.room.getReceiptsForEvent(this.event) : [])
|
||||
.filter(receipt => receipt.type == 'm.read' && receipt.userId !== this.$matrix.currentUserId)
|
||||
.map(receipt => this.room.getMember(receipt.userId));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
event() {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
|
|
@ -98,13 +98,15 @@ export default {
|
|||
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||
const content = this.event.getContent();
|
||||
const lines = content.body.split("\n").reverse();
|
||||
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
|
||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||
if (lines[0] === "") lines.shift();
|
||||
const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, "");
|
||||
if (text) {
|
||||
return text;
|
||||
if ('body' in content) {
|
||||
const lines = content.body.split("\n").reverse() || [];
|
||||
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
|
||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||
if (lines[0] === "") lines.shift();
|
||||
const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, "");
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.inReplyToEvent) {
|
||||
|
|
@ -122,13 +124,14 @@ export default {
|
|||
const relatesTo = this.event.getWireContent()["m.relates_to"];
|
||||
if (relatesTo && relatesTo["m.in_reply_to"]) {
|
||||
const content = this.event.getContent();
|
||||
|
||||
// Remove the new text and strip "> " from the old original text
|
||||
const lines = content.body.split("\n");
|
||||
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||
if (lines[0] === "") lines.shift();
|
||||
return lines.join("\n");
|
||||
if ('body' in content) {
|
||||
// Remove the new text and strip "> " from the old original text
|
||||
const lines = content.body.split("\n");
|
||||
while (lines.length && lines[0].startsWith("> ")) lines.shift();
|
||||
// Reply fallback has a blank line after it, so remove it to prevent leading newline
|
||||
if (lines[0] === "") lines.shift();
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
return this.event.getContent().body;
|
||||
},
|
||||
|
|
|
|||
23
src/main.js
23
src/main.js
|
|
@ -1,6 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import vuetify from './plugins/vuetify';
|
||||
import store from './store'
|
||||
import i18n from './plugins/lang';
|
||||
import router from './router'
|
||||
|
|
@ -15,6 +14,7 @@ import VueResize from 'vue-resize';
|
|||
import 'vue-resize/dist/vue-resize.css';
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
import VueSanitize from "vue-sanitize";
|
||||
import createVuetify from './plugins/vuetify';
|
||||
|
||||
var defaultOptions = VueSanitize.defaults;
|
||||
defaultOptions.disallowedTagsMode = "recursiveEscape";
|
||||
|
|
@ -26,11 +26,18 @@ Vue.config.productionTip = false
|
|||
Vue.use(VueResize);
|
||||
Vue.use(VEmojiPicker);
|
||||
Vue.use(matrix, { store: store, i18n: i18n });
|
||||
// eslint-disable-next-line
|
||||
Vue.use(config, globalThis.window.location.origin); // Use this before cleaninsights below, it depends on config!
|
||||
|
||||
const configLoadedPromise = new Promise((resolve, ignoredreject) => {
|
||||
// eslint-disable-next-line
|
||||
Vue.use(config, globalThis.window.location.origin, (config) => {
|
||||
resolve(config);
|
||||
}); // Use this before cleaninsights below, it depends on config!
|
||||
});
|
||||
Vue.use(analytics);
|
||||
Vue.use(VueClipboard);
|
||||
|
||||
const vuetify = createVuetify(config);
|
||||
|
||||
// Add bubble functionality to custom events.
|
||||
// From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events
|
||||
Vue.use((Vue) => {
|
||||
|
|
@ -161,7 +168,7 @@ Vue.directive('longTap', {
|
|||
|
||||
Vue.use(navigation, router);
|
||||
|
||||
new Vue({
|
||||
const vueInstance = new Vue({
|
||||
vuetify,
|
||||
store,
|
||||
i18n,
|
||||
|
|
@ -170,4 +177,10 @@ new Vue({
|
|||
config,
|
||||
analytics,
|
||||
render: h => h(App)
|
||||
}).$mount('#app');
|
||||
});
|
||||
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor;
|
||||
configLoadedPromise.then((config) => {
|
||||
vueInstance.$vuetify.theme.themes.light.primary = config.accentColor;
|
||||
vueInstance.$mount('#app');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ var _browserCanRecordAudioF = function () {
|
|||
}
|
||||
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
||||
|
||||
class AbortablePromise extends Promise {
|
||||
class UploadPromise extends Promise {
|
||||
constructor(executor) {
|
||||
const aborter = {
|
||||
aborted: false,
|
||||
abortablePromise: undefined,
|
||||
matrixClient: undefined,
|
||||
}
|
||||
|
||||
const normalExecutor = function (resolve, reject) {
|
||||
|
|
@ -42,8 +43,9 @@ class AbortablePromise extends Promise {
|
|||
super(normalExecutor);
|
||||
this.abort = () => {
|
||||
aborter.aborted = true;
|
||||
if (aborter.abortablePromise) {
|
||||
aborter.abortablePromise.abort();
|
||||
if (aborter.abortablePromise && aborter.matrixClient) {
|
||||
aborter.matrixClient.cancelUpload(aborter.abortablePromise);
|
||||
aborter.matrixClient = undefined;
|
||||
aborter.abortablePromise = undefined;
|
||||
}
|
||||
};
|
||||
|
|
@ -268,7 +270,7 @@ class Util {
|
|||
resolve(true);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("Image send error: ", err);
|
||||
console.log("Send error: ", err);
|
||||
if (err && err.name == "UnknownDeviceError") {
|
||||
console.log("Unknown devices. Mark as known before retrying.");
|
||||
var setAsKnownPromises = [];
|
||||
|
|
@ -290,7 +292,18 @@ class Util {
|
|||
Promise.all(setAsKnownPromises)
|
||||
.then(() => {
|
||||
// All devices now marked as "known", try to resend
|
||||
matrixClient.resendEvent(err.event, matrixClient.getRoom(err.event.getRoomId()))
|
||||
let event = err.event;
|
||||
if (!event) {
|
||||
// Seems event is no longer send in the UnknownDevices error...
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
if (room) {
|
||||
event = room.getLiveTimeline().getEvents().find(e => {
|
||||
// Find the exact match (= object equality)
|
||||
return e.error === err
|
||||
});
|
||||
}
|
||||
}
|
||||
matrixClient.resendEvent(event, matrixClient.getRoom(event.getRoomId()))
|
||||
.then((result) => {
|
||||
console.log("Message sent: ", result);
|
||||
resolve(true);
|
||||
|
|
@ -309,7 +322,7 @@ class Util {
|
|||
}
|
||||
|
||||
sendImage(matrixClient, roomId, file, onUploadProgress) {
|
||||
return new AbortablePromise((resolve, reject, aborter) => {
|
||||
return new UploadPromise((resolve, reject, aborter) => {
|
||||
const abortionController = aborter;
|
||||
var reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
|
|
@ -355,6 +368,7 @@ class Util {
|
|||
|
||||
if (!matrixClient.isRoomEncrypted(roomId)) {
|
||||
// Not encrypted.
|
||||
abortionController.matrixClient = matrixClient;
|
||||
abortionController.abortablePromise = matrixClient.uploadContent(data, opts);
|
||||
abortionController.abortablePromise
|
||||
.then((response) => {
|
||||
|
|
@ -548,24 +562,23 @@ class Util {
|
|||
isChildVisible(parentNode, childNode) {
|
||||
const rect1 = parentNode.getBoundingClientRect();
|
||||
const rect2 = childNode.getBoundingClientRect();
|
||||
var overlap = !(rect1.right < rect2.left ||
|
||||
rect1.left > rect2.right ||
|
||||
rect1.bottom < rect2.top ||
|
||||
rect1.top > rect2.bottom)
|
||||
var overlap = !(rect1.right <= rect2.left ||
|
||||
rect1.left >= rect2.right ||
|
||||
rect1.bottom <= rect2.top ||
|
||||
rect1.top >= rect2.bottom)
|
||||
return overlap;
|
||||
}
|
||||
|
||||
findOneVisibleElement(parentNode) {
|
||||
let start = 0;
|
||||
let end = parentNode.children.length - 1;
|
||||
let top = parentNode.scrollTop;
|
||||
while (start <= end) {
|
||||
let middle = Math.floor((start + end) / 2);
|
||||
let childNode = parentNode.children[middle];
|
||||
if (this.isChildVisible(parentNode, childNode)) {
|
||||
// found the key
|
||||
return middle;
|
||||
} else if (childNode.offsetTop < top) {
|
||||
} else if (childNode.getBoundingClientRect().top <= parentNode.getBoundingClientRect().top) {
|
||||
// continue searching to the right
|
||||
start = middle + 1;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -14,12 +14,22 @@ function importAll(r) {
|
|||
}
|
||||
importAll(require.context('@/assets/icons/', true, /\.vue$/));
|
||||
|
||||
|
||||
Vue.use(Vuetify);
|
||||
|
||||
export default new Vuetify({
|
||||
icons: {
|
||||
iconfont: 'md',
|
||||
values: icons,
|
||||
},
|
||||
});
|
||||
export default function(ignoredconfig) {
|
||||
return new Vuetify({
|
||||
icons: {
|
||||
iconfont: 'md',
|
||||
values: icons,
|
||||
},
|
||||
options: {
|
||||
customProperties: true
|
||||
},
|
||||
theme: {
|
||||
options: {
|
||||
customProperties: true,
|
||||
},
|
||||
dark: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
export default {
|
||||
install(Vue, defaultServerFromLocation) {
|
||||
install(Vue, defaultServerFromLocation, onloaded) {
|
||||
var config = Vue.observable(require('@/assets/config.json'));
|
||||
const getRuntimeConfig = async () => {
|
||||
const runtimeConfig = await fetch('./config.json');
|
||||
return await runtimeConfig.json()
|
||||
const getRuntimeConfig = () => {
|
||||
return fetch('./config.json').then((res) => res.json()).catch(err => {
|
||||
console.error("Failed to get config:", err);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
config.promise = getRuntimeConfig();
|
||||
config.promise.then(function (json) {
|
||||
config.promise = getRuntimeConfig().then((json) => {
|
||||
// Reactively use all the config values
|
||||
for (const key of Object.keys(json)) {
|
||||
Vue.set(config, key, json[key]);
|
||||
|
|
@ -16,6 +17,12 @@ export default {
|
|||
if (!json.defaultServer) {
|
||||
Vue.set(config, "defaultServer", defaultServerFromLocation);
|
||||
}
|
||||
|
||||
// Tell callback we are done loading runtime config
|
||||
if (onloaded) {
|
||||
onloaded(config);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
Vue.prototype.$config = config;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default {
|
|||
userDisplayName: null,
|
||||
userAvatar: null,
|
||||
currentRoom: null,
|
||||
currentRoomIsReadOnlyForUser: false,
|
||||
currentRoomBeingPurged: false,
|
||||
notificationCount: 0,
|
||||
};
|
||||
|
|
@ -95,6 +96,16 @@ export default {
|
|||
this.currentRoom = this.getRoom(roomId);
|
||||
},
|
||||
},
|
||||
currentRoom: {
|
||||
immediate: true,
|
||||
handler(room) {
|
||||
if (room) {
|
||||
this.currentRoomIsReadOnlyForUser = this.isReadOnlyRoomForUser(room.roomId, this.currentUserId);
|
||||
} else {
|
||||
this.currentRoomIsReadOnlyForUser = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -102,35 +113,59 @@ export default {
|
|||
console.log("create crypto store");
|
||||
return new LocalStorageCryptoStore(this.$store.getters.storage);
|
||||
},
|
||||
login(user) {
|
||||
const tempMatrixClient = sdk.createClient(user.home_server);
|
||||
login(user, registrationFlowHandler) {
|
||||
const tempMatrixClient = sdk.createClient({baseUrl: user.home_server, idBaseUrl: this.$config.identityServer});
|
||||
var promiseLogin;
|
||||
|
||||
const self = this;
|
||||
if (user.access_token) {
|
||||
// Logged in on "real" account
|
||||
promiseLogin = Promise.resolve(user);
|
||||
} else if (user.is_guest && !user.user_id) {
|
||||
} else if (user.is_guest && (!user.user_id || user.registration_session)) {
|
||||
// Generate random username and password. We don't user REAL matrix
|
||||
// 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.
|
||||
//
|
||||
// Instead, we use an ILAG approach, Improved Landing as Guest.
|
||||
const user = util.randomUser(this.$config.userIdPrefix);
|
||||
const pass = util.randomPass();
|
||||
const userId = user.registration_session ? user.user_id : util.randomUser(this.$config.userIdPrefix);
|
||||
const pass = user.registration_session ? user.password : util.randomPass();
|
||||
|
||||
const extractAndSaveUser = (response) => {
|
||||
var u = Object.assign({}, response);
|
||||
u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response.
|
||||
u.password = pass;
|
||||
u.is_guest = true;
|
||||
this.$store.commit("setUser", u);
|
||||
return u;
|
||||
};
|
||||
|
||||
promiseLogin = tempMatrixClient
|
||||
.register(user, pass, null, {
|
||||
.register(userId, pass, user.registration_session || null, {
|
||||
type: "m.login.dummy",
|
||||
initial_device_display_name: this.$config.appName,
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("Response", response);
|
||||
var u = Object.assign({}, response);
|
||||
u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response.
|
||||
u.password = pass;
|
||||
u.is_guest = true;
|
||||
this.$store.commit("setUser", u);
|
||||
return u;
|
||||
return extractAndSaveUser(response);
|
||||
})
|
||||
.catch(error => {
|
||||
if (registrationFlowHandler && error.httpStatus == 401 && error.data) {
|
||||
const registrationSession = error.data.session;
|
||||
|
||||
// Store user, pass and session, so we can resume if network failure occurs etc.
|
||||
//
|
||||
var u = {};
|
||||
u.user_id = userId;
|
||||
u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response.
|
||||
u.password = pass;
|
||||
u.is_guest = true;
|
||||
u.registration_session = registrationSession;
|
||||
this.$store.commit("setUser", u);
|
||||
|
||||
return registrationFlowHandler(tempMatrixClient, error.data).then((response) => extractAndSaveUser(response));
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
} else {
|
||||
var data = {
|
||||
|
|
@ -284,11 +319,11 @@ export default {
|
|||
* Will use a real account, if we have one, otherwise will create
|
||||
* a random account.
|
||||
*/
|
||||
getLoginPromise() {
|
||||
getLoginPromise(registrationFlowHandler) {
|
||||
if (this.ready) {
|
||||
return Promise.resolve(this.currentUser);
|
||||
}
|
||||
return this.$store.dispatch("login", this.currentUser || new User(this.$config.defaultServer, "", "", true));
|
||||
return this.$store.dispatch("login", { user: this.currentUser || new User(this.$config.defaultServer, "", "", true), registrationFlowHandler });
|
||||
},
|
||||
|
||||
addMatrixClientListeners(client) {
|
||||
|
|
@ -355,6 +390,14 @@ export default {
|
|||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.power_levels":
|
||||
{
|
||||
if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) {
|
||||
this.currentRoomIsReadOnlyForUser = this.isReadOnlyRoomForUser(event.getRoomId(), this.currentUserId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.updateNotificationCount();
|
||||
},
|
||||
|
|
@ -422,10 +465,14 @@ export default {
|
|||
}
|
||||
});
|
||||
Vue.set(this, "rooms", updatedRooms);
|
||||
const currentRoom = this.getRoom(this.$store.state.currentRoomId);
|
||||
if (this.currentRoom != currentRoom) {
|
||||
this.currentRoom = currentRoom;
|
||||
}
|
||||
|
||||
const resolvedId = (this.currentRoomId && this.currentRoomId.startsWith("#")) ? this.matrixClient.resolveRoomAlias(this.currentRoomId).then(r => r.room_id) : Promise.resolve(this.currentRoomId);
|
||||
resolvedId.then(roomId => {
|
||||
const currentRoom = this.getRoom(roomId);
|
||||
if (this.currentRoom != currentRoom) {
|
||||
this.currentRoom = currentRoom;
|
||||
}
|
||||
}).catch(ignorederror => {});
|
||||
},
|
||||
|
||||
setCurrentRoomId(roomId) {
|
||||
|
|
@ -492,10 +539,11 @@ export default {
|
|||
|
||||
leaveRoom(roomId) {
|
||||
return this.matrixClient.leave(roomId, undefined).then(() => {
|
||||
this.$store.commit("setCurrentRoomId", null);
|
||||
this.rooms = this.rooms.filter((room) => {
|
||||
room.roomId != roomId;
|
||||
});
|
||||
this.matrixClient.store.removeRoom(roomId);
|
||||
//this.matrixClient.store.removeRoom(roomId);
|
||||
//this.matrixClient.forget(roomId, true, undefined);
|
||||
});
|
||||
},
|
||||
|
|
@ -549,6 +597,69 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the current user is joined to the given room.
|
||||
* @param roomIdOrAlias
|
||||
* @returns Promise<Bool> - Whether the user is joined to the room or not
|
||||
*/
|
||||
isJoinedToRoom(roomIdOrAlias) {
|
||||
if (roomIdOrAlias && this.matrixClient) {
|
||||
try {
|
||||
const resolvedRoomId = roomIdOrAlias.startsWith("#") ? this.matrixClient.resolveRoomAlias(roomIdOrAlias).then(res => res.room_id) : Promise.resolve(roomIdOrAlias);
|
||||
return resolvedRoomId.then(roomId => {
|
||||
return this.matrixClient.getJoinedRooms().then(rooms => {
|
||||
return rooms.joined_rooms.includes(roomId);
|
||||
});
|
||||
});
|
||||
} catch (ignorederror) {
|
||||
console.error(ignorederror);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
|
||||
isReadOnlyRoom(roomId) {
|
||||
if (this.matrixClient && roomId) {
|
||||
const room = this.getRoom(roomId);
|
||||
if (room && room.currentState) {
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (powerLevelEvent) {
|
||||
return powerLevelEvent.getContent().events_default > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isReadOnlyRoomForUser(roomId, userId) {
|
||||
if (this.matrixClient && roomId && userId) {
|
||||
const room = this.getRoom(roomId);
|
||||
if (room && room.currentState) {
|
||||
return !room.currentState.maySendMessage(userId)
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
setReadOnlyRoom(roomId, readOnly) {
|
||||
if (this.matrixClient && roomId) {
|
||||
const room = this.getRoom(roomId);
|
||||
if (room && room.currentState) {
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (powerLevelEvent) {
|
||||
let content = powerLevelEvent.getContent();
|
||||
content.events_default = readOnly ? 50 : 0;
|
||||
this.matrixClient.sendStateEvent(
|
||||
room.roomId,
|
||||
"m.room.power_levels",
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Purge the room with the given id! This means:
|
||||
* - Make room invite only
|
||||
|
|
@ -761,7 +872,17 @@ export default {
|
|||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: "joined",
|
||||
history_visibility: "invited",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.room.power_levels",
|
||||
state_key: "",
|
||||
content: {
|
||||
users: {
|
||||
[this.currentUserId]: 100,
|
||||
[userId]: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -799,6 +920,20 @@ export default {
|
|||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return true if this room is a direct room with one other user. NOTE: this currently
|
||||
* only checks number of members, not any is_direct flag.
|
||||
* @param { } room
|
||||
*/
|
||||
isDirectRoom(room) {
|
||||
// TODO - Use the is_direct accountData flag (m.direct). WE (as the client)
|
||||
// apprently need to set this...
|
||||
if (room.getJoinRule() == "invite" && room.getMembers().length == 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
on(event, handler) {
|
||||
if (this.matrixClient) {
|
||||
this.matrixClient.on(event, handler);
|
||||
|
|
@ -856,7 +991,7 @@ export default {
|
|||
return this.matrixClient;
|
||||
});
|
||||
} else {
|
||||
const tempMatrixClient = sdk.createClient(this.$config.defaultServer);
|
||||
const tempMatrixClient = sdk.createClient({baseUrl: this.$config.defaultServer});
|
||||
var tempUserString = this.$store.state.tempuser;
|
||||
var tempUser = null;
|
||||
if (tempUserString) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const vuexPersistLocalStorage = new VuexPersist({
|
|||
return {
|
||||
language: state.language,
|
||||
currentRoomId: state.currentRoomId,
|
||||
hasShownMissedItemsHint: state.hasShownMissedItemsHint,
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
|
|
@ -55,6 +56,7 @@ const vuexPersistSessionStorage = new VuexPersist({
|
|||
return {
|
||||
language: state.language,
|
||||
currentRoomId: state.currentRoomId,
|
||||
hasShownMissedItemsHint: state.hasShownMissedItemsHint,
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
|
|
@ -93,11 +95,14 @@ export default new Vuex.Store({
|
|||
},
|
||||
setUseLocalStorage(state, useLocalStorage) {
|
||||
state.useLocalStorage = useLocalStorage;
|
||||
},
|
||||
setHasShownMissedItemsHint(state, flag) {
|
||||
state.hasShownMissedItemsHint = flag;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
login({ commit }, user) {
|
||||
return this._vm.$matrix.login(user).then(
|
||||
login({ commit }, { user, registrationFlowHandler }) {
|
||||
return this._vm.$matrix.login(user, registrationFlowHandler).then(
|
||||
user => {
|
||||
commit('loginSuccess', user);
|
||||
return Promise.resolve(user);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue